快速定位windows堆溢出

VSole2022-08-05 16:21:14

windows堆溢出的排查的困难

windows的堆溢出,具有一定的滞后性,溢出后有时不会立即崩溃,可能运行一段时间后再崩溃,此时查看调用堆栈,也不是溢出的第一现场,此时的调用堆栈意义也不大,看不出什么猫腻,所以堆溢出,对开发人员排查具有一定的难度。本文带你寻找定位堆溢出的第一现场。

页堆结构

1、页堆结构

图1画出了页堆的结构,其中的地址是以x86系统中的一个典型页堆。左侧的矩形是页堆的主题部分,右侧是附属的普通堆,创建每个页堆时,堆管理器都会创建一个附属的普通堆。

页堆上空间大多是以内存页来组织的,第一个内存页(起始4KB)用来伪装普通堆的HEAP结构,大多数空间被填充为0xeeeeeeee,只有少数字段(Flags和ForceFlags)是有效的,这个内存页的属性是只读的。因此可用来检测应用程序意外写HEAP结构的错误。

第二个内存页的开始处是一个DPH_HEAP_ROOT结构,该结构包含了DPH的各种信息和链表,是描述和管理页堆的重要资料。它的第一个字段是这个结构的签名(signature),固定为0xffeeddcc,与普通的堆结构的签名0xeeffeeff不同。它的NormalHeap字段记录着附属普通堆的句柄。

DPH_HEAP_ROOT结构之后的一段空间用来存储堆块节点,称为堆块节点池(node pool)。为了防止堆块的管理信息被覆盖,除了在堆块的用户数据区前面储存堆块信息,页堆还会在节点池中为每个堆块记录一个DPH_HEAP_BLOCK结构,简称为DPH节点结构。多个节点是以链表的形式链接在一起的。

DPH_HEAP_BLOCK结构的pNodePoolListHead字段记录这个链表的开头,pNodePoolListTail字段记录链表的结尾。

它的第一个节点描述的是DPH_HEAP_ROOT结构和节点池本身所占用的空间。节点池的典型大小是4个内存页(16KB)减去DPH_HEAP_ROOT结构的大小。

节点池后的一个内存页用来存放同步用的关键区对象,即_RTL_CRITICAL_SECTION结构,这个结构之外的空间被填充为0,DPH_HEAP_BLOCK结构的HeapCritSect字段记录着关键区对象的地址。

2、堆块结构

与普通堆块相比,页堆的堆块结构有很大的不同,每个堆块至少占用两个内存页(1个内存页4KB),在用于存放用户数据的内存页后面,堆管理器总会多分配一个内存页,这个内存页是专门用来检测溢出的,我们称其为栅栏页(fense page)。

栅栏页的页属性被设置为不可访问(PAGE_NOACCESS),因此,一旦用户数据区发生溢出并触及栅栏页,便会引发异常,如果程序在调试,那么调试器便会立刻收到异常,使调试人员可以在第一现场发现问题,并迅速定位到导致溢出的代码.

数据区在第一个内存页的结尾,第二个内存页紧邻在数据区的后面,图2显示了这样的一个页堆堆块(DPH_HEAP_BLOCK)的数据布局。

(图2 堆块结构 摘自《软件调试》)

堆溢出调查方法一:使用页堆调查堆溢出

页堆的工作机制

在堆块周围加一个不可访问的保护页,用来将各个堆块区分开;如果保护页被覆盖,将尽可能在接近覆盖问题发生的地方检测到问题并中断给调试器。

页堆有2种模式

普通页堆(normal page heap)和完全页堆(full page heap),两者主要差别在于保护方式(普通页堆在堆前后用固定填充数据保护,完全页堆用一个不可访问的页保护)。

最常见的元数据被覆盖问题是在使用堆时没有考虑到边界问题,导致的溢出问题。下面是溢出可能影响堆结构的示意图:

(图3 堆溢出图示)

测试代码:

//堆溢出示例//现象:输入字符长度小于等于10,程序正常运行;超过10个字符,程序会崩溃#include #include #include #include "dump.h"WCHAR* pszCopy = NULL;bool DupString(WCHAR* psz) {    bool bRet = false;    if (psz != NULL) {        pszCopy = (WCHAR*)HeapAlloc(GetProcessHeap(), 0, 10 * sizeof(WCHAR)); //10个宽字符内存        if (pszCopy) {            wcscpy(pszCopy, psz);                      //隐患:字符超过10个就会产生溢出            wprintf(L"Copy of string: %s", pszCopy);            HeapFree(GetProcessHeap(), 0, pszCopy);            bRet = true;        }    }    return bRet;}void __cdecl wmain(int argc, WCHAR* args[]) {    SetUnhandledExceptionFilter(ExceptionFilter);//异常回调,生成dump文件    if (2 == argc) {        wprintf(L"Press any key to start");        _getch();        DupString(args[1]);    }    else {        wprintf(L"Please enter a string");    }}

step 1:启动完全页堆

gflag启用页堆,执行命令后,其实是修改的注册表。

命令:gflags.exe /p /enable app.exe /full

(图4 对测试程序开启页堆)

(图 5 gflags启用页堆后,gflags修改的注册表)

step 2:WinDbg调试程序

1) 确定页堆是否启用

(图 6 windbg查看页堆是否启动)

2) 触发堆溢出

windbg打开可执行程序、输入参数:xiaosangTestForOverflow,这个参数会拷贝到动态分配的缓冲区,用来溢出缓冲区。

使用命令.reload /f testHeapOverflow.exe加载符号,然后在调试的程序中按下任意键(代码中getchar),触发缓冲区溢出。

(图 7 windbg打开程序,并传递参数)

(图 8加载符号,并运行)

(图 9 堆溢出报错)

step 3:关闭完全页堆

gflags -p /disable testHeapOverflow.exe

(图 10 关闭完全页堆)

堆溢出调查方法二:应用程序验证器

step 1:应用程序验证器设置

选择File->add application,然后右击Basics下的Heaps,选择full,然后保存。

设置后,实际修改是注册表(下图可以看到设置后注册表的变化)。

(图 11 使用应用程序验证器完全页堆)

(图 12 使用应用程序验证器完全页堆后的注册表变化)

step 2:WinDbg调试结果

运行应用程序

(图 13 打开测试程序,并传递参数)

应用程序崩溃之后,再次输入go,获取校验停止码,显示是13。

(图 14 查看校验停止码)

查询校验停止的原因,并根据指导进行查找原因。

(图 15 打开堆校验停止窗口)

(图 16 查看如何排查校验停止码13的方法)

根据校验停止码的原因,在windbbg输入!heap –p –a accessAddr。

(图 17 查看堆溢出地址的调用堆栈,观看溢出内存)

堆内存
本作品采用《CC 协议》,转载必须注明作者和本文链接
内存泄漏最终会导致OOM。造成内存泄漏典型场景:1.单例模式的不正确使用单例对象在初始化后将在JVM的整个生命周期中以静态变量的方式存在。如果单例对象持有对外部对象的引用,那么这个对象将不能被JVM正常回收2.数据库、网络、IO连接没有被关闭释放这类连接,一般会提供close方法进行显式关闭。
区分为两大区:Young区和Old区,又称新生代和老年代。在不同的JVM实现及不同的回收机制中,内存的划分方式是不一样的。相对于基于寄存器的运行环境来说,JVM是基于栈结构的运行环境。在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构。在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。而StackOverflowError表示请求的栈溢出,导致内存耗尽,通常出现在递归方法中。
无论对于Java程序员还是大数据研发人员,JVM是必须掌握的技能之一。Java提供的GC功能可以自动监测对象是否超过作用域等从而达到自动回收内存的目的,可以有效防止内存泄露,有效的使用可用内存。
在2019年东京Pwn2Own大会上,无线路由器引入为一个新种类:NETGEAR Nighthawk R6700v3。但该路由存在一些安全隐患,其中包括一个溢出漏洞,该漏洞可能允许恶意第三方从局域网控制设备。在这篇文章中,我们将详细讨...
新生代垃圾收集器1. Serial收集器serial收集器即串行收集器,是一个单线程收集器。能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。G1取消了内结构的新生代、老年代的物理空间划分,将整个Java划分为大小固定的独立区域,后台维护一个优先列表来跟踪这些区域的垃圾堆积程度,每次根据允许收集的时间,优先回收垃圾最多的区域。
利用UAF漏洞UAF漏洞(Use-After-Free)是一种内存破坏漏洞,漏洞成因是一块内存被释放了之后又被使用。又被使用指的是:指针存在(悬垂指针被引用)。这个引用的结果是不可预测的,因为不知道会发生什么。由于大多数的内存其实都是C++对象,所以利用的核心思路就是分配去占坑,占的坑中有自己构造的虚表。
然而在内核态中,内存的分配策略发生了变化。并把这个slab划分为一个个object,并将这些object组成一个单向链表进行管理,这里需要注意slub系统把内存块当成object看待,而不是伙伴系统中的页。本次选择演示的例题是2019-SUCTF的sudrv例题,查看start.sh中的信息可以发现开启了kaslr保护与smep保护。
奇安信CERT致力于第一时间为企业级用户提供安全风险通告和有效解决方案。风险通告近日,奇安信CERT监测到官
推荐 goleak 的背景goroutine 作为 golang 并发实现的核心组成部分,非常容易上手使用,但却很难驾驭得好。我们经常会遭遇各种形式的 goroutine 泄漏,这些泄漏的 goroutine 会一直存活直到进程终结。等性能分析工具更多是作用于监控报警/故障之后的复盘。我们需要一款能在编译部署前识别 goroutine 泄漏的工具,从更上游把控工程质量。
上周,该威胁组织受到韩国外交部的制裁。赔偿金将由谷歌管理层自行决定,并且在 12 个月内最高不超过 100 万美元。该计划目前涵盖由 SCC Premium 的虚拟机威胁检测支持的 Compute Engine 虚拟机和计算环境。
VSole
网络安全专家