C#非托管泄漏中HEAP_ENTRY的Size对不上解析

目录
  • 一:背景
    • 1. 讲故事
  • 二:如何正确推导
    • 1. 原理是什么?
    • 2. 通过汇编观察编解码逻辑
    • 3. 寻找 edi 所属的堆块
  • 总结

一:背景

1. 讲故事

前段时间有位朋友在分析他的非托管泄漏时,发现NT堆的_HEAP_ENTRY 的 Size 和 !heap 命令中的 Size 对不上,来咨询是怎么回事? 比如下面这段输出:

0:000> !heap 0000000000550000 -a
Index   Address  Name      Debugging options enabled
  1:   00550000
    Heap entries for Segment00 in Heap 0000000000550000
                 address: psize . size  flags   state (requested size)
        0000000000550000: 00000 . 00740 [101] - busy (73f)
        0000000000550740: 00740 . 00110 [101] - busy (108)
0:000> dt nt!_HEAP_ENTRY 0000000000550740
ntdll!_HEAP_ENTRY
   +0x000 UnpackedEntry    : _HEAP_UNPACKED_ENTRY
   +0x000 PreviousBlockPrivateData : (null)
   +0x008 Size             : 0xa6a7
   +0x00a Flags            : 0x33 '3'
   +0x00b SmallTagIndex    : 0x75 'u'
   ...

从输出中可以看到,用 !heap 命令的显示 0000000000550740 的 size=0x00110 ,而 dt 显示的 size=0xa6a7,那为什么这两个 size 不一样呢? 毫无疑问 !heap 命令中显示的 0x00110 是对的,而 0xa6a7 是错的,那为什么会错呢? 很显然 Windows 团队并不想让你能轻松的从 ntheap 上把当前的 entry 给挖出来,所以给了你各种假数据,言外之意就是 size 已经编码了。

原因给大家解释清楚了,那我能不能对抗一下,硬从NtHeap上将正确的size给推导出来呢? 办法肯定是有办法的,这篇我们就试着聊一聊。

二:如何正确推导

1. 原理是什么?

其实原理很简单,_HEAP_ENTRY 中的 Size 已经和 _HEAP 下的 Encoding 做了异或处理。

0:004> dt nt!_HEAP 
ntdll!_HEAP
   ...
   +0x07c EncodeFlagMask   : Uint4B
   +0x080 Encoding         : _HEAP_ENTRY
   ...

那如何验证这句话是否正确呢?接下来启动 WinDbg 来验证下,为了方便说明,先上一段测试代码。

int main()
{
	for (size_t i = 0; i < 10000; i++)
	{
		int* ptr =(int*) malloc(sizeof(int) * 1000);
		printf("i=%d \n",i+1);
		Sleep(1);
	}
	getchar();
}

既然代码中会用到 Encoding 字段来编解码size,那我是不是可以用 ba 在这个内存地址中下一个硬件条件,如果命中了,就可以通过汇编代码观察编解码逻辑,对吧? 有了思路就可以开干了。

2. 通过汇编观察编解码逻辑

因为 malloc 默认是分配在进程堆上,所以用 !heap -s 找到进程堆句柄进而获取 Encoding 的内存地址。

0:004> !heap -s
************************************************************************************************************************
                                              NT HEAP STATS BELOW
************************************************************************************************************************
LFH Key                   : 0x64ffdd9683678f7e
Termination on corruption : ENABLED
          Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast
                            (k)     (k)    (k)     (k) length      blocks cont. heap
-------------------------------------------------------------------------------------
00000000004a0000 00000002    2432   1544   2040     50    12     2    0      0   LFH
0000000000010000 00008000      64      4     64      2     1     1    0      0
-------------------------------------------------------------------------------------
0:004> dt nt!_HEAP 00000000004a0000
ntdll!_HEAP
   +0x000 Segment          : _HEAP_SEGMENT
   ...
   +0x07c EncodeFlagMask   : 0x100000
   +0x080 Encoding         : _HEAP_ENTRY
   ...
