引用注明>> 【作者:张佩】【原文:www.yiiyee.cn/blog】
我前几个月曾经分析了一个BAD_POOL_CALLER的问题(链接),今天收到的这个dump文件,系统是Win7 X64,最后发现问题和前者非常相似,但二者的分析过程却截然不同。
引子
打开dump文件后,首先进行自动分析。蓝屏号是0x4E。
************************************************************ * * * Bugcheck Analysis * * * ************************************************************ PFN_LIST_CORRUPT (4e) Typically caused by drivers passing bad memory descriptor lists (ie: calling MmUnlockPages twice with the same list, etc). If a kernel debugger is available get the stack trace. Arguments: Arg1: 000000000000009a, Arg2: 000000000014e26c Arg3: 0000000000000006 Arg4: 0000000000000002 Debugging Details: ------------------ BUGCHECK_STR: 0x4E_9a DEFAULT_BUCKET_ID: WIN7_DRIVER_FAULT PROCESS_NAME: System CURRENT_IRQL: 0 LAST_CONTROL_TRANSFER: from fffff8000495b9ef to fffff800048ccbc0 STACK_TEXT: ... nt!KeBugCheckEx ... nt!MiBadRefCount+0x4f ... nt!MiFreePoolPages+0xa8b ... nt!ExFreePoolWithTag+0x7c7 ... TestModule!MemMgr::Free+0x77 //省略若干TestModule的调用帧 ... nt!PopIrpWorker+0x3c5 ... nt!PspSystemThreadStartup+0x5a ... nt!KxStartSystemThread+0x16
从Windbg的帮助文档中,找到0x4E相关的内容,根据第一个参数值0x9A,拿到下面的一些信息。
Parameter 1 | Parameter 2 | Parameter 3 | Parameter 4 | Cause of Error |
0x9A | Page frame number | Current page state | The reference count of the entry that is being removed | A driver attempted to free a page that is still locked for IO. |
驱动试图释放一个仍然被锁定的页。到目前为止,我对这个描述还是不太清楚。
内存分析
从调用栈看,出问题的时候,驱动程序正在调用ExFreePoolWithTag来释放一个内存块。加载符号文件,通过进一步分析,我发现正在释放的是一个维度为4的指针数组里面的第三个成员指针所指向的内存。前面两个指针所指向的内存已经释放成功,并且指针已经清零。剩下的两个指针值如下:
Pointer 2 = 0xfffffa80`04c6c000 pointer 3 = 0xfffffa80`04c8d000
分别对这两个指针使用!pool命令进行分析。如果!pool命令还能够认识它们,说明内存块相关的结构体没有被破坏。否则就是内存块被破坏了。
0: kd> !pool 0xfffffa80`04c6c000 Pool page fffffa8004c6c000 region is Nonpaged pool fffffa8004c6c000 is not a valid large pool allocation, checking large session pool... fffffa8004c6c000 is not a valid small pool allocation, checking large pool... unable to get pool big page table - either wrong symbols or pool tagging is disabled fffffa8004c6c000 is freed (or corrupt) pool Bad allocation size @fffffa8004c6c000, zero is invalid *** *** An error (or corruption) in the pool was detected; *** Attempting to diagnose the problem. *** *** Use !poolval fffffa8004c6c000 for more details. Pool page [ fffffa8004c6c000 ] is __inVALID. Analyzing linked list... [ fffffa8004c6c000 --> fffffa8004c6c010 (size = 0x10 bytes)]: Corrupt region Scanning for single bit errors... None found 0: kd> !pool 0xfffffa80`04c8d000 Pool page fffffa8004c8d000 region is Nonpaged pool *fffffa8004c8d000 : large page allocation, Tag is TAG6, size is 0x4020 bytes
两个指针的分析结果,第一个分析不出任何结果,第二个则看上去是正确的。为了确定第二个不是“碰巧”正确的,可确认一下TAG6这个内存分配时指定的tag,如果确实有的话,就基本可认为不是“碰巧”的了。
第一个内存块已经被破坏了,并且这时候,我已经确认,指针列表中的4个内存缓冲区的size都是一样的。所以知道第一个pool块的size也是0x4020字节。
我此时首先想到的是,为什么4个pool memory,看上去只有第三个被破坏了。因为前面两个已经被成功释放掉了,第四个也是好的。这有点奇怪。在没有进一步的资料前,我只能认为这4个pool memory是用在不同的地方,而第三个是碰巧被破坏了。后来的事实也证明,通过这个途径我不可能找到任何突破口。好在很快也就放弃了。
Large Pool
上一次我通过使用POOL_HEADER结构体来查看被破坏的pool块。但这一次,这个办法不能用。原因是这次内存块的大小超过了一个页面长度(定义为PAGE_SIZE,即4K字节)。
根据系统的定义(见MSDN),如果申请的内存块size < PAGE_SIZE,系统会在一个内存页中分配,不会跨越页边界。举例说,如果有一个0x20字节的内存申请请求,系统不会把page1尾巴上的0x10字节和page2前面的0x10字节拼在一起返回给申请者;只会把page2前面的0x20字节返回给调用者。
如果申请的size > PAGE_SIZE,系统怎么做的呢?假设现在用户准备申请5000字节(4K+4)的内存块,它恰好比一个page多4个字节。系统会以4K为边界,找到相邻的两个空闲页,将起始地址返回给申请者。PAGE1的全部和PAGE2的前面部分,由申请者使用;PAGE2尾部的部分,系统另做他用。在这里,我们把这种大内存块称为:Large Pool。
和普通pool块不同,large pool块不使用POOL_HEADER结构体来维护内存块的大小。所以这次我不能从POOL_HEADER入手,这正是困难所在。通过网上搜索,我发现了MSDN BLOG上的这篇文章。它介绍了一种查看large pool块完整性的方法。
在large pool块的尾部,系统会保留两个小pool块,并把它们的tag分别设置为Frag和Free。我估计frag是指Fragment(碎片),而Free指空闲的意思。
通过这篇文章介绍的方法,我先对正确的那个large pool块进行了测试:
0: kd> dc fffffa80`04c8d000+0x4020 L40 fffffa80`04c91020 02010000 67617246 00000000 00000000 ....Frag........ fffffa80`04c91030 00020001 65657246 00000000 00000000 ....Free........ fffffa80`04c91040 04c94040 fffffa80 04bf6040 fffffa80 @@......@`...... fffffa80`04c91050 020c0002 6c734d46 00000000 00000000 ....FMsl........
果然看到了Frag和Free这两个tag。能正确看到这两个tag,至少说明这个pool块在使用时没有发生越界的情况,否则应该会覆盖tag值(但如果只越界4个字节,也有可能,此处不考虑此种情况)。还可以对这个页使用!pool命令:
0: kd> !pool fffffa80`04c8d000+0x4020 Pool page fffffa8004c91020 region is Nonpaged pool *fffffa8004c91020 size: 10 previous size: 0 (Allocated) *Frag fffffa8004c91030 size: 20 previous size: 10 (Free) Free fffffa8004c91050 size: c0 previous size: 20 (Allocated) FMsl fffffa8004c91110 size: 160 previous size: c0 (Allocated) Ntfx 后省略
说明这一页的前0x20个字节是large pool块的一部分。剩下的部分则可以看到开头tag为Frag和Free的特殊pool块。后面的页内存被其它模块申请。
再来看看发生错误的那个large pool块:
0: kd> dc fffffa8004c6c000+0x4020 fffffa80`04c70020 02010000 67617246 b660fdb5 4fabc845 ....Frag..`.E..O fffffa80`04c70030 0c050003 6873534b 0538ab30 fffffa80 ....KSsh0.8..... fffffa80`04c70040 053f6170 fffffa80 00000000 00000000 pa?............. fffffa80`04c70050 00000001 00000001 00000000 00000000 ................ fffffa80`04c70060 000006e4 00000000 001ea6c8 00000000 ................ fffffa80`04c70070 00000000 00000000 00000032 00000001 ........2....... 0: kd> !pool fffffa8004c6c000+0x4000 Pool page fffffa8004c70000 region is Nonpaged pool *fffffa8004c70000 size: 30 previous size: 0 (Free) *Free fffffa8004c70030 size: 50 previous size: 30 (Free ) KSsh Process: fffffa800538ab30 fffffa8004c70080 size: 80 previous size: 50 (Free ) Even (Protected) fffffa8004c70100 size: 1a0 previous size: 80 (Free) None fffffa8004c702a0 size: 50 previous size: 1a0 (Free ) VadS fffffa8004c702f0 size: c0 previous size: 50 (Allocated) FMsl fffffa8004c703b0 size: c0 previous size: c0 (Free) CcPL
可以看到被破坏的痕迹:Frag后面没有Free这个tag,而是tag为KSsh的pool块。这说明large pool 块被破坏了。而最可能的破坏原由就是缓冲区溢出。
后记
这时候,还有一个较好的办法可证实是不是缓冲区溢出:把指定pool块的长度增大后,再测试。我建议把申请的size增加到0x5000字节,其它地方不动。代码逻辑仍然把它当做只有0x4020字节来用。这个问题原本比较容易做出来,但做了这样的改动后,经过两天的测试,再也没有蓝屏。从侧面证实了缓冲区溢出的可能。对我而言,问题至此就算解决了。
另外还有两个悬疑。第一是为什么这个问题被报成0xCE错误,即试图释放被锁定的内存。我个人的理解是,系统在释放pool块时,最后一个page已经被别的模块锁定或挪作他用,系统坚持释放而导致错误。
第二个问题是,系统既然不使用POOL_HEADER结构体,到底是如何维护large pool块的?我在网上查找large page/large pool关键字,暂时还没有得到满意的结果。
参考:
Stop 0x19 in a Large Pool Allocation
MSDN: ExAllocatePoolWithTag