快速定位windows堆溢出
2022-8-5 18:1:55
Author: 看雪学苑(查看原文)
阅读量:15
收藏
本文为看雪论坛优秀文章
看雪论坛作者ID:sanganlei
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)的数据布局。在堆块周围加一个不可访问的保护页,用来将各个堆块区分开;如果保护页被覆盖,将尽可能在接近覆盖问题发生的地方检测到问题并中断给调试器。普通页堆(normal page heap)和完全页堆(full page heap),两者主要差别在于保护方式(普通页堆在堆前后用固定填充数据保护,完全页堆用一个不可访问的页保护)。最常见的元数据被覆盖问题是在使用堆时没有考虑到边界问题,导致的溢出问题。下面是溢出可能影响堆结构的示意图://堆溢出示例
//现象:输入字符长度小于等于10,程序正常运行;超过10个字符,程序会崩溃
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#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\n");
_getch();
DupString(args[1]);
}
else {
wprintf(L"Please enter a string");
}
}
step 1:启动完全页堆
gflag启用页堆,执行命令后,其实是修改的注册表。命令:gflags.exe /p /enable app.exe /full(图 5 gflags启用页堆后,gflags修改的注册表)
step 2:WinDbg调试程序
1) 确定页堆是否启用
2) 触发堆溢出
windbg打开可执行程序、输入参数:xiaosangTestForOverflow,这个参数会拷贝到动态分配的缓冲区,用来溢出缓冲区。使用命令.reload /f testHeapOverflow.exe加载符号,然后在调试的程序中按下任意键(代码中getchar),触发缓冲区溢出。step 3:关闭完全页堆
gflags -p /disable testHeapOverflow.exestep 1:应用程序验证器设置
选择File->add application,然后右击Basics下的Heaps,选择full,然后保存。设置后,实际修改是注册表(下图可以看到设置后注册表的变化)。(图 12 使用应用程序验证器完全页堆后的注册表变化)step 2:WinDbg调试结果
应用程序崩溃之后,再次输入go,获取校验停止码,显示是13。根据校验停止码的原因,在windbbg输入!heap –p –a accessAddr。(图 17 查看堆溢出地址的调用堆栈,观看溢出内存)
看雪ID:sanganlei
https://bbs.pediy.com/user-home-917132.htm
*本文由看雪论坛 sanganlei 原创,转载请注明来自看雪社区
文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458460890&idx=2&sn=6dda0e1e0dbde3e28864d065c2ad3745&chksm=b18e125086f99b46b5f7e384e417c9c209a052445c1a4c33659f298da40476785f6ed5d9e038#rd
如有侵权请联系:admin#unsafe.sh