0:004> dx -r1 (*((ntdll!_HEAP_ENTRY *)0x4a0080))
(*((ntdll!_HEAP_ENTRY *)0x4a0080))                 [Type: _HEAP_ENTRY]
    [+0x000] UnpackedEntry    [Type: _HEAP_UNPACKED_ENTRY]
    [+0x000] PreviousBlockPrivateData : 0x0 [Type: void *]
    [+0x008] Size             : 0x8d69 [Type: unsigned short]
    [+0x00a] Flags            : 0xfd [Type: unsigned char]
    ...
0:004> dp 00000000004a0000+0x80 L4
00000000`004a0080  00000000`00000000 000076a1`cefd8d69
00000000`004a0090  0000ff00`00000000 00000000`eeffeeff

可以看到 Encoding 中的 Size 偏移是 +0x008,所以我们硬件条件断点的偏移值是 0x88 ,命令为 ba r4 00000000004a0000+0x88 ,设置好之后就可以继续 go 啦。

从图中可以看到在 ntdll!RtlpAllocateHeap+0x55c 方法处成功命中,从汇编中可以看到。

  • eax: 这是 Encoding ,即我们硬件断点。
  • edi: 某个 heap_entry 的 size 掩码值。

最后就是做一个 xor 异或操作,也就是正确的 size 值。

0:000> r eax,edi
eax=cefd8d69 edi=18fd8ab8
0:000> ? eax ^ edi
Evaluate expression: 3590326225 = 00000000`d60007d1
0:000> ? 07d1 * 0x10
Evaluate expression: 32016 = 00000000`00007d10

可以看到最后的size=7d10, 这里为什么乘 0x10,过一会再说,接下来我们找一下 edi 所属的堆块。

3. 寻找 edi 所属的堆块

要想找到所属堆块,可以用内存搜索的方式,再用 !heap -x 观察即可。

0:000> s-d 0 L?0xffffffffffffffff 18fd8ab8
00000000`005922b8  18fd8ab8 000056a0 004a0150 00000000  .....V..P.J.....
0:000> !heap -x 00000000`005922b8
Entry             User              Heap              Segment               Size  PrevSize  Unused    Flags
-------------------------------------------------------------------------------------------------------------
00000000005922b0  00000000005922c0  00000000004a0000  00000000004a0000      7d10     20010         0  free
0:000> dt nt!_HEAP_ENTRY 00000000005922c0
ntdll!_HEAP_ENTRY
   +0x008 Size             : 0x4020
   +0x00a Flags            : 0xa3 ''
   ...

有了这些信息就可以纯手工推导了。

  • 获取 Encoding 值。
0:000> dp 00000000004a0000+0x88 L4
00000000`004a0088  000076a1`cefd8d69 0000ff00`00000000
00000000`004a0098  00000000`eeffeeff 00000000`00400000
  • 获取 size 值。
0:000> dp 00000000005922b0+0x8 L4
00000000`005922b8  000056a0`18fd8ab8 00000000`004a0150
00000000`005922c8  00000000`00a34020 00000000`00000000
  • 异或 size 和 Encoding
0:000> ? 000076a1`cefd8d69 ^  000056a0`18fd8ab8
Evaluate expression: 35192257382353 = 00002001`d60007d1
0:000> ? 07d1 * 0x10
Evaluate expression: 32016 = 00000000`00007d10

怎么样,最后的size 也是size=7d10, 这和刚才汇编代码中计算的是一致的,这里要乘 0x10 是因为 entry 的粒度按 16byte 计算的,可以用 !heap -h 00000000004a0000 ;观察下方的 Granularity 字段即可。

0:000> !heap -h 00000000004a0000
Index   Address  Name      Debugging options enabled
  1:   004a0000
    Segment at 00000000004a0000 to 000000000059f000 (000fa000 bytes committed)
    Segment at 0000000000970000 to 0000000000a6f000 (000c9000 bytes committed)
    Segment at 0000000000a70000 to 0000000000c6f000 (00087000 bytes committed)
    Flags:                00000002
    ForceFlags:           00000000
    Granularity:          16 bytes

总结

这就是解答异或的完整推导逻辑,总的来说思路很重要,这些知识也是我们调试 dump 的必备功底,了解的越深,解决的问题域会越大。

