[翻译]查找Windows内存泄露的几种方法

发布者:阿東
发布于:2018-04-20 14:13

笔者:

Windows内存泄露问题和很多二进制漏洞类似,比如说uaf 释放后重引用,那么windows内存泄露则属于分配后未释放或者未使用,造成的危害虽然远不及通俗的二进制漏洞,但是在企业角度看,按数十小时级的工作时间单位来说,造成服务中断,运行效率减慢,这些问题也是普遍存在的,区别在于是否明显和带来影响,和传统的二进制漏洞比较,发现的周期更长(Fuzz除外),找出问题的成本要更高,却又是不可忽略的一个问题

下文来自:https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/finding-a-memory-leak

正文:

当进程从分页池或非分页池分配内存但不释放内存时发生内存泄漏。因此,随着时间的推移,这些有限的内存池将耗尽,导致Windows放慢速度。如果内存完全耗尽,可能会导致故障。

一. 验证漏洞是否存在

如果Windows性能随着时间的推移而降低,并且怀疑可能涉及内存泄漏,则本节中介绍的技术可以指示是否存在内存泄漏。它不会告诉你泄漏的来源是什么,也不知道是用户模式还是内核模式。

从开始菜单搜索 ‘性能监视器’  添加以下计数器:

Memory– > 池非分页字节(Pool Nonpaged Bytes)

Memory– > 池分页字节(Pool Paged Bytes)

Paging File – > %使用率(%Usage)

将更新时间更改为600秒,以捕获一段时间内的泄漏图。您可能还想将数据记录到文件以供日后检查。启动你认为是造成泄漏的应用程序进行测试,允许应用程序在测试运行这段时间不受干扰, 在这段时间内不要使用目标计算机,泄漏通常很慢,可能需要几个小时才能检测到,等待几个小时后再判断是否发生泄漏,监视性能监视器计数器。在测试开始后,计数器值将快速变化,并且可能需要一段时间才能使内存池值达到稳定状态。用户模式内存泄漏始终位于可分页池中,并导致池分页字节计数器和页面文件使用率计数器随着时间的推移稳步增加。内核模式内存泄漏通常耗尽非分页池,导致池非分页字节计数器增加,尽管可分页内存也可能受到影响。偶尔这些计数器可能会显示误报,因为应用程序正在缓存数据。





二. 查找由内核模式的驱动或组件引起的内存泄露

1)使用poolmon查找内核模式的内存泄露

如果您怀疑存在内核模式的内存泄漏,最简单方法是使用PoolMon来确定哪个pool tag 是与泄漏关联的

PoolMon(Poolmon.exe)按池标记名称(pool tag)监视池的内存使用情况,此工具包含在Windows驱动程序工具包(WDK)中。有关完整说明,请参阅WDK文档中的PoolMon。

启用池标记(Windows 2000和Windows XP)

在Windows 2000和Windows XP上,您必须先使用GFlags启用池标记。Windows调试工具中包含GFlags。启动GFlags,选择“ 系统注册表”选项卡,选中“ 启用池标记”框,然后单击“ 应用”。您必须重新启动Windows才能使此设置生效。有关更多详细信息,请参阅GFlags。在Windows Server 2003和更高版本的Windows上,池标记始终处于启用状态。

使用PoolMon

PoolMon头显示总共分页和非分页池字节。这些列显示每个池标记的池使用情况。显示每隔几秒自动更新一次。例如:

Memory: 16224K Avail: 4564K PageFlts: 31 InRam Krnl: 684K P: 680K
Commit: 24140K Limit: 24952K Peak: 24932K Pool N: 744K P: 2180K

## Tag Type Allocs Frees Diff Bytes Per Alloc

CM Paged 1283 ( 0) 1002 ( 0) 281 1377312 ( 0) 4901
Strg Paged 10385 ( 10) 6658 ( 4) 3727 317952 ( 512) 85
Fat Paged 6662 ( 8) 4971 ( 6) 1691 174560 ( 128) 103
MmSt Paged 614 ( 0) 441 ( 0) 173 83456 ( 0) 482

PoolMon具有根据各种标准对输出进行排序的命令键。按下与每个命令关联的字母,以重新排序数据。每个命令都需要几秒钟的工作。排序命令包括:


要使用PoolMon实用工具查找内存泄漏,请执行以下过程:

