Heap Overflow - 103 By WraithOfGhost
시간이지남에따라잊혀지지않았으면하는힙공격을위한매우중요한 기법을설명하고자한다. 지금까지다룬문서의내용을간단하게정리하면 다음과같다. Heap Overflows 101 [ Windows XP SP1 이하환경에서의기본적인힙오버플로우 ] - 기본적인 unlink() 공격 + 함수포인터덮어쓰기 - 처리되지않은예외필터이용 - 벡터화예외처리이용 Heap Overflows 102 - 힙, 세그먼트, Lookaside List, FreeList, 덩어리헤더등과같은구조체와 RtlAllocateHeap(), RtlFreeHeap(), 블록합치기 / 나누기를포함하여할당 / 해제알고리즘과힙데이터구조체를좀더상세히다룸 - 안전한언링킹 (Safe Unlinking) - 힙헤더쿠기 - 익스플로잇기법 : ListHead 덮어쓰기 라고도알려진 Lookaside 덩어리덮어쓰기 Heap Overflows 102.5 - FreeList[0] 삽입공격기법 - 이뮤니티디버거를위한새로운힙플러그인!heaper Heap Overflows 103 [ 이번문서 ] - FreeListInuse bitmap에관한이론 - Nicoloas Wisemans의 FreeListInUse bitmap flip 공격 - 익스플로잇실패케이스소개 RtlCommitRoutine를이용한경우 + 해결하기위한솔루션제공 - heaper 플러그인을수정하여, FreeListInUse bitmap 분석을가능하게하고손쉬운코드관리를위해 Github 저장소를만듬
The FreeListInUse FreeListInUse 는힙베이스기준으로오프셋 0x0158 에위치한 16 바이트크기의구조체이고어떤 FreeList[n] 엔트리가해제덩어리 (free chunks) 를보유하고있는지를나타내는테이블을생성하는수많은바이트로구성되어있다. FreeListInUse bitmap 사용목적은 FreeList 로부터할당이발생될때 RtlAllocateHeap() 의속도를증가시키기위함이다. 할당과정동안힙매니저는할당사이즈로부터 8을더한다음나누어서비트맵을스캔한다. 예를들어할당크기를 664 바이트로가정한다면 672(+8) 가되고 8을나눔으로써 84=0x54( 8) 가된다. 따라서힙매니저는 FreeList[54] 부터스캔을시작하여아래쪽 (FreeList[54] -> 53 -> 52 순서 ) 방향으로스캔을계속진행한다. 이는순전히힙매니저의스피드를증가하기위한최적화방법이다. 위에서설명한 bitmap 이어떤모습인지살펴보면다음과같다. 32 비트의각각 4 longs 는총 128(FreeList 의정확한크기 ) 를의미한다. 그러나위에서 보여지는형식은약간알아보기어렵기때문에좀더자세히살펴볼것이다.
FreeList[n] 으로부터할당을하고, 할당된것이마지막덩어리라면비트를초기화 (unest) 한다. 또한 HeapFree() 를사용하여 FreeList 쪽으로해제될때 (lookaside 는가득찬상태 라고가정 ) 비트를세팅한다. ntdll 의!RtlAllocateHeap 코드는새로운비트값을생성 하기위해현재의비트를 1 과 XOR 연산한다. ; FreeListInUse modification: 7C910CEE. 0FB70E MOVZX ECX,WORD PTR DS:[ESI] ; ecx = chunk->size 7C910CF1. 8BC1 MOV EAX,ECX ; eax = ecx 7C910CF3. C1E8 03 SHR EAX,3 ; ListOffset = size / 8 7C910CF6. 8985 28FFFFFF MOV DWORD PTR SS:[EBP-D8],EAX 7C910CFC. 83E1 07 AND ECX,7 ; entrybyte = size & 7, ecx = entrybyte 7C910CFF. 33D2 XOR EDX,EDX ; edx = NULL 7C910D01. 42 INC EDX ; edx = 0x01 7C910D02. D3E2 SHL EDX,CL ; bytetoset = 1 << entrybyte 7C910D04. 8995 04FFFFFF MOV DWORD PTR SS:[EBP-FC],EDX 7C910D0A. 8D8418 5801000>LEA EAX,DWORD PTR DS:[EAX+EBX+158] ; eax = 0x004907A6 FreeListInUse Offset 7C910D11. 33C9 XOR ECX,ECX ; ecx = NULL 7C910D13. 8A08 MOV CL,BYTE PTR DS:[EAX] ; current_val = FreeListInUse[ ; FreeListInUseOffset ] 7C910D15. 33CA XOR ECX,EDX ; current_val = xor(current_val,bytetoset) 7C910D17. 8808 MOV BYTE PTR DS:[EAX],CL ; FreeListInUse[ FreeListInUseOffset ] = ; current_val
위과정에서중요한점은 bytetoset 는항상 1 이된다는것이다. xor 테스트를 해보면다음과같다. >>> current_val = 0 >>> bytetoset = 1 >>> >>> current_val ^ bytetoset 1 >>> >>> current_val = 1 >>> current_val ^ bytetoset 0 따라서우리는 FreeListInUse 엔트리의값이마지막값의무엇이었는지에따라변경되는것을알수있다. 좀더진행하기에앞서생각을하기바란다. XOR 연산이익스플로잇를하는동안영향을주게된다는것이현재상황이다. 만약우리가 FreeListInUse 의비트 1개를컨트롤할수있는상황이라면어떨까?
Exploiting FreeListInUse (bitmap flip 공격 ) FreeListInUse bitmap 의비트를반전 (0->1, 1->) 하기전에해당상황의결과부터보여줄것이다. FreeList[0x66] 엔트리에서힙매니저가사용할수있는가용해제덩어리가없다고가정한다. 가정된상황을나타내면다음과같다. FreeList[0x066] 0x00a804a8 -> [ flink: 0x00a804a8 blink: 0x00a804a8 ] 근본적으로위엔트리의 FreeListInUse 는 0과일치한다. 다만설명을위해 FreeListInuse 엔트리가 1로세팅되었다고가정한다. 만약우리가특정크기만큼 [ (0x66 * 0x8 / 8) 혹은이하 ] 할당을요청했다면, RtlAllocatewHeap() 함수는 FreeListInUse 를스캔하여할당크기에대응하는엔트리 (lookaside 는비어있다고가정 ) 를찾을것이다. 따라서요청은성공적으로수행되어 0x00a804a8 를 가용덩어리 로인식하여리턴 한다. 이때 FreeList[n] 가리스트엔트리자체를가리키고있기때문에만약가용 덩어리가없는경우해당힙을위해어떠한관리구조체값을리턴한다. 이시점에서 0x00a804a8 를기준으로 216 바이트를덮어쓰고, 오프셋 0x57c 에있는 RtlCommitRoutine 포인터를덮어쓰는것은간단하게이루어질수있다. 그럼에도불구하고이공격기법을테스트하고 RtlAllocateHeap() 를리버싱할때본인은 FreeListInUse 엔트리값을반전시킨후에특정한조건 1개가필요하다는것을알아차렸다. 우리가할당하고자하는덩어리는리스트의마지막덩어리여야만한다. 아니라면 RtlAllocateHeap() 가뒤에위치한 ( 마지막이아니기때문에뒤에엔트리들이존재 ) 엔트리들에 접근하여접근위반오류가나타난다. IDA 를통해코드를보면다음과같다.
살펴봐야할명령어는 TEST CL,0x10 이다. 우린 0x10 이엔트리의마지막덩어리를나타내는것을이미알고있다. 따라서 Freelist[n] 엔트리를살펴볼때덩어리의주소는덩어리헤더가아니라 flink/blink 를가리키고있을것이다. 따라서덩어리가제대로존재하는지 ( 있는지 ) 알아보기위해검사할때이와같은체크과정에서실패하게된다면, 덩어리주소 0x8을한다음덩어리헤더에있는덩어리플래그오프셋을위해 + 0x5를한주소를살펴봐야한다. 가장가독성이높은형태로나타내면다음과같다. 그리고 0x00a804a8 에위치한덩어리를덤프하면헤더는 0x00a804a0 에위치하고 덩어리플래그는 0x00a804a5( 다음사진에서커서가위치한곳 ) 에위치하게된다. 만약우리가해당 ( 덩어리플래그 ) 값을알려진값 (0x04 대신에 0x10 으로 ) 으로변경하면 명령어는상수 0x10 을대상으로 TEST 연산을한다. 그러면체크과정이실패하고 jump 가이루어지지않는다. ( 물론 CMP 명령어가더좋은옵션임 ) 다음의사코드는방금 설명한것들을수행한다. if (chunk->flag & 0x10) <= 0: walk_freelist_entry() 이제방금설명한것처럼다음과같이값을변조한다. 어떤 16진수문자를사용해도상관없지만해당문자는반드시 8 비트표현단위에서 0x5 위치에바이너리값을포함하고있어야한다. 체크를우회하기위한것으로 00010000 등의값을사용하면된다. 물론이는정확히 256/2, 다시말해총 128 가지바이트를사용하여마지막덩어리의체크를우회할수있다는뜻이다. 값들의리스트는다음과같다.
0x10-0x1f 0x30-0x3f 0x50-0x5f 0x70-0x7f 0x90-0x9f 0xb0-0xbf 0xd0-0xdf 0xf0-0xff 이전에언급했듯이, Freelist 는 0x0178 에서시작해서 0x0570 에서끝난다. 그러므로 사용할수있는값의범위는 freelist[n] 자체의할당작업에서절대제대로동작하지 않는 0x01-0x05 이다. 성공하는한가지방법은 FreeList[n] 엔트리내부에해제 덩어리가있게만드는것이다. 해당덩어리의주소는반드시위에서언급한바이트 값을좌측을기준으로 3 번째위치에가지고있어야한다. 에를들어다음과같은 0xXXXXYYXX 주소에서 YY 는위리스트바이트중하나와동일해야한다. 다시 이런개념 / 현상을가독성을높인형태로나타내면다음과같다. 이전엔트리가 0x4 대신에 0x32 바이트값을가지는것을볼수있다. 해당값을 덤프창에서보면다음과같다. 0x32 값이체크로직을우회하는것일까?? 그렇다, 왜냐하면해당값은 False 를 리턴하여점프가수행되지않기때문이다. >>> print (0x32 & 0x10) <= 0 False 따라서점프는수행되지않고, unlinking 과정은우리가 HeapAlloc(984) 를수행했 다고생각하여시작되게된다. 이시점에서우린 EAX 레지스터에 0x00490558 를 리턴값으로가지게된다.
Flipping the bit 비트반전시키기 FreeList[n] 가짜덩어리를할당하기전에어떤방식으로던 bitmask 를반전시켜야 한다. 지금까지본인은총 3 가지방법을알아내었다. 힙오버플로우를발생시키고 FreeList[n] 덩어리 ( 해당 FreeList[n] 엔트리에있는유일한덩어리여야만함 ) 크기를반전시키고자하는크기로변경한다. 따라서해제된오버플로우덩어리 (free overflow chunk) 가할당되면해당덩어리는 FreeList 엔트리를변경된크기로변경한다. 힙오버플로우를발생시키고 flink/blink 크기를변경하고 FreeList[n] 덩어리의플래그를 0x10으로변경 (FreeList[n] 엔트리가복수의덩어리를가져도상관없음 ) 한다. 그러면 flink/blink는동일한값을가지게되고플래그가세팅되었으니알고리즘은해당덩어리를 FreeList 엔트리의마지막덩어리로인식한다. 해당덩어리를할당할때 FreeListInUse 엔트리가가리키는변경된크기는 1로변경되게된다. int (ptr) 를통해근본적인제어를얻는다. FreeListInUse를빈엔트리 (empty entry) 를위해변경한다. 엔트리크기 8 만큼의크기를가지는덩어리를할당한다. 누가꼭힙오버플로우가필요하다고말했는가? 자세한것은조금있다설명할것이다. 테스트 & 분석과정을통해본인은 FreeListInuse bitflip( 비트반전 ) 을제어 하는것의중요성을이해하였다. 이와같은동작을할수있게해주는함수를!heaper 플러그인에포함시켰다.
0x00490000 힙을위한 FreeListInUse 를살펴보면다음과같다.
FreeListInUse 를위해 FreeList[20] 엔트리를변경할것이다. 이제우리가필요한것은확실하게해제덩어리 (free chunk) 가 FreeList[0x1C] 에 있게만드는것과해당덩어리플래그를위해사용될수있는값을가지고 있게만드는것이다. 2007 년 5 월 30 일, Nicolas Waisman 은 DailyDaves 메일링리스트에보안 전문가들에게기본적인제어권을얻은상황이라고가정한후어떻게그들이 inc[r32] 명령어를익스플로잇할수있는지에대한수수께기를작성하였다. Lets have a fun riddle to cheer up the spirit (Mate at 11pm, its all night insomnia). The riddle: Let said you are trying to exploit a remote service on an old Windows 2000 (whatever SP you want) and the primitive is the following inc [edi] // you control edi. What would be the best option for edi? Nico
만약우리가기본적인제어권을얻을수있고, 특정주소와상관없이포인터를여러번증가시킬수있다면다음과같은공격을진행할수도있다. - heapbase 주소는 0x00490000 - FreeList[0] 을제외하면그어떤 FreeList[n] 엔트리도가지고있지않음 - EDX 레지스터는제어가능함 - 실행하고자하는명령어는 inc byte [edx] ( 증가 ) 형태임 - 증가명령어를여러번수행함 1) EDX 레지스터를 0x0049015C 로세팅하고, FreeListInUse 를변경한다. FreeListInUse 는현재다음과같은형태를가진다. 2) 엔트리자기자신 ( 가짜덩어리 ) 을가리키는 FreeList[0x20] 에위치한덩어리의 가짜헤더플래그가올바르게세팅되게만들어야한다. 현재값은다음과같다.
기본적인제어권이있다고가정한상황이기때문에이를이용하여간단하게 해당값을 0x10 으로증가시킬수있다. 이제 0x20 크기의할당이요청되면, 힙매니저는 FreeList[20] 엔트리자체를 (0x00490278) 리턴할것이다. - 힙오버플로우를유발시킬필요가없었음 - FreeList[n] 에존재하는해제덩어리 (free chunks) 가필요없음 - inc byte [r32] 명령어를위해기본적인제어권이필요함 - 특정크기를가지는 2개의할당에대한제어권이필요함. - 1개는 FreeList[n] 엔트리를가용덩어리로만들기위함 - 1개는강제적으로할당자 (allocator) 가더많은메모리를할당하게만들어 RtlCommitRoutine() 함수포인터를유발하기위함
Exploitation via RtlCommitRoutine RtlCommitRoutine 를이용한익스플로잇 RtlCommitRoutine 를이용한익스플로잇은본인에게있어서 ( 최소한지금은 ) 성공적이지않다는것을증명해주었다. 원인은오프셋 0x57c 에위치한 RtlCommitRoutine 를메모리에서호출하기전에힙구조체에서수많은포인터가망가지고해당포인터를참고하여읽거나쓰기때문이다. 이와같은포인터로는오프셋 0x578 에위치한 Lock 변수가있다. 그럼에도불구하고 ( 성공적이지않아도 ) 본인이트리거하려는코드를살펴보면다음과 같다. 7C918B26. 8B88 7C050000 MOV ECX,DWORD PTR DS:[EAX+57C] 7C918B2C. 85C9 TEST ECX,ECX 7C918B2E. 0F85 9F210300 JNZ ntdll.7c94acd3 ;jump taken if ecx!= 0 7C94ACD3 > 57 PUSH EDI 7C94ACD4. 8D55 0C LEA EDX,DWORD PTR SS:[EBP+C] 7C94ACD7. 52 PUSH EDX 7C94ACD8. 50 PUSH EAX 7C94ACD9. FFD1 CALL ECX <- code execution 일반적인 write4 기법을사용할수있거나 lookaside 리스트덩어리의 flink 를덮어쓰고리턴할수있다면포인터자체를덮어쓸수있을것이다. heapbase+0x57c -> heapbase+0x608 -> RtlCommitRoutine heapbase 가 0x0049 라고가정한다면, flink를 0x00490608 로세팅하고해당주소에임의적인코드 ( 대부분쉘코드 ) 를작성할수있을것이다.
How did I fail? 어떻게 / 왜실패했을까? 실패원인을보기전에우선우리는 0x7C90100B 주소에서시작하는 2 가지 체크로직을우회해야한다. 힙구조체내에있는 lock 변수오프셋에서 0xffffffff00000000 를가리키는포인터를 이용해 ( 힙오버플로우를통해해당값을이용할수있음 ) 체크로직을우회할수있다. 그 다음에는 FreeList[0] 에해제된덩어리가있는지없는지검사한다. 그다음오프셋 0x668( 본인예상으로해당오프셋은 FrontEndHeapType 를참조하는듯 ) 과 0x654 에대하여특정값과일치하는지검사하는코드로이동된다. 다만현재 EAX 레지스터값 (0x00490640) 이잘못설정된것일수있기때문에위오프셋들역시잘못된값일수있다고본인은생각한다.
결과적으로모든체크로직을통과하면, sub_7c919ae3 함수를호출하게된다. IDA 에서본 sub_7c918ae3 함수코드를통해우리는힙구조체에있는어쩌면 잘못된값일수있는오프셋들에대한추가검증로직을볼수있다. 그리고나면우리의쉘코드를실행하는코드루틴으로이동된다.
물론이는그저한가지코드흐름일뿐이고본인은아직다른코드흐름들에대한분석을완료하지않았다. 그러나살짝훑어본결과우리가변경한포인터를실행하는함수에대한 2개의호출을알아내었고호출이되어변경포인터가실행이되면 2개의코드흐름 ( 약간해석하기애매한데아마도분석한것과분석하지않은것총 2개를의미하는듯 ) 은 RtlAllocateHeap 에서시작할것이다.
A possible solution 공격이실패되지않게사용할만한방법 공격성공을방해하는장애물들을해결하기위해수많은방법을고려한 결과나는사용할만한방법을찾아내었다. 그방법은바로버퍼를할당하고 RtlCommitRoutine 포인터직전까지꽉채우는방법이다. 위와같은방법을이용해도아직몇몇의포인터들은망가지게되는데, 이는약간의희망 (?) 을제공한다. 왜냐하면이론적으로우리는 FreeList[0] 가빈 (empty) 상태라는것과 heapbase+0x570->heapbase+0x570에위치하는자기자신을가리키는포인터와우리가망가뜨린 lock 변수를바꿀수만있으면되기때문이다. 참고로실제예제에서는대부분 heapbase에널바이트가존재하지않는다. FreeList 를좀더깊게조사하면특정한크기를알아챌수있다. 우린나머지 FreeList 부분을망가뜨리고포인터만덮어쓰는할당을생성할수있다. Heap dump 0x00490000, item 75 Address=0x00490380 Chunks=[041] 0x00490380 -> [ 0x00490380 0x00490380 ] 예를들어위에서설명한엔트리를위한계산된사이즈는 0x41 * 8 8= 0x200 바이트이거나할당의최대크기인 512 바이트이다. 0x0049057C(RtlCommitRoutine 포인터 ) 와 0x00490380(FreeList[41] 덩어리주소 ) 와의거리는 508 바이트이다. 하지만만약우리가 FreeList[41] 로부터할당을한다면버퍼를 512 바이트까지채워넣는다면기본적으로그냥할당만수행하여 heap base의오프셋 0x57C에위치한포인터만덮어쓸수있다. 물론 0x578, 0x570 에위치한포인터를복구해야하지만, 다행히이는실행 성공확률이꽤높은작업이다.
추가사항 : 이론이실제로동작하는듯하다. 우린가짜덩어리헤더가 덩어리플래그 (chunk flag) 와현재크기를위해올바른값을가지도록확실히만들어줘야한다. 만약임의적인할당을수행하고해제할수있는능력이있다면요청한할당크기에맞는덩어리가나올때까지 FreeList[40] 에계속덩어리를위치시킬수있다. ( 할당 & 해제무한반복 ) 빙고! :
The attack by example 예제를통한공격 다음의 FreeListInUse.c 소스코드는예제를위해억지로만든코드이다. 직접컴파일하고실습을따라올수있게만들었다. 코드를다운로드하고 원하는컴파일러로컴파일하면된다. 나는 Dev c++ 를사용하였다. * 공격이제대로수행되지않을수있는데왜그런지이해하려고노력 하기바란다. #include <stdio.h> #include <windows.h> int main(int argc,char *argv[]) { char *a,*b,*c,*d,*e,*f,*g,*h,*i,*j,*k,*l,*m,*trigger; char *x, *y, *w, *u, *z, *q, *o; long *hheap; hheap = HeapCreate(0x00040000,0,0); a = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16); b = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16); c = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16); d = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16); e = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16); f = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16); // 2 개의사용중인덩어리가존재하기때문에 freelist[0x3] 로강제이동 g = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16); // 덩어리근처 // freelist[0x7b] 를위한할당 z = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,976); x = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,976); y = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,976); w = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,976); q = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,976); u = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,976); // 2 개의사용중인덩어리가존재하기때문에 freelist[0x7b] 로강제이동 o = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,976); // 덩어리근처 // lookaside[0x3] 채우기 HeapFree(hHeap, 0, a); HeapFree(hHeap, 0, b); HeapFree(hHeap, 0, c); HeapFree(hHeap, 0, d); // freelist[0x3] 으로삽입 HeapFree(hHeap, 0, f); printf("(+) Chunk e: 0x%08x\n",e); printf("(+) Fill chunk e (using 16 bytes), overflowing into chunk f (0x%08x) by 1 byte:\n",e); printf("(+) Overflow with size 0x7c (AAAAAAAAAAAAAAAA )...\n"); gets(e); // lookaside 에존재하는덩어리제거 h = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16); i = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16); j = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16); k = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16); // 비트맵을반전하는부분. FreeListInUse[0x7c] = 1 l = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16); // lookaside[0x7b] 채우기 HeapFree(hHeap, 0, z); HeapFree(hHeap, 0, x); HeapFree(hHeap, 0, y); HeapFree(hHeap, 0, w);
// lookaside[0x7b] 채우기 HeapFree(hHeap, 0, z); HeapFree(hHeap, 0, x); HeapFree(hHeap, 0, y); HeapFree(hHeap, 0, w); // freelist[0x7b] 으로삽입 HeapFree(hHeap, 0, u); // freelist[0x7c] 에서자신을가리키는포인터리턴 => 그러면우린관리구조체를덮어쓸수있음 m = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,984); printf("(+) Fill chunk m and destroy the management structure:\n"); gets(m); // RtlCommitRoutine 를실행하게위해강제로힙을크게만듬 trigger = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,4096); } exit(0); 이뮤니티디버거에서컴파일한바이너리를열고 0x004016C4, 0x00401612 (2 개모두 HeapAlloc 호출주소 ) 에 BP 를설정하고다음과같은명령어를실행한다.!hidedebug ZwQueryInformationProcess FreeListInUse, FreeList[n] 을분석할때우린 0x3 엔트리가해제된덩어리로 세팅된것을알수있다. 크기값 0x010 을기억하기바란다.
덩어리크기만큼덮어쓰고 (1 바이트덮어쓰기 ) 할당을유발할것이다. 그러면 우리가덮어쓴값과함께 FreeListInUse 엔트리에대응하는크기만큼반전이 일어난다. 15개의아스키바이트 + 문자열 ( 총 16글자 ) 을입력값으로이용한다. 예를들면 AAAAAAAAAAAAAAA 이다. 크기값을 \x7c 로덮어쓴다음엔터키를누른다. 그러면이전에설정한첫번째 BP가있는 0x00401612에서멈춘다. 덩어리가제대로존재하는지다시확인한다. 참고로이단계에서 0x7c 를 위한 FreeListInUse 엔트리값은아직 0 이지만 FreeList[0x3] 에위치한 덩어리에위치한덩어리내에있는크기값을덮어썼다.
F8 를누르면 HeapAlloc() 코드를 Step-Over 하고 FreeList, FreeListInUse 테이블을볼수있고, 0x3 를위한 FreeListInUse 엔트리값이원하는데로 1 로 세팅되었지만아직그어떠한덩어리도존재하지않는상태이다. 우리의목표는 0x7c 의비트를반전하는것이기때문에반전유무를확인한다. 정상적으로반전된것을볼수있다. 이제우리의 2번째 BP에서코드는 0x7c 엔트리로부터할당을시도할것이다. 그러나우리가 0x7c 이전에위치한덩어리를해제하지않는이상할당시도는실패한다. 어플리케이션을실행하면 0x004016C4에위치한 BP에도달한다음 0x7b 쪽으로덩어리가해제되는것을알수있다. 이는 FreeListInUse가잘세팅된것을의미한다고추정할수있다. 함수호출을넘어가기위해 f8 을누른면 EAX 레지스터값이 0x00490558 (FreeList[0x7c] 자체값 ) 로세팅된것을볼수있다.
이제남은할일은오프셋 0x57c에위치한 RtlCommitRoutine를덮어쓰는것이다. 0x57c-0x558=36 바이트 + 4 바이트 ( 포인터제어를위한 ) 만큼덮어쓰면된다. 참고로할당을하는순간오프셋 0x558에위치한모든포인터를망가뜨린다는것을기억하기바란다. 그러나지금은일단위사진에서보여지는구조 ( 체 ) 를 gets() 와 \x41 * 36 + \x44 * 4 를이용하여포인터를전부 D 로만들어서덮어쓴다.
이시점에서, 당신은힙확대 ( 힙세그먼트로부터더많은메모리커밋 ) 를유발해야한다. FreeList[0] 에있는그어떠한덩어리보다큰크기로 HeapAlloc() 을실행하면쉽게수행할수있다. 물론당신은 0x7c 다음에덩어리가존재하지않기때문에 FreeList[n] 으로부터어떤크기의덩어리라도할당할수없다. 참고로이번문서에서모든해결방안을서술하지않았는데, 왜냐하면독자가스스로이런방법이왜내가제공한코드에서제대로동작하지않는지에대해생각하고이해하게만들고싶기때문이다. 힌트는크기를보면된다. ( 이부분은이전에사용할만한방법에서서술하였음 )
Conclusion 결론 bitmap 반전공격이꽤놀라운기법이기때문에공격자에게꽤많은어려움을제공한다. 올바른환경이라면이공격이힙할당자 (allocator) 와관리구조체 (management structures) 를공격하는데완벽한기법이라는것에는의심할여지가없다. 실제공격에서어려움은가짜헤더가올바른크기와플래그값을가지게만드는것이다. 실행하기어려운기법이긴하지만동작시키기위해힙오버플로우가필요하지않기때문에다른어플리케이션특화공격에비하여보다많은케이스에적용할수있다. 이기법을발견하고이해하는데도움을준 Nicolas Waisman 에게감사의마음을전한다. 또한그의 Heaps about heaps 연구결과를발표해준 Brett Moore 에게도마찬가지로감사의마음을전한다. 마지막은아니지만최소한이튜토리얼을작성하면서!heaper 플러그인코드를계속수정할것이기때문에업데이트된최신버전은여기에서다운로드할수있다. References 참고문헌 Abusing the bitmask Heaps about Heaps