专栏名称: 看雪学苑
致力于移动与安全研究的开发者社区,看雪学院(kanxue.com)官方微信公众帐号。
目录
相关文章推荐
zartbot  ·  推荐一本《Streaming Systems》的书 ·  2 天前  
zartbot  ·  推荐一本《Streaming Systems》的书 ·  2 天前  
中国能源报  ·  打破国外技术垄断,成功热试 ·  4 天前  
中国能源报  ·  打破国外技术垄断,成功热试 ·  4 天前  
中国能源报  ·  特变电工±800kV平波电抗器发运安装,守护 ... ·  4 天前  
中国能源报  ·  特变电工±800kV平波电抗器发运安装,守护 ... ·  4 天前  
FreeBuf  ·  AIGC场景的内生安全防御体系构建 | ... ·  6 天前  
智能建筑电气技术杂志  ·  【IBE】阻燃电缆与耐火电缆:定义及选用 ·  6 天前  
智能建筑电气技术杂志  ·  【IBE】阻燃电缆与耐火电缆:定义及选用 ·  6 天前  
51好读  ›  专栏  ›  看雪学苑

【系列】堆溢出研究(二)

看雪学苑  · 公众号  · 互联网安全  · 2017-05-19 18:05

正文

调试中识别堆表


工具:ollydbg 2.0版本 & vc6.0(release模式)  编译选项默认 os: windows2000

函数的抽离

在堆中进行内存分配的时候,C 语言函数调用的是 malloc()函数,c++ 中调用new()函数,当动态调试进入函数内部的时候察觉此两个函数调用的都是底层 ntdll.dll 中的 RtAllocateHeap() 函数,所有的 windows 分配堆的函数在底层调用的都是此函数,这也死程序员可以看到的关于堆的最底层函数。因此研究堆分配,重点关注此函数即可。


堆的调试


在此之前需要理解一个概念:调试堆与调试栈不同,不能直接加载或者attach 程序,否则堆管理策略就会采用调试状态下的堆管理策略,使用调试状态下的堆管理函数。

正常堆和调试堆的区别:

1. 调试堆只采用空表分配,不采用快表分配

2. 所有的堆块末尾都加上十六个字节的用来防止程序溢出,(仅仅是用来防止程序溢出,而不是堆溢出),其中这十六个字节包括:
8 * 0xAB + 8 * 0x00

3. 块首的标志标志位不同,调试状态下的堆和正常堆的区别如同 debug 下的 PE 文件和 release 下的 PE 文件类似,做堆移除实验的时候,调试器中可以正常运行的shellcode,单独运行却不行。很可能就是调试堆与正常堆的差异造成的。

为拉避免采用调试状态下的堆,我i们直接在程序中嵌入 int3 断点,然后调用实时调试器即可,源码:

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

step:

1. 调整ollydbg 为 just in time (实时调试器)

2. 直接进行编译链接运行程序,根据程序中的 int3 断点. ollydbg 会直接断在 int3 断点出如图所示:


如上图所示程序断点段在拉地址 VA = 0040101D 处,此时使用快捷键 ALT + M 查看内存映射窗口来到如图所示重点部分已经标注出来:


如上图所示可以、得到信息:发现进程堆地址为:  00130000 大小为 0x6000  (此处可以通过函数 GetPcocessHeap() 函数获得句柄)如图:


还有我们程序中创建出来的堆地址是0x00360000 size = 0x1000


识别堆表


根据上图中的信息我们直接转到程序中创建出的堆地址 0x360000处在(数据窗口  直接 快捷键 ctrl + g )

 

对于上图来到地址 0x360000处后,根据和堆溢出有关的数据结构我们直接关注 空表索引区即可(即偏移地址 0x178 地址处):


堆初始化的状态


当堆刚被初始化的时候结构很简单,

1. 其中只包含一个空闲大块(称为 “尾块”)

2. 此尾块地址位于 0x178(360178)处 (未启用块表的情况下)算上基地址就是 0x360688  (又称为freelist【0】 )   

   
3.freelist[0] 指向“尾块 ‘,八个字节 (前四个字节是前向指针 后四个字节是后向指针 即:空表中的一对指针) ,其余的各项索引都指向其自身


对堆块块首做一个简介 ####

堆块的块首占八个字节下面根据占用态和空闲态分别介绍:

 

 
共同点:

  • 0-2 字节代表本快的大小(包括块首)

  • 2-4字节表示计算单位是多少字节

不同点

  • Flags出 占用态标志是1  空闲态标志是 0

  • 空闲态块首后的八个字节为一对指针,分别是前向指针和后向指针。当堆块变为占用态的时候重新回分配数据。

  • 实际上尾块的起始位置是 0x360680

因此根据地址 0x360680处八个字节的情况可以知道:此尾块的大小是 0x130  计算单位是 0x0008 个字节  总大小是 0x980字节。