启动PoolMon。

如果您确定在非分页池中发生泄漏,请按P一次,如果您确定它在分页池中发生,请按P两次。如果你不知道,不要按P键,这两种都包括在内。

按B按最大字节使用排序显示。

开始你的测试。采取屏幕截图并将其复制到记事本。

每半小时生成一个新的屏幕截图。通过比较屏幕截图,确定哪些标签的字节正在增加。

停止测试并等待几个小时。在这个时候有多少标签被释放了?

通常情况下,应用程序达到稳定运行状态后,会以大致相同的速率分配内存和空闲内存,如果分配内存比释放内存更快,内存的使用将随着时间的推移而增长。这通常表示存在内存泄漏。

处理泄漏

确定哪个池标签与泄漏相关后,可能会显示您需要了解的泄漏信息。如果您需要确定分配例程的哪个特定实例导致泄漏,请参阅使用内核调试器查找内核模式内存泄漏。

2)使用内核调试器来查找内核模式的内存泄露

启用池标记(Windows 2000和Windows XP)

在Windows 2000和Windows XP上,您必须先使用GFlags启用池标记。Windows调试工具中包含GFlags。启动GFlags,选择“ 系统注册表”选项卡,选中“ 启用池标记”框,然后单击“ 应用”。您必须重新启动Windows才能使此设置生效。在Windows Server 2003和更高版本的Windows上,池标记始终处于启用状态。

确定泄漏的标签

要确定哪个池标签与泄漏相关联,通常最简单的方法是使用PoolMon工具进行此步骤。有关详细信息,请参阅上面使用poolmon来查找内核模式的内存泄露

或者,您可以使用内核调试器查找与大型池分配关联的标记,请按照以下步骤操作

1.使用.reload(重新加载模块)命令重新加载所有模块。

2.使用!poolused扩展。包含标记“4”以便按页面内存使用排序输出:

kd> !poolused 4 

Sorting by Paged Pool Consumed

Pool Used:

       NonPaged                     Paged     

Tag            Allocs      Used            Allocs            Used 

Abc                 0          0            36405           33930272 

Tron                0          0            552             7863232 

IoN7                0          0            10939           998432 

Gla5                1         128           2222            924352 

Ggb                 0          0             22             828384

3. 确定哪个池标记与最大的内存使用相关联。在这个例子中,使用标签“Abc”的驱动程序正在使用最多的内存 – 几乎是34 MB。所以内存泄漏最有可能在这个驱动程序中。

确定与泄漏相关联的池标签后,请按照以下过程找到泄漏本身:

1.使用ed(Enter Values)命令来修改全局系统变量PoolHitTag的值。每当使用与其值匹配的池标记时,此全局变量会导致调试器中断。

2.将PoolHitTag设置为等于您怀疑是内存泄漏源的标记。模块名称“nt”应该被指定为更快的符号解析。标签值必须以little-endian格式输入(即向后)。由于池标签总是四个字符,这个标签实际上      是Abc空间,而不仅仅是Abc。所以使用下面的命令:

kd> ed nt!poolhittag ' cbA' 

要验证PoolHitTag的当前值,请使用db(显示内存)命令:

kd> db nt!poolhittag L4 

820f2ba4  41 62 63 20           Abc.

每次使用标签Abc分配或释放池时,调试器都会中断。每次调试器在这些分配或释放操作之一中断时,使用kb(Display Stack Backtrace)调试器命令查看堆栈跟踪。

使用此过程,您可以确定驻留在内存中的代码是否使用标记Abc过度分配池。

要清除断点,请将PoolHitTag设置为零:

kd> ed nt!poolhittag 0 

如果有几个不同的地方在分配带有这个标签的内存,并且这些地方在你编写的应用程序或驱动程序中,你可以改变你的源代码为每个分配使用唯一的标签。如果您不能重新编译程序,但是您想确定代码中几个可能位置中的哪一个导致泄漏,则可以在每个位置取消组合代码,并使用调试器编辑驻留在内存中的此代码,以便每个实例使用不同的(以前未使用的)池标签。然后让系统运行几分钟或更长时间。经过一段时间后,再次使用调试器分解并使用!poolfind扩展来查找与每个新标签关联的所有池分配。

3)使用驱动程序验证程序查找内核模式内存泄漏

驱动程序验证程序确定内核模式驱动程序是否在泄漏内存。

Driver Verifier的池跟踪功能监视指定的驱动程序所做的内存分配。在卸载驱动程序时,Driver Verifier会验证驱动程序所有分配的内存是否已经释放。如果某些驱动程序分配的内存没有被释放,则会发出错误检查,并且错误检查的参数会指出问题的性质。

当此功能处于活动状态时,使用Driver Verifier Manager图形界面来监视池分配统计信息。如果内核调试程序连接到驱动程序,请使用!verifier 0x3扩展名来显示分配统计信息。

如果驱动程序使用直接内存访问(DMA),则驱动程序验证程序的DMA验证功能也有助于查找内存泄漏。DMA验证测试了一些常见的DMA例程错误,包括释放常见缓冲区失败以及可能导致内存泄漏的其他错误。如果在此选项处于活动状态时连接了内核调试器,则使用!dma扩展名显示分配统计信息。

有关驱动程序验证程序的信息,请参阅Windows驱动程序工具包(WDK)文档中的驱动程序验证程序。

三. 查找用户模式内存泄漏

1)使用性能监视器来查找用户模式的内存泄露

如果您怀疑存在用户模式内存泄漏,但不确定哪个进程导致该泄露,则可以使用性能监视器来测量各个进程的内存使用情况,启动性能监视器。添加以下计数器:


Process–>Private Bytes (你要检查的每一个进程)

Process–>Virtual Bytes (你可能需要检查的每一个进程)

将更新时间更改为600秒,以捕获一段时间内的泄漏图象。您也可以将数据记录到文件以供日后检查。

专用字节(Private Bytes) 计数器是指一个进程已分配,不包括与其他进程共享存储器的存储器总量。

虚拟字节(Virtual Bytes) 计数器指当前进程正在使用的虚拟地址空间的大小

数据文件中出现的一些内存泄漏,因为分配的私有字节数增加所以其他内存泄漏显示为虚拟地址空间的增加。

2)使用UMDH查找用户模式内存泄漏

用户模式转储堆(UMDH)应用程序可与操作系统配合使用便于分析特定进程的Windows堆分配。UMDH可用于定位特定进程中的哪个例程正在泄漏内存。

UMDH包含在Windows调试工具中。详情请参阅 UMDH。

如果您还没有确定是哪个进程正在泄漏内存,请先参阅使用上面使用性能监视器查找用户模式内存泄漏

UMDH日志中最重要的数据是堆分配的堆栈跟踪。要确定某个进程是否在泄漏堆内存,请分析这些堆栈跟踪

在使用UMDH显示堆栈跟踪数据之前,您必须使用GFlags来正确配置您的系统。Windows调试工具中包含GFlags

以下GFlags设置启用UMDH堆栈跟踪:

在GFlags图形界面中,选择Image File选项卡,输入进程名称(包括文件扩展名),按TAB键,选择Create user mode stack trace database,然后点击Apply

或者使用以下GFlags命令行,其中ImageName是进程名称(包括文件扩展名):

gflags /i ImageName +ust

默认情况下,Windows收集的堆栈跟踪数据量在x86处理器上限制为32 MB,x64处理器上的64 MB。如果您想增加此数据库的大小,请选择GFlags图形界面中的图像文件选项卡,输入进程名称,按TAB键,选中堆栈回溯(Megs)复选框,在数据库中输入一个值(以MB为单位)关联文本框,然后单击应用。仅在必要时增加此数据库,因为它可能会耗尽有限的Windows资源。当您不再需要较大空间时,请将此设置恢复为原始值。如果您在“ 系统注册表”选项卡上更改了任何标志,则必须重新启动Windows才能使这些更改生效。如果更改了“ 映像文件”选项卡上的任何标志,则必须重新启动该过程以使更改生效。“ 内核标志”选项卡的更改立即生效,但在下次重新启动时会丢失。在使用UMDH之前,您必须有权访问您的应用程序的正确符号。UMDH使用由环境变量_NT_SYMBOL_PATH指定的符号路径。将此变量设置为等于包含应用程序符号的路径。如果您还包含Windows符号的路径,分析可能会更完整。该符号路径的语法与调试器使用的语法相同; 有关细节,请参阅符号路径。

