一、背景1、讲故事最近分享了几篇关于非托管内存泄露的文章。倾倒,一饮一啄,莫非预定。让我不得不加深一下对NT堆和页堆的理解。本文将为您带来另一种内存泄漏。前段时间有个朋友来找我,说他的程序中存在非托管内存泄漏,操作某个块会导致非托管内存快速增加。帮我逆向看看是哪里操作没有释放资源?既然找到我了,那我们就在WinDbg上分析一下。二、WinDbg分析1、内存泄漏在哪里看内存泄漏还是老规矩,用!address-summary命令即可。0:000>!address-summary---使用总结--------------RgnCount------------总大小-------Busy%oftalfree4437fc`685D1000(7.986tb)99.82%堆6583`563AA000(13.347GB)92.89%0.16%0.16%<不知名MB)1.80%0.00%stack1080`08C40000(140.250MB)0.95%0.00%其他310`081D8000(129.844MB)0.88%0.00%TEB360`00048000(288.000kb)0.00%0.00%0.00%0.00%0.00%0`00000000001000(4.000kB)0.00%0.00%---状态摘要--------------RgnCount------------总大小-------%ofBusy%ofTotalMEM_FREE4437fc`685d1000(7.986TB)99.82%MEM_COMMIT24643`67933000(13.618GB)94.77%0.17%MEM_RESERVE3360`300ec000(768.922MB)5.23%的当前六边形进程从GAP0.0提交的内存%13G,很明显这是一个非托管内存泄漏,既然是非托管泄漏,就需要二番战,就是让朋友开ust,或者启用应用程序验证器(ApplicationVerifier)打开页堆,目的是为了记录这次内存分配的来源,这里让小伙伴们用gflags打开ust,怎么打开这里就不介绍了,大家可以上网搜索一下2.跟踪ust加持下的调用栈和blessingofust,接下来就可以继续分析了,使用!heap-s观察nt堆的布局。0:000>!heap-sSEGMENTHEAPERROR:初始化扩展失败NtGlobalFlag为新堆启用以下调试辅助工具:堆栈回溯tracesLFH密钥:0x0000004c4f657ebf腐败终止:启用堆标志保留提交VirtFreeListUCRVirtLockFast(k)(k)(k)(k)长度块(续)堆------------------------------------------------------------------------------000000000006000008000002325761721232576430161610LFH000000000001000008008000648645110000000000088100000800100210885001088155200LFH...0000000029fb00000800100288320674088832032559343471891b7LFHExternalfragmentation48%(343freeblocks)000000002987000008001002512851231100...-------------------------------------------------------------------------------------从卦象来看,最大的commit是67408k=67M,与13G相差不到一星半。如果你了解NtHeap的布局,你应该知道当你分配的内存>512k时,它会进入HEAP的virtualAllocdBlocks双向链表。言下之意就是当你觉得内存不对的时候,就要观察这个链表,也就是上图中的Virtblocks一栏,可以看到Virtblocks=189的handle=0000000029fb0000,然后继续下钻到handle=0000000029fb0000的堆0:000>!heap-h00000029FB0000段堆错误:无法初始化extentionIndex地址名称调试选项启用了启用23:29FB0000段at0000000029fb0000to00000000000000至0000000000000000000000000000000000000000000000000000000000来0000000027f10000(001f7000bytescommitted)Segmentat00000000318a0000to0000000031ca0000(00400000bytescommitted)Segmentat0000000044a00000to0000000045200000(005f1000bytescommitted)Segmentat000000004ae90000to000000004be60000(00efc000bytescommitted)Segmentat000000005b3b0000to000000005c380000(00e2e000bytescommitted)Segmentat000000005d8c0000to000000005e890000(提交00cf1000字节)段位于000000005c380000到000000005d350000(提交002e7000字节)标志:08001002ForceFlags:00000000粒度:16字节es...VirtualAllocList:29fb0118Unabletoreadnt!_HEAP_VIRTUAL_ALLOC_ENTRYstructureat0000000043500000Uncommittedranges:29fb00f8我去,华中出现不想看到的Unabletoreadnt!_HEAP_VIRTUAL_ALLOC_ENTRYstructureat000000000435000004,就是不愠055000004_HEAP_VIRTUAL_ALLOC_ENTRY结构可以通过dt0:000>dtnt!_HEAP_VIRTUAL_ALLOC_ENTRYSymbolnt!_HEAP_VIRTUAL_ALLOC_ENTRYnotfound来验证。为什么没有记录在他的机器上,可能与其生产服务器的Windows系统有关。接下来的问题是:!heap命令失败,如何挖出VirtualAllocdBlocks?只有纯人肉……3、如何挖人肉VirtualAllocdBlocks要想挖人肉,需要一些底层知识,比如以下三点。什么是VirtualAllocdBlocks?VirtualAllocdBlocks是一个记录大块内存的双向链表结构,可以通过dtnt!_HEAP0000000029fb0000命令从HEAP中找到。0:000>dtnt!_HEAP0000000029fb0000ntdll!_HEAP+0x118VirtualAllocdBlocks:_LIST_ENTRY[0x00000000`43500000-0x00000000`32970000]+0x128SegmentList:_LIST_ENTRY[0x00000000`29fb0018-0x00000000`5c380018]...0:000>dt_LIST_ENTRY0000000029fb0000+0x118ntdll!_LIST_ENTRY[0x00000000`43500000-0x00000000`32970000]+0x000Flink:0x00000000`43500000_LIST_ENTRY[0x00000000`47240000-0x00000000`29fb0118]+0x008Blink:0x00000000`32970000_LIST_ENTRY[0x00000000`29fb0118-0x00000000`4ee90000]从卦中可以See,VirtualAllocdBlocks与Flink和Blink是一个双向链表结构。什么是_HEAP_VIRTUAL_ALLOC_ENTRY?我们都知道堆的<512k的block是_HEAP_ENTRY结构,>512k的block是_HEAP_VIRTUAL_ALLOC_ENTRY结构。不信可以用dt导出。0:016>dtnt!_heap_virtual_alloc_entryntdll!_heap_virtual_alloc_entry+0x000输入:_list_entry+0x010Extrastuff:_heap_entry_extra_extra+0x020commitsize:uint8b+0x028usevsize:uintry:1028useply;uselty:uintriNt:uintriNt:uintrimty;ustrimity;ustrimity;还有一些辅助信息,比如CommitSize,ReserveSize等,然后可以提取第一个节点地址加上??+0x30找到真正的内存分配块,即0x0000000043500000+0x30,然后使用!heap-p-a可以看到这个分配块的来源在哪里。0:000>!堆-p-p-a0x0000000043500000+0x30地址0000000043500030在_heap@29fb0000heap_entrysizesizeprevsflagsflagsflagsuserptruserptruserptr-状态000000000043500030100100000000000000000000000000000000004010000401004DLT-CURIT!??::FNODOBFM::`string'+0x00000000000153eb7fed230483bhalcon!HXmalloc+0x000000000000008b7fed22dd81dhalcon!HXAllocRLTmp+0x000000000000265d7fed22d6bd0halcon!HXAllocTmp+0x0000000000000a807fed44a346ahalcon!HCancelWait+0x000000000000007a7fed2386b8fhalcon!CCallHProc+0x000000000000073f7fe83e3bcf6+0x000007fe83e3bcf60:000>!ip2md0x000007fe83e3bcf6MethodDesc:000007fe83c39138MethodName:HalconDotNet.xxxClass:000007fe83c6b890MethodTable:000007fe83c3f300mdToken:0000000006000df5Module:000007fe83a7f498IsJitted:yesCodeAddr:000007fe83e3bb90Transparency:Safecritical可以看到第一块size=0x1000040byte=16M内存由HalconDotNet分配。接下来再画几个,或者用脚本总结一下,发现有大量占用88M内存,大致可以归为两类:C#代码分配未释放:内部代码:3。总结最后,我把这个结果给了朋友们,让他们看看。使用!ip2md显示的托管方式,为什么不发布?这个转储丢失了吗?可以看出是因为我做了一套halconDotNet版本的打包有一些瑕疵。这个dump的难点在于如何在!heapextension命令失败的情况下,通过纯手工的方式,清晰明了的剥离NTHeap。
