windows堆
堆数据结构
windows的堆由堆块和堆表组成
堆表:一般位于堆区的起始位置,用于索引所有堆块,包括堆块位置,大小,是否空闲
堆块:性能原因,一般分为大小不同的块.每个堆块都分为两部分,块首和块身.其中块首包含了描述本堆块的信息,包括大小,是否空闲等.块身紧跟在块首后面
申请堆空间的函数比如malloc,new等,其返回的地址是块身起始地址
占用的堆块被使用它的程序索引.空闲的堆块被堆表索引
堆表数据结构
空闲双向链表Freelist
,空表
快速单向链表Lookaside
,快表
Freelist
空闲堆块的块首有两个指针,用于将多个堆块链接成双向链表
堆表区有一个128项的指针数组,空表索引画在图上就长这样
每个空表索引后面拉着一溜全都是一样大小的堆块
空表索引free[n]
后面拉着的堆块大小都是\(n\times 8\ byte\)
特别的是free[0]
,其中的堆块都是1024
以上的,并且按照大小不降拉溜
空表索引就是包含两个指针的结构体,两个指针分别指向拉溜的头和腚.
相当于
1 | struct FreeListIndex{ |
意思意思就行了
Lookaside
块表用来加速堆分配,块表是单向链表,不会发生堆块合并
每个lookaside[n]节点,后面最多拉4个节点
后面最多拉4个节点,后面最多拉4个节点,后面最多拉4个节点
堆操作
对操作分为堆块分配,堆块释放,堆块合并
其中分配释放是程序员在程序中规定的,堆块合并由数据结构自己管理
堆块分配
快表分配,普通空表分配,0号空表分配
快表分配
1.找到大小匹配的空闲堆块
2.将该堆块的状态修改为占用
3.从堆表中把他卸下,也就是其前驱指向其后继
4.返回堆块块身首地址给程序指针托管
普通空表分配
寻找能够满足空间要求的最小堆块
零号空表分配
对于希望分配的空间大小x,首先反向搜索最后一个零号堆块,由于零号空表只增不减,最后一个零号堆块是最大的,如果最大的堆块没有x大,则零号空表分配失败
否则从头开始寻找第一个可以容纳x的堆块
空表找零钱现象
如果没有精确匹配希望分配空间大小的堆块,则拿一个大堆块,精确割出需要的空间大小,然后剩下的重新标注块首链接到空表中
快表只有精确匹配时才会分配,不会发生找零钱现象
堆块释放
1.修改堆块状态为空闲
2.堆块重新连入堆表末尾
堆块合并
将两个块从空表卸下,合并堆块,合并块首信息,然后将该堆块重新连入空闲链表
快表中的空闲块都会被设置为占用状态,不参与堆块合并
内存紧缩
堆分配释放算法
下面调试验证上述数据结构和操作
堆调试
书上调试的程序长这样
1 |
|
第七行用int 3指令下断点让程序停下,从这里附加调试器,因为如果直接从头调试运行的话,堆会装孙子,不使用快表
首先要了解一下这几个API的作用
API
HeapCreate
在进程地址空间中创建辅助堆
1 | HANDLE HeapCreate( |
返回值是该堆空间的句柄,也就是基地址
1 | hp = HeapCreate(0,0x1000,0x10000); |
这样用的意思是,没有访问限制,初始时分给0x1000字节的堆空间,最大可以要0x10000字节的堆空间,将该堆空间的句柄交给hp变量保管
HeapAlloc
1 | DECLSPEC_ALLOCATOR LPVOID HeapAlloc( |
hHeap是堆空间的句柄,可以是HeapCreate或者GetProcessHeap的返回值
其中HeapCreate是我们手动申请的堆空间,GetProcessHeap是进程堆空间,这个在程序运行开始就有了
如果函数调用成功则返回申请空间的基地址
1 | h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3); |
这样用的意思是,从刚才HeapCreate创建的堆空间中要一块3字节的空间,该块空间将会被初始化为0
HeapFree
释放HeapAlloc或者HeapReAlloc申请的堆空间
1 | BOOL HeapFree( |
1 | HeapFree(hp,0,h1); |
将h1这一小块还给hp
哪一块是堆?
ollydbg起来实时调试时,程序是停在0x40101D这个地方的
在反汇编视图上INT3发生于调用HeapCreate函数之后
显然此时EAX就存放的HeapCreate的返回值,即hp句柄,也就是申请的堆空间的基地址0x390000,显然是一个用户地址空间的值
在内存界面上它长这样
大小0x1000是我们给CreateHeap指定的初始值
双击该地址到内存视图看看
从0x390000开始,堆表中一次是段表索引,虚表索引,空表使用标识,空表索引区
堆块结构
两者的区别主要在于[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]这个索引项的
左手拉右手,右手拽左手
然后后面全都是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,这个位置长这样
Freelist[0]上只挂着尾块,因此前后指针都是指向尾块的,尾块的地址是0x00390688,即堆偏移0x688的地方
其他的空表索引Freelist[n]的两个指针都是指向自己,整个Freelist数组一共有128项,每项两个4字节指针,共128*4*2=1024=0x400
字节,从0x390178到0x390578全都是Freelist
一直持续到0x390578
分配
重新从win2k上用od调试了一下,现在堆基址是0x360000
下面是六次分割
1 | h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3); |
堆句柄 | 希望请求字节数 | 实际分配堆块的字节数 |
---|---|---|
h1 | 3 | 16 |
h2 | 5 | 16 |
h3 | 6 | 16 |
h4 | 8 | 16 |
h5 | 19 | 32 |
h6 | 24 | 32 |
分割之前
分割之前的尾块
flags=0x10表示尾块
第一次分割
第一次分割时
ESI作为第一个参数,是CreateHeap返回的堆句柄0x360000
8是要初始化该片空间为0,实际是枚举值HEAP_ZERO_MEMORY
3是希望申请的字节数
edi中放着的是AllocateHeap函数的地址
call edi之后0x360680附近发生了变化
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其上的值也修改了
实时修改为当前尾块data地址
六次分割后
继续调试,直到六次分配完成
1 | 00360680 02 00 08 00 00 01 0D 00 00 00 00 00 78 01 36 00 .........x6.//h1 |
此时尾块大小已经减为0x120(单位8字节),其两个指针仍然是指向0x00360178,
freelist[0]@0x00360178的两个指针必然也指向了0x360708
被申请走的堆块,对于系统中的堆来说,算是平白无故就没了,无法通过任何方式索引到拿走的堆块,只有是用户程序归还的时候,系统才可以把堆块重新放到Freelist里面,这时候就不是放到尾块里面了,16字节的块(包括首部)应该放到freelist[2]
释放
释放h1
释放是从h1开始的
1 | HeapFree(hp,0,h1); //free to freelist[2] |
esi=0x360000是hp句柄
dwflags=0没有任何标志
ebx是h1句柄,这里貌似有一个编译优化,在h1堆申请完毕之后,h1句柄是放在eax中的,在h2堆申请之前,eax又放到了ebx中,从那到现在ebx中一直都是h1句柄没有改变
调用之后eax=1,表示执行成功
此时的 h1堆块@0x360680发生了变化
1 | 释放之前 |
其变化为,标志位又回归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 | //三次释放前 |
三次释放前后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表也发生了变化
1
2
3
4
5 Freelist[0]@0x00360178
Freelist[1]@0x00360180
Freelist[2]@0x00360188
Freelist[3]@0x00360190
Freelist[4]@0x00360198
相当于画在下图中的状态
合并
前面三个释放,都不是连续的堆地址,现在要再释放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 | Freelist[0]@00360178 08 07 36 00 08 07 36 00 6.6. |
从h1,3,5释放完成后到h4释放完成后,发生变化的Freelist记录有
1 | Freelist[2]@0x00360188 |
可以看出Freelist[2]原来后面挂着两个节点h1@00360688,h3@003606A8,现在只剩下一个h1@0x00360688
Freelist[4]原来后面挂着h5@003606C8,现在指向自己了,后面啥也没挂
Freelist[8]后面挂了一个H@0x003606A8
也就是说消失了h3,h5,还有一个h4,恰好他仨是顺序的,是不是Freelist[8]后面挂着的这个就是呢?去0x003606A0看看这个堆块首部
1 | 003606A0 08 00 02 00 00 00 0A 00 ...... |
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 |
|
与调试空表时的区别就在HeapCreate函数这里
1 | hp = HeapCreate(0,0x1000,0x10000);//调试空表时 |
HeapCreate(0,0,0);
这样才能启用快表
call HeapCreate
调用HeapCreate之后,返回值堆区句柄eax=0x360000
此时的空表Freelist和之前不一样了,Freelist[0]@0x00360178挂着的尾块在0x00361E90了,原来是在0x360688
从尾块分配
1 | h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);//希望从hp堆申请8字节空间,初始化为0 |
这一条执行之后,返回的堆空间句柄eax=0x361E90,到这里看看
0x361E88开始的八个字节是堆块首
selfsize=0x0002(单位8字节),即堆块首加上data共16字节
Flag=0x01,已占用
unused bytes=8,有8字节尚未使用
此时Freelist[0]指向的尾块也发生了变化
Freelist[0]@0x00360178
尾块原来在0x00361E90,现在下移到了0x00361EA0
说明刚才的堆块仍然是从尾块上割出去的
当四条HeapAlloc都执行完成之后
1 | h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8); |
下面就到了HeapFree了,此时就和之前不同了
但是还给快表
调试空表时,HeapAlloc申请的空间,使用HeapFree之后会归给还到空表,并且相邻的堆块还会进行合并
而本次调试,HeapAlloc申请的空间,将会还给快表
释放h1
第一次释放的时h1@0x361E90
1 | HeapFree(hp,0,h1); |
到0x361E90看看
self size=0x02,即16字节
flags=0x01,因为快表中的节点都标记为已占用
unused bytes=0x08未使用字节数
再到快表看看
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看看
原来是h2指向了h1
目前快表是这样一个状态
h1的指针占用了h2data的前4个字节,但是没关系,再申请8字节的堆空间的时候,直接从Lookaside[1]拉的链上把最后面一个摘下来,前面的data就不用存最后这个的指针了
四次释放完毕后
将从尾块上割下来的h1,2,3,4都释放了,他们都会挂在lookaside上
画到图上是这样的
从快表分配
四次分配释放完成后,实际上干了一个瓜分尾块到快表的事,下面又要进行分配了
1 | h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16); |
16字节,正好Lookaside[2]上面挂着一个h3@361EB0,这个幸运儿又要得到程序的重用了
不出所料,调用HeapAlloc函数,返回之后,eax中的句柄值为0x361EB0
到这里去看看
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时,释放其中一个节点应该怎么写?
1 | struct Node{ |
然后篡改node节点的前后指针
然而这都是猜想,windows开发者怎么写的现在我们不知道,下面调试观察是不是这样
调试分配时DWORD SHOOT
调试程序是这样的
1 |
|
在win2k上,发行版的程序在INT3之后报错,此时附加od进行调试
六次HeapAlloc之后
此时程序刚执行了最后一个HeapAlloc,eax中还保存着句柄h6=0x5206D8
可以推测,堆区在0x520000,在内存映射视图中观察,确实如此
6次HeapAlloc都是瓜分的尾块
尾块现在的位置应该是位于h6的下面
h6块首@0x5206D0
h6data@0x5206D8
确实如此,尾块@0x526E8,两个指针都是指向Freelist[0]@0x520178
尾块首@0x5206E0.
h1,3,5释放之后
1 | Freelist[0]@00520178 E8 06 52 00 E8 06 52 00 ?R.?R. |
其中Freelist[0]两个指针都指向尾块,
Freelist[2].flink=h1@0x00520688
Freelist[2].blink=h5@0x005206C8
在图上就是这么一个状态
篡改h5的两个指针
下一次分配h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
将会把刚才释放的h5@0x005206C8再要回去,
在要回去之前要修改h3还有Freelist[2]的指针
在该次分配前,将h5的两个指针先改掉
修改之后
然后单步步过执行该分配函数,
此时控制跳转到0x77CCAAD位置,这应该是在HeapAlloc库函数或者其调用函数中了
尝试将ecx中的数据写到内存上ds:[EDX],ds即程序数据段选择子,此时ECX和EDX就是我们赋的两个值
ollydbg已经报错
利用DWORD SHOOT
利用点:
内存变量
代码逻辑
返回地址
异常处理机制
函数指针
PEB线程同步函数入口地址
书上的实验选定受害者是RtlEnterCriticalSection函数指针
该函数的作用是同步一个进程的不同线程,RtlEnterCriticalSection和RtlCriticalSection函数是访问临界区时需要调用的,保证线程对临界区的访问互斥
进程退出时,ExitProcess函数会调用这两个临界区函数,但是调用方式是函数指针
这两个函数指针在PEB+0x20处
RtlEnterCriticalSection@0x7FFDF020
RtlLeaveCriticalSection@0x7FFDF024
利用程序
1 | char shellcode[]= |
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
0x758开始的八个字节是尾块的两个指向Freelist[0]的指针
然后是将shellcode拷贝到h1
1 | 00401026 B9 80000000 MOV ECX,80;重复拷贝次数,0x80次 |
如此总共拷贝了0x80*4=512字节,显然要溢出尾块了.
拷贝完成后再看尾块
尾块的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 | 00424B08 mov eax,7FFDF020h ;程序PEB中线程同步函数的地址 RtlEnterCriticalSection函数指针的位置.放到eax中 |
三层循环解析函数地址,弹窗
剩下的shellcode之前已经分析过了