人在做, 天在看 boxcounter.com boxcounter.org boxcounter[a]boxcounter.org 注册 | 登陆

堆破坏 和 Special pool

    不介意转载,转载请注明来自 boxcounter.org


    前阵子遇到个堆破坏的BUG,花了一个早上才找到根源,一个低级错误:
    首先我定义了一个变量用于存储进程完整路径:

    UNICODE_STRING usProcessPath = {0, MAX_PATH * sizeof(WCHAR), NULL};

    然后,为它从 look-aside 表里申请了一块内存:

    usProcessPath.Buffer = LsaCreateFilePath(usProcessPath.MaximumLength);

    之后用这块 buffer 来存储进程完整路径,进行一些处理等等。最开始运行的挺正常的,后来某天进行代码优化,结果给优化出了个低级错误:
    为了比较某个进程是不是我要关注的进程,我调整了usProcess.Buffer,之后进行一些列的比较等等,然后释放的时候没有调整回去,结果导致堆崩溃。

    很郁闷,一个鸟问题耗费了一个上午。堆破坏这种BUG,最让人头疼,因为它很难追溯到问题的根源。于是干脆又多花了点时间琢磨了下解决方案,希望能通过这一个BUG,解决一系列此类BUG。
    
    首先,考虑下堆破坏,最有可能导致堆破坏BUG的原因有3:
    1. 申请了内存,结果却释放错了(比如上面我说的那个问题)。
    2. 重复释放同一块内存。
    3. 溢出。
    
    上述三者,其实再仔细想想,可以把1、2合成一条,统一视为“错误释放”。
    对于这种堆错误,可以采用一个简单的方法来解决:
    建立一个链表(或者类似的数据结构,只要方便插入、摘除、查询就OK),每申请一块内存,就将内存的信息,比如内存起始地址,申请代码的行数(__LINE__)、文件路径(__FILE__)、tag(如果有的话)等信息组成一个节点,插入链表中。如果申请内存的方法不一样,最好能为每种方法都单独建立一个链表。以我的代码为例:我为从 look-aside 表中申请的内存,和用 ExAllocXXXX 申请内存各准备了一个链表。
    然后释放内存的时候,去查询链表,看该内存地址是否在链表中,如果不在,则说明释放有问题。

    对于另外一种破坏方式,“溢出”,则需要借助工具 - Driver Verifier,这是微软提供的一个利器。该工具的具体作用、使用方法等,我就不啰嗦了,需要的朋友请自行google,《windows internals》里也有说明。需要多嘴一句的是,这个工具已经整合在系统里了,至少我用的 xp、2003、7 里都已整合了,存放在 %systemroot%\system32 下,文件名是 verifier.exe。

    Verifier 可以设置 Special pool,这个功能可以帮助我们减少“溢出”的问题。
    
    首先,啰嗦下“Special pool”,
    这种特殊的内存池,特殊之处就在于它的结构:
    
大小: 12.47 K
尺寸: 177 x 217
浏览: 1 次
点击打开新窗口浏览全图
    
    比如,咱们申请一块0x208字节的内存,系统实际会给咱们分配3个内存页(大方吧~),其中一头一尾两个是无效页(图中的“无效页1”和“无效页2”),中间这个页的最底部就是咱们的0x208个字节,从这也的页首部到咱们分配的0x208个字节的首地址之间的这块区域被打上了特殊标记(这个标记的作用,我还没继续琢磨,有空补上)。
    通过 windbg,咱们可以验证一下:
kd> !pool 0x857c6df8                                   << 0x857c6df8 是我申请到的一块内存的首地址
Pool page 857c6df8 region is Special pool       << 确认是从 special pool 中分配的
857c6000: Unable to get contents of special pool block

kd> db 0x857c6df8 0x857c6df8+400
857c6df8  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6e08  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6e18  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6e28  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
    ...... << 这里省略若干完全一样的输出