调试中识别堆的分配,释放,合并


堆块的分配

我们直接在cpu窗户 命令 F8单步执行程序到地址:0x00401028地址处也就是在源码中我们执行完:

h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3);

当h1被分配完以后直接查看地址:0x360178地址处的值:    

此时的地址0x360178处的值已经从0x360688改变为0x360698  同时跳转到 0x360698,如下图:

 

如上图所示:在地址0x360698出值为0x360178 链表的条件。

同样的根据地址0x360688处的值即:分配的h1可以发现,h1的大小是 0x002 size = 16bytes

接下来直接运行到地址 0x00401059 此时直接查看 0x360178的地址出看到 值已经更改为:0x360708.接下来直接来到0x360680处进行查看

h1 - h6 的分配情况如下图所示:  


如上所示:尾块现在的地址是:0x360700 大小是 0x120 = 0x130 - 0x2 * 4 - 0x4 * 2
以上从h1 - h6的分配情况验证啦 空表分配中的找零钱现象(从一个大块中依次一小块一小块地进行切割)

堆块的释放

接着上面的程序执行,直接执行到地址:00401077地址处

HeapFree(hp,0,h1); //free to freelist[2]  HeapFree(hp,0,h3); //free to freelist[2]  HeapFree(hp,0,h5); //free to freelist[4]

分别释放啦堆块 h1 h3 h5 这样做是防止相邻堆块进行堆块的合并。直接查看地址 0x360178 地址处的值重点观察变化的值如下图:

 
从上图中可以发现地址 0x360188 的值发生啦变化 从原来的指向自身现在变为指向:0x360688  0x3608A8 

地址 0x360198 处的值变化为: 0x003606C8 和 0x003606c8

由上图可知 h1 h3分别被释放到 freelist[2] 空表中, h5被释放到啦 freelist【4】空表中。

根据freelist【2】 的空表索引 以及h1 h3堆块的指针组,可以发现 :


如图所示左边箭头是前向指针,顺序为 Frllist -> h1 > h3  右边是后向指针 顺序是 h3> h1 > freelist[2]

对于h5堆快倒是没啥 ,freelist【5】直接索引到 地址 0x3606c8

堆表的合并

接着程序运行直接运行到地址 0x401080地址处,执行的是代码:

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

当释放 h4 的时候会发生堆块的合并现象(两个连续的空闲块就会发生合并)。首先是先从空表中将三个空闲块摘下,重新计算合并后的堆块的大小,然后合并成新的空闲块,链入空表。如下图所示分别为空表索引区状态和合并后堆块状态:  


如上图所所示:地址 0x3606A0处的值 0x0008 即是:合并后的堆块的大小。后八个字节的指针对,则指向空表的索引区。

注意事项

  1. 以上是空表中的堆块的合并,并且只发生在空表中。

  2. 整个过程比较费时,繁琐,在强调效率的情况下,堆块合并就会被禁止,设置为占用太。

  3. 空表中第一个块的情况下不会向前发生合并,最后一个块不会向后进行合并。

快表的申请与释放

快表和空表的区别在于 HeapCreate()函数的参数的不同。

hp = HeapCreate(0,0,0);//块表 hp = HeapCreate(0,0x1000,0x10000);//空表

源码:

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

与空表的申请大致类似。

环境:与空表使用的环境一样

直接在dunp窗口中进行跳转到 0x360688处,此时发现快表为空。这也是为什么要反复申请释放内存的原因,接下来分别申请 8,8,16,24字节的内存,然后进行释放,(快表未满时释放到快表中)。 

先运行程序到地址 0x40109F处。此时直接观察快表中的变化,此时发现让然为空,下面运行释放程序,直接单步执行命令运行到地址:0x401106处,这是观察快表的变化如图所示:


运行程序到地址 0x40110D处观察堆块是否链如块表:

 

如上图所示h1 - h4已经链接进入块表中并且都是处于占用态。 地址 0x361e90指向下一个堆块(因为h1 h2 同时为八字节的空闲堆块) 

当程序运行到地址 0x401140时(也就是执行完申请内存的代码时)

h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);

此时申请的堆块应该从块表中申请,此时查看堆表区的索引:

 

从以上两图中可以看到当继续申请内存的时候,是从快表lookside[2]处卸下的堆块。当释放的时候,还是将空闲堆块释放到此处执行代码:

HeapFree(hp,0,h2);

执行完后继续查看上图中地址的值:


如图所示:当释放完堆块后还是链接进入啦快表 looksize[2]

你可能对以下内容感兴趣:



更多优秀文章,长按下方二维码,“关注看雪学院公众号”查看!

看雪论坛:http://bbs.pediy.com/

微信公众号 ID:ikanxue

微博:看雪安全

投稿、合作:www.kanxue.com