例如,如果您的应用程序的符号文件位于C:\ MySymbols,并且想要将Windows公共符号存储用于Windows符号,则可以使用以下命令来设置你的符号路径:
set _NT_SYMBOL_PATH=c:\mysymbols;srv*c:\mycache*https://msdl.microsoft.com/download/symbols 
另外,为了确保准确的结果,您必须禁用BSTR缓存。为此,请将OANOCACHE环境变量设置为一(1)在启动要跟踪其分配的应用程序之前进行此设置。

如果您需要跟踪服务分配,则必须将OANOCACHE设置为系统环境变量,然后重新启动Windows以使此设置生效。

在Windows 2000上,除了将OANOCACHE设置为1之外,还必须安装Microsoft支持文章139071提供的修补程序。在Windows XP和更高版本的Windows上不需要此修补程序。
做好这些准备之后,可以使用UMDH捕获有关进程的堆分配的信息。为此,请按照以下步骤操作:首先确定您要调查的过程的过程ID(PID)

然后使用UMDH分析此进程的堆内存分配,并将其保存到日志文件。将-p开关与PID一起使用,-f开关与日志文件的名称一起使用。例如,如果PID是124,并且想要命名日志文件Log1.txt,请使用以下命令:
umdh -p:124 -f:log1.txt 
使用记事本或其他程序打开日志文件。该文件包含每个堆分配的调用堆栈,通过该调用堆栈分配的数量以及通过该调用堆栈消耗的字节数来观察程序的内存分配

因为你正在寻找一个内存泄漏,单个日志文件的内容是不够的。您必须比较不同时间记录的日志文件,以确定哪些分配正在增长

UMDH可以比较两个不同的日志文件,并显示其各自分配大小的变化。您可以使用大于号(>)将结果重定向到第三个文本文件。您可能还需要包含-d选项,该选项将字节和分配计数从十六进制转换为十进制。例如,要比较Log1.txt和Log2.txt,并将比较结果保存到文件LogCompare.txt中,请使用以下命令:

umdh log1.txt log2.txt > logcompare.txt 

打开LogCompare.txt文件。其内容类似于以下内容:

+ 5320 ( f110 - 9df0) 3a allocs BackTrace00B53 

Total increase == 5320 

对于UMDH日志文件中的每个调用堆栈(标记为“BackTrace”),在两个日志文件之间进行比较。在本例中,第一个日志文件(Log1.txt)记录了为BackTrace00B53分配的0x9DF0字节,而第二个日志文件记录了0xF110个字节,这意味着在捕获两个日志之间分配了额外的0x5320字节。这些字节来自BackTrace00B53标识的调用堆栈。要确定回溯中的内容,请打开原始日志文件之一(例如Log2.txt)并搜索“BackTrace00B53”。结果与这些数据相似:

00005320 bytes in 0x14 allocations (@ 0x00000428) by: BackTrace00B53
ntdll!RtlDebugAllocateHeap+0x000000FD
ntdll!RtlAllocateHeapSlowly+0x0000005A
ntdll!RtlAllocateHeap+0x00000808
MyApp!_heap_alloc_base+0x00000069
MyApp!_heap_alloc_dbg+0x000001A2
MyApp!_nh_malloc_dbg+0x00000023
MyApp!_nh_malloc+0x00000016
MyApp!operator new+0x0000000E
MyApp!DisplayMyGraphics+0x0000001E
MyApp!main+0x0000002C
MyApp!mainCRTStartup+0x000000FC
KERNEL32!BaseProcessStart+0x0000003D  

UMDH输出显示从调用堆栈分配的总字节数为0x5320(十进制数21280)。这些字节是从0x14(十进制20)分配的0x428(十进制1064)个字节开始分配的。调用堆栈被赋予一个标识符“BackTrace00B53”,并显示这个堆栈中的调用。在查看调用堆栈时,您会看到DisplayMyGraphics例程通过new运算符分配内存,该运算符调用例程malloc,该malloc使用Visual C ++运行时库从堆中获取内存。确定这些调用中的哪一个是最后一个显式出现在您的源代码中。在这种情况下,它可能是新的操作符,因为对malloc的调用是作为新实现的一部分发生的,而不是作为单独的分配。因此,DisplayMyGraphics例程中新运算符的这个实例会重复分配未被释放的内存。

翻译:https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/finding-a-memory-leak


声明:该文观点仅代表作者本人,转载请注明来自看雪