每一SHE结构均拥有指向下一个SHE结构的指针,后面再连接一个指向ntdll!_except_handler4的异常处理函数指针。在通过覆盖SHE结构的栈溢出中,next SHE指针被一些字节码覆盖,而SHE handler被一个指向pop pop ret指令串的地址覆盖掉,该指令串位于一个non-SafeSEH模块中。
这种使用SEHOP的验证算法首次被A. Sotirov在Black Hat 2008大会上批露[3]。下面我们来看一下:
BOOL RtlIsValidHandler(handler)
{
if (handler is in an image) {
if (image has the IMAGE_DLLCHARACTERISTICS_NO_SEH flag set)
return FALSE;
if (image has a SafeSEH table)
if (handler found in the table)
return TRUE;
else
return FALSE;
if (image is a .NET assembly with the ILonly flag set)
return FALSE;
// fall through
}
if (handler is on a non-executable page) {
if (ExecuteDispatchEnable bit set in the process flags)
return TRUE;
else
// enforce DEP even if we have no hardware NX
raise ACCESS_VIOLATION;
}
if (handler is not in an image) {
if (ImageDispatchEnable bit set in the process flags)
return TRUE;
else
return FALSE; // don't allow handlers outside of images
} // everything else is allowed return TRUE;
}
[...] // Skip the chain validation if the
DisableExceptionChainValidation bit is set if (process_flags & 0x40 == 0) {
// Skip the validation if there are no SEH records on the
// linked list
if (record != 0xFFFFFFFF) {
// Walk the SEH linked list
do {
// The record must be on the stack
if (record < stack_bottom || record > stack_top)
goto corruption;
// The end of the record must be on the stack
if ((char*)record + sizeof(EXCEPTION_REGISTRATION) > stack_top)
goto corruption;
// The record must be 4 byte aligned
if ((record & 3) != 0)
goto corruption;
handler = record->handler;
// The handler must not be on the stack
if (handler >= stack_bottom && handler < stack_top)
goto corruption;
record = record->next;
} while (record != 0xFFFFFFFF);
// End of chain reached
// Is bit 9 set in the TEB->SameTebFlags field?
// This bit is set in ntdll!RtlInitializeExceptionChain,
// which registers FinalExceptionHandler as an SEH handler
// when a new thread starts.
if ((TEB->word_at_offset_0xFCA & 0x200) != 0) {
// The final handler must be ntdll!FinalExceptionHandler
if (handler != &FinalExceptionHandler)
goto corruption;
}
}
}
SHE handler指向POP POP RET指令串,next SHE中的栈地址被jmp 06 nop nop(机器码:EB 06 90 90)替换掉。当处理一个程序异常时,Windows将控制权传给SHE链表中的异常处理程序。当第一个异常处理函数被覆盖掉后,程序的执行流程重定向到POP POP RET指令串中(代替真实的异常处理函数)。该指令串将执行到当前SHE结构中next SHE的前两个字节(jmp 06),以此直接跳转到我们的shellcode。这样,SHE链表就被拆分了:
2.2 微妙之处
还有一个问题:哪一被编码的跳转指令可以代替4字节对齐地址?只有一指令与其相配:JE(机器码为0x74),该指令是一条件跳转:只有当Z标志被设置时才实现跳转。在Windows在处理异常时,Z标志是默认关闭的,我们必须通过执行一指令来计算出零值,以设置该标志位。Xor指令似乎是不错的选择,看来我们的解决方法已经很明了了:跳至XOR,POP,POP,RET指令串,以便将JE指令解释为JMP。这就是SEHOP绕过技术的微妙之处。
XOR,POP,POP,RET指令串并不难找到,我们可以通过查找返回NULL值的函数的末端来找到这些指令串:
XOR EAX, EAX
POP ESI
POP EBP
RET
为了确保该方案可行,我们就以XOR,POP,POP,RET指令串来编写一份PoC测试一下。
已知第一个SEH结构位于0x0022FD48,我们就可以在0x0022FD74处构造第二个SHE结构,它的next SHE指针值为0xFFFFFFFF,handler指向ntdll!FinalExceptHandler。如果我们将第一个SHE结构中的handler指向XOR,POP,POP,RET指令串,那么我们就可以将执行流程重定向到任何地方,真正地破坏掉SHE链表。具体地,我们可以重新伪造一个有效的SHE链表,以使其重定向到特定的shellcode。
另一个主要限制是Microsoft Windows 7和vista中的ASLR安全机制。我们知道程序的利用需要依赖于ntdll!FinalExceptHandler的地址,但这一地址在每次电脑重启后都会发生改变。Ntdll ImageBase在重启时都被随机化,这样就增加攻击的难度。我们针对ASLR(未经逆向分析)进行一些测试,结果表明ImageBase似乎在16位中只有9位发生随机化,也就是说,在超过512次机会中你只有一次机会可以成功地绕过SEHOP实现溢出攻击。
3. Proof Of Concept
3.1 目标程序与限制条件
我们编写一段小程序,以在Windows 7下演示这一技术。这一程序只是复制一个文件(OwnMe.txt)的内容到内存中,并在操作中实现栈溢出,以引发可被异常处理程序捕获的异常。
我们需要完成以下任务:
• 伪造一个有效的SEH链表
• 将最后一个SEH结构中的handler指向ntdll!FinalExceptHandler
• 在栈中放置shellcode(与Windows 7相兼容)
• 定位内存中的XOR,POP,POP,RET指令串地址
首先,XOR,POP,POP,RET指令串是已知的,因为我们已经将其放置在程序代码中了,我们可以发现该指令串位于内存地址0x004018E1中。
3.2 程序崩溃与利用
当程序崩溃时,可发现:
0012F700 41414141 Pointer to next SEH record
0012F704 41414141 SE handler
接着我们需要利用存储在0x0012F774中的第二个SEH结构来伪造SEH链表,这一SEH结构中next SEH指针值为0xFFFFFFFF,SHE handler指向ntdll!FinalExceptHandler。然后位于0x0012F700的SEH结构被篡改,使其指向仿造的SHE结构,并将handler设置为0x004018E1,同时在每一SHE结构之前写入一跳转指令(JMP+8),以避免数据段被执行。整个利用的工作流程如下图所示: