selph
selph
发布于 2022-03-23 / 475 阅读
0
0

漏洞学习--堆调试&识别

堆的调试方法

实验环境:

  • Windows 2000虚拟机
  • Visual C++6.0
  • 默认编译选项(VS2003 2005需要把GS编译选项关掉)
  • build版本:release版本,使用debug版本实验会失败

实验代码:

#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);

	HeapFree(hp, 0, h1);
	HeapFree(hp, 0, h3);
	HeapFree(hp, 0, h5);
	
	HeapFree(hp, 0, h4);
	return 0;
	
}

堆调试不能直接用调试器直接加载程序,堆管理函数会检测当前进程是否处于调试状态,而使用调试状态的堆管理策略

调试态的堆管理和普通的堆管理有很大差异:

  • 调试的堆不使用快表,只使用空表分配

  • 所有堆块会加上多余的16字节尾部防止程序溢出,包括8个字节0xAB和0x00

  • 块首标志位也不一样

正常运行需要进行配置:在程序中设置断点__asm int 3,设置调试器(OD)为实时调试器接管程序错误,编译release程序运行发生错误时,如图,点击取消来启动调试器接管

image-20220223182812112

malloc函数会在内部自己使用HeapCreate函数为自己创建堆区

点击OD上面的按钮M可以查看内存映射:

image-20220223183941447

识别堆表

程序初始化过程中,malloc使用的堆块和进程堆已经用过很多次了,已经比较乱了,所以自己建一个堆来理解堆管理策略

HeapCreate创建堆之后返回地址到eax:这里是0x00520000

image-20220223185543855

堆表中包含的信息依次是段表索引,虚表索引,空表使用标识,空表索引区

(图截自《0day安全》)

image-20220223190125265

与堆溢出利用关系比较大的是空表索引区,位于0x178偏移处

当一个堆刚初始化完成后,堆块状况如下:

  • 只有一个空闲态的大块--尾块
  • 位于堆偏移0x688处(这里是x0x00520688)
  • freelist[0]指向尾块
  • 除了零号空表外,其余各项索引都指向自己,意思是都是空表

堆块首部

占用态堆块首部:

image-20220223194028903

空闲态堆块首部:

image-20220223194113447


空闲态和占用态的堆块首部结构基本一致,区别是在首部后数据区的前8字节,空闲态堆块会保存空表指针,当变成占用态时,着8个字节会用于存数据


查看创建的堆块:(只有一个空闲态的大块)

image-20220223194703448

  • 堆块开始于0x00520680,一般引用堆块的指针会越过堆首8字节直接指向数据区
  • 尾块大小为0x0130,计算单位是0x0008字节,共0x980字节
  • 堆块大小包括块首

按照堆表数据结构的规定,指向快表的指针位于偏移0x584字节处

指向空表的指针位于0x178处,一共有128个双向链表,结尾地址也就是0x178+0x400=0x578,中间距离一堆0,0x578处有一个数据,然后距离16字节的0之后,便是0x584字节处的快表指针

堆块的分配

堆块分配里:

  • 堆块大小包括堆首大小
  • 堆块的单位是8字节,不足8字节按8字节分配
  • 初始状态使用次优块分配,分配函数会从尾块中切走小块,修改首部信息,再把尾块重新放回0号链表

把这6次申请内存都执行了,分配字节情况如下:

	h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3);	// 8 bytes + 8 bytes
	h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,5);	// 8 bytes + 8 bytes
	h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,6);	// 8 bytes + 8 bytes
	h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);	// 8 bytes + 8 bytes
	h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,19);	// 24 bytes + 8 bytes
	h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);	// 24 bytes + 8 bytes

一共分配了2+2+2+2+4+4 = 0x10个堆块大小(堆管理里以堆块(8字节)为单位计算大小),所以剩下的空间应该是0x130-0x10=0x120个堆块大小:

image-20220223232445988

堆块的释放

接下来释放堆块h1,h3,h5,三个不连续的堆块,查看内存:

image-20220224002956088

这三个堆块变成了空闲堆块,数据区添加了双链表指针指向空表(h1和h3在free[2],h5在free[4])

堆块的合并

接下来释放h4,查看内存:

image-20220224004957027

这里h3,h4,h5都是空闲的且连续,则会进行堆块合并,这三个块从空表中摘下,合并后再插入空表

合并后的大小是2+2+4=8个块,链入free[8]

这里合并的结果在内存上的体现是,改变了原h3位置处的堆块大小,双向链表指针,对于其数据区的内容不进行改动

堆块合并可以更加有效的利用内存,但需要修改多处指针,比较费时,因此,堆块合并只发生在空表中,在快表中被禁止

快表的使用

实验代码:

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

为何这个代码会有快表用而上个代码却没快表用?

问题出在申请堆区的API:HeapCreate

这里的参数不同,第三个参数是最大堆区大小,设置为0则表示这是动态大小,可用大小上限是内存大小,可能会很大,则会启动快表来帮助快速定位


查看空表地址,偏移0x178处,freelist[0]地址为:0x00521E90

image-20220224011114496

跳转到freelist[0]处可见:

image-20220224011213817

尾块的大小是0x178个


代码接下来进行4次申请和4次释放操作,会将分割出来的块都填充到快表里(原先0x688偏移处是尾快,现在是快表):

image-20220224011603954

从上到下的红框,依次是lookaside[0]到lookaside[3]

查看lookaside[2]的堆块首部信息:

image-20220224012053009

前8字节是首部信息,后16字节是数据区,首部里的第六个字节为01,标识这个块为busy状态,也就是为什么不能进行合并堆块的原因

现在看lookaside[1]的两个堆块:

image-20220224012416746

这里的数据区4个字节保存单向链表下一个节点的地址,先释放的h1,再释放的h2,这里的指针是原h2的指针指向原h1的地址,所以这里是头插法插入快表的


接下来再次申请堆内存,申请16字节,也就是2个堆块大小,快表如下:

image-20220224012651538

这里原本是lookaside[2]的指针,因为这里的块被申请了,所以这里不再指向任何空闲块,所以就清空了,当再次释放后,这里的指针又会被设置回来


评论