857c6fd8  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6fe8  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6ff8  87 87 87 87 87 87 87 87-?? ?? ?? ?? ?? ?? ?? ??  ........????????  << 结束地址
857c7008  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
857c7018  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ??


    从上面可以看到这块内存的结尾是857c7fff,正好是一个内存页的末尾(我的机器x86上,一个内存页为0x1000个字节)。而紧接着的一个页都是无效页(之前说到的“无效页面2”。另,windbg 中将无效页的内容表示为 ?),我们可以继续验证这整个页是否都是无效,这里就不浪费篇章了。



    我们再来看看 0x857c6df8 之前的内存是不是都被打上了特殊标记
kd> db 0x857c6df8-100 0x857c6df8
857c6cf8  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6d08  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6d18  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6d28  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6d38  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6d48  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6d58  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6d68  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6d78  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6d88  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6d98  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6da8  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6db8  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6dc8  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6dd8  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6de8  87 87 87 87 87 87 87 87-87 87 87 87 87 87 87 87  ................
857c6df8  87  
   从这里看来,这些'87'就是所谓的特殊标记。再往前看,可以发现,这一页的最开始有一些特殊标识:
857c5ff8  ?? ?? ?? ?? ?? ?? ?? ??-08 42 87 00 78 44 62 67  ????????.B..xDbg
857c6008  b0 fc cb 84 87 87 87 87-87 87 87 87 87 87 87 87  ................
    这应该是特殊标志的标志头。(关于特殊标记,由于我没有关注,所以这里只能通过现象来验证,也只能用“看来”、“应该”这样的词来说明 :-))
   


    OK,咱们最后来看一下再往前的一个页面,上面说了,咱们申请的内存所在的页的尾巴是 857c7fff,那我们就往前看0x1000个字节:
kd> db 857c7fff-0x1000  857c7fff
857c6fff  87 ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  .???????????????
857c700f  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
857c701f  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
857c702f  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
857c703f  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
857c704f  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
857c705f  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
  ...... << 之后的部分全部省略
 
    上面显示的第一个字节实际上是属于更前面的一个内存页,除它以外,这一页全显示为 ?,也就验证了这一个页是无效页,也就是之前说的“无效页面1”。
    
    
    OK,咱们完整了验证了 Special pool 的 Special 之处。咱们可以继续讨论“溢出”的问题了。
    其实,也没什么好多说的了,最常见的“溢出”是向后溢出,而上面说描述的 Special pool 的结构,就可以保证一旦驱动发生向后的“溢出”,调试器就捕获到异常,中断下来。那向前溢出呢? verifier 也提供了相应的方法,实际上 verifier 准备了两种 Special pool 的结构,上面咱们看到的是其中一种,也是默认的一种,被称为"Verify End"。与之相对应的是"Verify Start",对于这种方式,Special pool 的总体结构没有变,仍然是两个无效页面夹着一个有效页面,但是这个有效页面就有变化:咱们申请的内存被安置在这一页的页首,也就是这个样子:

大小: 12.18 K
尺寸: 153 x 201
浏览: 1 次
点击打开新窗口浏览全图
   
    可见,这种情况下,一旦发生向前溢出,调试器也会立刻就捕捉到。
    
    
    这样看来,维护一套自己的内存信息链表,然后再启用 special pool,几乎就完美解决了堆破坏。但是,(转折鸟~,很多时候,这种转折最扫人兴)我查到的资料上说, special pool 只针对少量的内存的申请,多少是“少量”呢?不超过一个页面大小。我就不继续验证了。

    另说一句:对于较新的系统,如vista,7等,系统将 special pool 机制应用到了更多的函数上,比如 IoAllocateMdl、IoAllocateIrp、RtlAnsiStringToUnicodeString等。但是我还没仔细查 xp、2003 上有哪些函数是被支持的,目前只知道 ExAllocateFromNPagedLookasideList 和 ExAllocXXX 是肯定支持的。
    
    OK,暂时说这么多,收工。
    
    
    参考资料[1]:http://msdn.microsoft.com/en-us/library/ms792863.aspx
    参考资料[2]: 《Microsoft Windows internals》

Tags: 堆破坏, special pool

« 上一篇 | 下一篇 »

Trackbacks

点击获得Trackback地址,Encode: UTF-8

发表评论

评论内容 (必填):