工具: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字节表示计算单位是多少字节
不同点
因此根据地址 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 即是:合并后的堆块的大小。后八个字节的指针对,则指向空表的索引区。
注意事项
以上是空表中的堆块的合并,并且只发生在空表中。
整个过程比较费时,繁琐,在强调效率的情况下,堆块合并就会被禁止,设置为占用太。
空表中第一个块的情况下不会向前发生合并,最后一个块不会向后进行合并。
快表的申请与释放
快表和空表的区别在于 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