首页
社区
课程
招聘
[原创]快速定位windows堆溢出
发表于: 2022-7-20 10:25 17965

[原创]快速定位windows堆溢出

2022-7-20 10:25
17965

windows的堆溢出,具有一定的滞后性,溢出后有时不会立即崩溃,可能运行一段时间后再崩溃,此时查看调用堆栈,也不是溢出的第一现场,此时的调用堆栈意义也不大,看不出什么猫腻,所以堆溢出,对开发人员排查具有一定的难度。本文带你寻找定位堆溢出的第一现场。

1、一个堆有若干个堆段(Segment)组成,每个堆段是连续的空间,一个堆最多有64个堆段

2、一个堆段由若干个堆块(Entry)组成。

备注:使用malloc/new申请的内存就是占用一个堆块(Entry),一个堆块(Entry)中只有User Data部分是malloc/nw

返回给应用程序使用的地址,其他的部分为当前堆块(Entry)的元数据,用于堆管理,在调试模式下还有一些用于调试的信息。

在这里插入图片描述

                                                                            (堆结构)


下图显示了一个win32堆所拥有的内存块,左侧的大矩形是这个堆创建之初所批发过来的第一个段(Segment),我们将其称为0号段,每个堆至少拥有一个段,即0号段,最多可以有64个段,在0号堆段的开始处存放着堆的头信息,是一个_HEAP结构,其中定义了很多个字段来标记堆的属性和“资产”状况,比如Segment这个数组记录了这个堆拥有的所有段,每个堆段都用一个HEAP_SEMENT结构来描述自己,对于0号堆栈,这个HEAP_SEGMENT结构位于_HEAP结构之后,对于其他堆段,这个HEAP_SEGMENT结构就在段的起始处。例如下图右侧上面的矩形代表了这个堆的1号段,它的最上方就是一个HEAP_SEGMENT结构



下图展示了堆是如何被破坏的,假设有两处应用程序申请的内存,分别为堆块1(Entry1)和堆块2(Entry2)管理,并且是连续的内存,那么这个时候拷贝

数据到堆块1(Entry1)的user Data部分,但是没有控制好拷贝的长度,覆盖了后面堆块2(Entry2)的元数据(Meta)和用户数据部分(User Data),

当上述堆被破坏发生时,可能并不会立即报错,而是在下次在分配内存或释放的时候,和这个堆块相关联的操作检查到堆破坏,这时候崩溃。


                                                                (堆破坏)


图1画出了页堆的结构,其中的地址是以x86系统中的一个典型页堆。左侧的矩形是页堆的主题部分,右侧是附属的普通堆,创建每个页堆时,堆管理器都会创建一个附属的普通

堆。

页堆上空间大多是以内存页来组织的,第一个内存页(起始4KB)用来伪装普通堆的HEAP结构,大多数空间被填充为0xeeeeeeee,只有少数字段(Flags和ForceFlags)是有效的,

这个内存页的属性是只读的。因此可用来检测应用程序意外写HEAP结构的错误。第二个内存页的开始处是一个DPH_HEAP_ROOT结构,该结构包含了DPH的各种信息和链表,是描述和管理页堆的重要资料。它的第一个字段是这个结构的签名(signature),固定为0xffeeddcc,与普通的堆结构的签名0xeeffeeff不同。它的NormalHeap字段记录着附属普通堆的句柄。

DPH_HEAP_ROOT结构之后的一段空间用来存储堆块节点,称为堆块节点池(node pool)。为了防止堆块的管理信息被覆盖,除了在堆块的用户数据区前面储存堆块信息,页堆还会在节点池中为每个堆块记录一个DPH_HEAP_BLOCK结构,简称为DPH节点结构。多个节点是以链表的形式链接在一起的。DPH_HEAP_BLOCK结构的pNodePoolListHead字段记录这个链表的开头,pNodePoolListTail字段记录链表的结尾。它的第一个节点描述的是DPH_HEAP_ROOT结构和节点池本身所占用的空间。节点池的典型大小是4个内存页(16KB)减去DPH_HEAP_ROOT结构的大小。节点池后的一个内存页用来存放同步用的关键区对象,即_RTL_CRITICAL_SECTION结构,这个结构之外的空间被填充为0,DPH_HEAP_BLOCK结构的HeapCritSect字段记录着关键区对象的地址。

       (图1 页堆结构,摘自《软件调试》)


与普通堆块相比,页堆的堆块结构有很大的不同,每个堆块至少占用两个内存页(1个内存页4KB),在用于存放用户数据的内存页后面,堆管理器总会多分配一个内存页,这个内存页是专门用来检测溢出的,我们称其为栅栏页(fense page)。栅栏页的页属性被设置为不可访问(PAGE_NOACCESS),因此,一旦用户数据区发生溢出并触及栅栏页,便会引发异常,如果程序在调试,那么调试器便会立刻收到异常,使调试人员可以在第一现场发现问题,并迅速定位到导致溢出的代码。数据区在第一个内存页的结尾,第二个内存页紧邻在数据区的后面,图2显示了这样的一个页堆堆块(DPH_HEAP_BLOCK)的数据布局。

图2 堆块结构 摘自《软件调试》


页堆的工作机制

在堆块周围加一个不可访问的保护页,用来将各个堆块区分开;如果保护页被覆盖,将尽可能在接近覆盖问题发生的地方检测到问题并中断给调试器

页堆有2种模式

普通页堆(normal page heap)和完全页堆(full page heap),两者主要差别在于保护方式(普通页堆在堆前后用固定填充数据保护,完全页堆用一个不可访问的页保护)

 

最常见的元数据被覆盖问题是在使用堆时没有考虑到边界问题,导致的溢出问题;下面是溢出可能影响堆结构的示意图

                                                            (图3 堆溢出图示)


测试代码:


gflag启用页堆,执行命令后,其实是修改的注册表。

命令:gflags.exe /p /enable app.exe /full

(图4 对测试程序开启页堆)

(图 5 gflags启用页堆后,gflags修改的注册表)

               (图 6 windbg查看页堆是否启动)

windbg打开可执行程序、输入参数:xiaosangTestForOverflow,这个参数会拷贝到动态分配的缓冲区,用来溢出缓冲区。


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2022-10-31 19:25 被sanganlei编辑 ,原因:
收藏
免费 7
支持
分享
打赏 + 50.00雪花
打赏次数 1 雪花 + 50.00
 
赞赏  Editor   +50.00 2022/08/05 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (4)
雪    币: 1025
活跃值: (628)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2022-7-21 17:08
0
雪    币: 881
活跃值: (9856)
能力值: ( LV13,RANK:385 )
在线值:
发帖
回帖
粉丝
3
学到了,感谢分享.
2022-7-21 17:34
0
雪    币: 10027
活跃值: (4421)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
感谢分享
2022-7-23 10:34
0
雪    币: 319
活跃值: (347)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
感谢分享
2022-7-23 16:30
1
游客
登录 | 注册 方可回帖
返回
//