以上就是C#非托管泄漏中HEAP_ENTRY的Size对不上解析的详细内容,更多关于C# HEAP_ENTRY Size的资料请关注我们其它相关文章!

(0)

相关推荐

  • C#中sizeof的用法实例分析

    sizeof是C#中非常重要的方法,本文就以实例形式分析C#中sizeof的用法.分享给大家供大家参考.具体分析如下: 在C#中,sizeof用来计算类型的大小,单位是字节.有这样的一个类: public class MyUglyClass { public char myChar1; public int myInt; public char myChar2; } 在客户端,试图使用sizeof计算该类型的大小. class Program { static void Main(string[

  • C#加载嵌入到资源的非托管dll

    如何加载非托管Dll 我们总会遇到需要加载非Win32的非托管dll,这里推荐一种方式就是将那些非win32的非托管dll嵌入资源的方式,在入口解压并且加载的方式,我先来看看如何实现吧,首先我们准备好demo,新增控制台项目如下: 代码如下: static void Main(string[] args) { UnzipAndLoad(); } /// <summary> /// 解压资源并且加载非托管DLL /// </summary> static void UnzipAndL

  • C# 常用协议实现模版及FixedSizeReceiveFilter示例(SuperSocket入门)

    Socket里面的协议解析是Socket通讯程序设计中最复杂的地方,如果你的应用层协议设计或实现不佳,Socket通讯中常见的粘包,分包就难以避免.SuperSocket内置了命令行格式的协议CommandLineProtocol,如果你使用了其它格式的协议,就必须自行实现自定义协议CustomProtocol.看了一篇文档之后, 你可能会觉得用 SuperSocket 来实现你的自定义协议并不简单. 为了让这件事变得更容易一些, SuperSocket 提供了一些通用的协议解析工具, 你可以用

  • C#(.Net)将非托管dll嵌入exe中的实现

    目录 托管dll与非托管dll 下载与安装 添加Dll 调用 编译 托管dll与非托管dll 托管dll实际上是指C#编写的dll,可以直接右键"引用"导入 而大部分情况下,我们需要引用C++写的dll,如果你的dll是使用 DllImport来导入的,那么它就属于非托管dll,这种dll无法直接嵌入exe中,需要借助工具:Costura.Fody,该工具可以使用VS直接下载 下载与安装 右键引用,选择"管理NuGet程序包",搜索 "fody"

  • C#中托管DLL和非托管DLL的区别详解

    首先解释一下,托管DLL和非托管DLL的区别.狭义解释讲,托管DLL就在Dotnet环境生成的DLL文件.非托管DLL不是在Dotnet环境生成的DLL文件. 托管DLL文件,可以在Dotnet环境通过 "添加引用" 的方式,直接把托管DLL文件添加到项目中.然后通过 Using DLL命 名空间,来调用相应的DLL对象 .  非托管DLL文件,在Dotnet环境应用时,通过 DllImport 调用. C# 调用非托管DLL文件.DLL文件是用C语言编写的. 托管DLL就是能够在公共

  • C#托管内存与非托管内存之间的转换的实例讲解

    c#有自己的内存回收机制,所以在c#中我们可以只new,不用关心怎样delete,c#使用gc来清理内存,这部分内存就是managed memory,大部分时候我们工作于c#环境中,都是在使用托管内存,然而c#毕竟运行在c++之上,有的时候,(比如可能我们需要引入一些第三方的c++或native代码的库,在Unity3d开发中很常见)我们需要直接在c#中操纵非托管的代码,这些non-managed memory我们就需要自己去处理他们的申请和释放了, c# 中提供了一些接口,完成托管和非托管之间

  • C#非托管泄漏中HEAP_ENTRY的Size对不上解析

    目录 一:背景 1. 讲故事 二:如何正确推导 1. 原理是什么? 2. 通过汇编观察编解码逻辑 3. 寻找 edi 所属的堆块 总结 一:背景 1. 讲故事 前段时间有位朋友在分析他的非托管泄漏时,发现NT堆的_HEAP_ENTRY 的 Size 和 !heap 命令中的 Size 对不上,来咨询是怎么回事? 比如下面这段输出: 0:000> !heap 0000000000550000 -a Index Address Name Debugging options enabled 1: 00

  • C#调用非托管动态库中的函数方法

    C#如何调用一个非托管动态库中的函数呢,比如用VC6写的动态库,总之C#调用动态库的过程是比Java调用DLL动态库方便快捷多了,下面举例说明这个过程. 1.创建一个非托管动态库 代码如下: 复制代码 代码如下: //这一句是声明动态库输出一个可供外不调用的函数原型.     extern   "C"  __declspec(dllexport)  int  add( int ,  int ); int  add( int  a, int  b)      {          //实

  • .net非托管资源的回收方法

    本文实例讲述了.net非托管资源的回收方法,分享给大家供大家参考.具体分析如下: 释放未托管的资源有两种方法   1.析构函数 2.实现System.IDisposable接口   一.析构函数  构造函数可以指定必须在创建类的实例时进行的某些操作,在垃圾收集器删除对象时,也可以调用析构函数.析构函数初看起来似乎是放置释放未托管资源.执行一般清理操作的代码的最佳地方.但是,事情并不是如此简单.由于垃圾回收器的运行规则决定了,不能在析构函数中放置需要在某一时刻运行的代码,如果对象占用了宝贵而重要的

  • C#+无unsafe的非托管大数组示例详解(large unmanaged array in c# without ‘unsafe’ keyword)

    C#申请一个大数组(Use a large array in C#) 在C#里,有时候我需要能够申请一个很大的数组.使用之.然后立即释放其占用的内存. Sometimes I need to allocate a large array, use it and then release its memory space immediately. 由于在C#里提供的 int[] array = new int[1000000]; 这样的数组,其内存释放很难由程序员完全控制,在申请一个大数组后,程序

  • C#实现IDisposable接口释放非托管资源

    目录 完整示例 为什么要实现Foo析构函数 Dispose方法中为什么要调用GC.SuppressFinalize Reference Why using finalizers is a bad idea 当在一个类中使用了另外一个实现了IDisposable的类作为一个成员属性时, 此时这个类就有必要也去实现IDisposable接口, 以确保在合适的实际释放非托管资源, 到底该如何正确的实现这个接口呢? 当然这只是需要实现IDisposable接口其中一种情况 完整示例 示例的Foo类中包含

  • C#使用stackalloc分配堆栈内存和非托管类型详解

    目录 stackalloc 表达式 stackalloc 分配 System.Span<T> 或 System.ReadOnlySpan<T> 类型 stackalloc 分配 指针类型 stackalloc分配内存的注意点 非托管类型 Unmanaged type stackalloc 表达式 stackalloc表达式在栈(stack)上分配内存块. 在方法执行期间创建的栈中分配的内存块会在方法返回时自动丢弃.不能显式释放使用 stackalloc 分配的内存.stackall

  • C#使用DllImport调用非托管的代码的方法

    找到GetShortPathName的方法签名, DWORD GetShortPathName(LPCTSTR tpszLongPath,TPTSTR lpszShortPath,DWORD cchBuffer): 非托管及托管数据类型对应关系: LPCTSTR         String LPTSTR           StringBuilder DWORD          int DllImport的导入规则: 1.方法名与Win API完全一样.如果在C#中调用时显示完全不同的方法名

  • 详解C# 托管资源和非托管资源

    托管资源指的是.NET可以自动进行回收的资源,主要是指托管堆上分配的内存资源.托管资源的回收工作是不需要人工干预的,有.NET运行库在合适调用垃圾回收器进行回收. 非托管资源指的是.NET不知道如何回收的资源,最常见的一类非托管资源是包装操作系统资源的对象,例如文件,窗口,网络连接,数据库连接,画刷,图标等.这类资源,垃圾回收器在清理的时候会调用Object.Finalize()方法.默认情况下,方法是空的,对于非托管对象,需要在此方法中编写回收非托管资源的代码,以便垃圾回收器正确回收资源. 在

随机推荐