Windows 之 CRT的检测内存泄露

VSole2023-07-21 09:58:54
一
使用方法
使用方法非常简单,首先定义宏_CRTDBG_MAP_ALLOC,然后包含头文件crtdbg.h,最后在main函数结尾调用_CrtDumpMemoryLeaks统计内存申请和释放的情况。相关例子如下,编译的时候需要在Debug模式:
#define _CRTDBG_MAP_ALLOC
#include 
#include 
#include 
int main()
{
    std::cout << "Hello World!";
    int* x = (int*)malloc(sizeof(int));
    *x = 7;
    printf("%d", *x);
    x = (int*)calloc(3, sizeof(int));
    x[0] = 7;
    x[1] = 77;
    x[2] = 777;
    printf("%d %d %d", x[0], x[1], x[2]);
    _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG); 
    _CrtDumpMemoryLeaks();
}

运行结果如下:

Detected memory leaks!
Dumping objects ->
main.cpp(16) : {163} normal block at 0x000002882AE17740, 12 bytes long.
 Data: <    M       > 07 00 00 00 4D 00 00 00 09 03 00 00 
main.cpp(10) : {162} normal block at 0x000002882AE148C0, 4 bytes long.
 Data: <    > 07 00 00 00 
Object dump complete.
// main.cpp(x) 表示在main.cpp的x行申请的内存没有被释放


二
CRT 检测的原理
在安装Visual Studio之后,Windows CRT的源码已经被存放在C:\Program Files (x86)\Windows Kits\10\Source\,这个目录下面有多个sdk的版本,我选择的是19041

内存的申请

在C++编程语言中,内存申请对应的关键字是newmalloc,其实new最后调用的也是malloc函数,对应源代码文件是debug_heap.cpp。在包含相关头文件之后,malloc函数的调用栈为:malloc -> _malloc_dbg -> heap_alloc_dbg -> heap_alloc_dbg_internal。heap_alloc_dbg_internal函数分析如下:

1.获取临界区,保证当前只有一个线程进入。

__acrt_lock(__acrt_heap_lock);
extern "C" void __cdecl __acrt_lock(_In_ __acrt_lock_id _Lock)
{
    EnterCriticalSection(&__acrt_lock_table[_Lock]);
}

2.执行自定义malloc的回调函数。

if (_crtBreakAlloc != -1 && request_number == _crtBreakAlloc)
{
    _CrtDbgBreak();
}
if (_pfnAllocHook && !_pfnAllocHook(
    _HOOK_ALLOC,
    nullptr,
    size,
    block_use,
    request_number,
    reinterpret_cast(file_name),
    line_number))
{
    if (file_name)
        _RPTN(_CRT_WARN, "Client hook allocation failure at file %hs line %d.", file_name, line_number);
    else
        _RPT0(_CRT_WARN, "Client hook allocation failure.");
    __leave;
}

_pfnAllocHook有一个默认的回调函数,也允许程序员自己定义,回调函数原型如下:

typedef int (__cdecl * _CRT_ALLOC_HOOK)(
    int  const allocation_type,
    void*                const data,
    size_t               const size,
    int                  const block_use,
    long                 const request,
    unsigned char const* const file_name,
    int                  const line_number
);

设置回调函数的接口为_CrtSetAllocHook

3.调用Windows API分配内存,不过需要多分配一些冗余内存,记录一些信息,用于管理malloc分配的内存。

管理的数据结构如下:

struct _CrtMemBlockHeader
{
    _CrtMemBlockHeader* _block_header_next;     // 
    _CrtMemBlockHeader* _block_header_prev;     // 双向链表,访问该双向链表的全局变量为__acrt_first_block
    char const*         _file_name;             // 调用malloc的文件名
    int                 _line_number;           // 调用malloc的行数
    int                 _block_use;             // 内存类型,所有内存类型如下
    /*
     #define _FREE_BLOCK      0     // 内存释放
     #define _NORMAL_BLOCK    1     // 内存申请
     #define _CRT_BLOCK       2     // 标注CRT库申请的内存
     #define _IGNORE_BLOCK    3     // 此类内存不进行管理
     #define _CLIENT_BLOCK    4     // 暂时没找到用法
     #define _MAX_BLOCKS      5     // 暂时没找到用法
     */
    size_t              _data_size;             // malloc分配的大小
    long                _request_number;        // 记录分配内存的序号,每次分配内存自增1
    unsigned char       _gap[no_mans_land_size]; // 标记
    // Followed by:
    // unsigned char    _data[_data_size];          // malloc返回的内存
    // unsigned char    _another_gap[no_mans_land_size];    // 标记
};

结构中成员_gap填充了no_mans_land_size(4)个0xFD,在释放内存时检测写内存时是否出现溢出(上溢)。该结构后续的内容是malloc返回的内存,内存中被填充了0xCD。最后内存_another_gap也是填充了no_mans_land_size(4)个0xFD,在释放内存时检测写内存时是否出现溢出(下溢)。

内存的扩容

在C++编程语言中,内存扩容的关键字为realloc,对应的源文件是realloc.cpp,realloc函数的调用栈为:realloc -> _realloc_dbg -> realloc_dbg_nolock。该函数的函数原型如下:
static void * __cdecl realloc_dbg_nolock(
    void*       const block,
    size_t*     const new_size,
    int         const block_use,
    char const* const file_name,
    int         const line_number,
    bool        const reallocation_is_allowed
    ) throw()

1.检查block和new_size的情况。

if (!block)         // block为nullptr,蜕变为malloc(size)
{
    return _malloc_dbg(*new_size, block_use, file_name, line_number);
}
if (reallocation_is_allowed && *new_size == 0)  // *new_size为0,则蜕变为free(block)
{
    _free_dbg(block, block_use);
    return nullptr;
}

2.调用_pfnAllocHook回调函数,参数allocation_type为_HOOK_REALLOC。

if (_pfnAllocHook && !_pfnAllocHook(
    _HOOK_REALLOC,
    block,
    *new_size,
    block_use,
    request_number,
    reinterpret_cast(file_name),
    line_number))
{
    if (file_name)
        _RPTN(_CRT_WARN, "Client hook re-allocation failure at file %hs line %d.", file_name, line_number);
    else
        _RPT0(_CRT_WARN, "Client hook re-allocation failure.");
    return nullptr;
}

3.对block进行一系列检查。

is_block_an_aligned_allocation(block)       // 检查block是否被_aligned_malloc分配的,若是,则返回nullptr.
_ASSERTE(_CrtIsValidHeapPointer(block));    // 保证block内存属于进程堆
检查 -> 堆的类型是否是_IGNORE_BLOCK
检查 -> block的header的_data_size是否被破坏
检查 -> *new_size 是否过大
// Ensure the new requested size is not too large:
if (*new_size > static_cast(_HEAP_MAXREQ - no_mans_land_size - sizeof(_CrtMemBlockHeader)))
{
    errno = ENOMEM;
    return nullptr;
}

4.分配一个新的_CrtMemBlockHeader结构。

size_t const new_internal_size{sizeof(_CrtMemBlockHeader) + *new_size + no_mans_land_size};
_CrtMemBlockHeader* new_head{nullptr};
new_head = static_cast<_CrtMemBlockHeader*>(_realloc_base(old_head, new_internal_size));
// _realloc_base中调用HeapReAlloc函数重新分配

5.对新分配的内存初始化。

// If the block grew, fill the "extension" with the land fill value:
if (*new_size > new_head->_data_size)       // *new_size 新的内存大于原来的
{
    memset(new_block + new_head->_data_size, clean_land_fill, *new_size - new_head->_data_size);
}
// Fill in the gap after the client block:
memset(new_block + *new_size, no_mans_land_fill, no_mans_land_size);    // 填充向下溢出的标记

6.将新的header链接到双向链表中。

// 删除原来的元素
new_head->_block_header_prev->_block_header_next = new_head->_block_header_next;
new_head->_block_header_prev->_block_header_next = new_head->_block_header_next;
// 替换__acrt_first_block指向的元素
__acrt_first_block->_block_header_prev = new_head;
new_head->_block_header_next = __acrt_first_block;
new_head->_block_header_prev = nullptr;
__acrt_first_block = new_head;

内存的释放

在C++编程语言中,内存释放对应的关键字是deletefree,delete操作符最后调用到free函数,对应的源文件是debug_heap.cpp
free函数的调用栈为:free -> _free_dbg -> free_dbg_nolock,_free_dbg函数会获取临界区然后调用free_dbg_nolock。free_dbg_nolock函数分析过程如下:

1.释放内存时block_use为_FREE_BLOCK,若此时block_use为_NORMAL_BLOCK,且block由_aligned_malloc分配,不进行内存释放。

// Check to ensure that the block was not allocated by _aligned routines
if (block_use == _NORMAL_BLOCK && is_block_an_aligned_allocation(block))
{
    // We don't know (yet) where (file, linenum) block 

2.调用_pfnAllocHook,只不过allocation_type换成了_HOOK_FREE。

// Forced failure handling
if (_pfnAllocHook && !_pfnAllocHook(_HOOK_FREE, block, 0, block_use, 0, nullptr, 0))
{
    _RPT0(_CRT_WARN, "Client hook free failure.");
    return;
}

3.进行一系列检查。

_ASSERTE(_CrtIsValidHeapPointer(block));    // 保证block是从进程堆申请的
_ASSERTE(is_block_type_valid(header->_block_use)); // 保证block的堆类型是正常的
_ASSERTE(header->_block_use == block_use || header->_block_use == _CRT_BLOCK && block_use == _NORMAL_BLOCK);
// 检查之前的标记是否被破坏,被破坏意味着存在内存溢出
check_bytes(header->_gap, no_mans_land_fill, no_mans_land_size)
check_bytes(block_from_header(header) + header->_data_size, no_mans_land_fill, no_mans_land_size)

4.在双向链表中,删除block元素,并释放内存。

// 删除双向链表中的元素
header->_block_header_next->_block_header_prev = header->_block_header_prev;
header->_block_header_prev->_block_header_next = header->_block_header_next;
// 调用Windows api释放内存
extern "C" void __declspec(noinline) __cdecl _free_base(void* const block)
{
    if (block == nullptr)
    {
        return;
    }
    if (!HeapFree(select_heap(block), 0, block))
    {
        errno = __acrt_errno_from_os_error(GetLastError());
    }
}

内存统计

调用_CrtDumpMemoryLeaks进行内存统计,主要是两个函数:_CrtMemCheckpoint(统计)和_CrtMemDumpAllObjectsSince(显示)

◆_CrtMemCheckpoint主要统计除了_FREE_BLOCK类型之外的其他内存,结果的数据结构如下:

typedef struct _CrtMemState
{
    struct _CrtMemBlockHeader * pBlockHeader;   // __acrt_first_block
    size_t lCounts[_MAX_BLOCKS];    // 统计各类型的数据
    size_t lSizes[_MAX_BLOCKS];     // 统计各类型内存的大小
    size_t lHighWaterCount;         // 
    size_t lTotalCount;             // 所有的内存
} _CrtMemState;

◆_CrtMemDumpAllObjectsSince主要显示_NORMAL_BLOCK、_CRT_BLOCK和_CLIENT_BLOCK类型的内存,显示的回调函数可以自行设置,函数原型如下:

typedef void (__cdecl * _CRT_DUMP_CLIENT)(void *, size_t);


三
CRT库检测内存泄露的优缺点

优点

◆Windows SDK自带的内存泄露检测工具,使用简单方便。

缺点

◆无法检测使用Windows APi来分配内存的情况,如: HeapAlloc或VirtualAlloc。

◆仅使用源码模式下的检测,对于已编译成功的二进制文件无能为力。

◆若程序依赖于其他的库文件,库文件出现的内存泄露无法被检测。

crtblock
本作品采用《CC 协议》,转载必须注明作者和本文链接
相关例子如下,编译的时候需要在Debug模式:#define _CRTDBG_MAP_ALLOC. main.cpp : {163} normal block at 0x000002882AE17740, 12 bytes long.Data: < M > 07 00 00 00 4D 00 00 00 09 03 00 00. main.cpp : {162} normal block at 0x000002882AE148C0, 4 bytes long.内存的申请在C++编程语言中,内存申请对应的关键字是new或malloc,其实new最后调用的也是malloc函数,对应源代码文件是debug_heap.cpp。在包含相关头文件之后,malloc函数的调用栈为:malloc -> _malloc_dbg -> heap_alloc_dbg -> heap_alloc_dbg_internal。_CrtMemBlockHeader* _block_header_prev; // 双向链表,访问该双向链表的全局变量为__acrt_first_block. size_t _data_size; // malloc分配的大
一篇静态免杀的文章
如果手工测试无果后再用代理手法扫描网站,必要的时候设置二级代理。什么站能扫什么站不能扫,心里一定要有点 b 数!浅蓝的渗透测试导图以及小工具这里有一个更加详细的导图,可以做一个大致的方向参考,跟着方向逐一测试, 期间也可以巩固基础。
docker run -it -d -p 13443:3443 -p 8834:8834 leishianquan/awvs-nessus:v1. 如XSS,XSRF,sql注入,代码执行,命令执行,越权访问,目录读取,任意文件读取,下载,文件包含,远程命令执行,弱口令,上传,编辑器漏洞,暴力破解等验证码与邮箱以及token的返回泄露,以及后台为校验从而可删除的参数。从某个成功请求中捕获数据包观察cookie或者token是否存在规律或加密。token的key参数解密构建获取真实user密钥,可拼接、规律、时间戳……winodws桌面:TeamViewerQS单文件windows下载文件;certutil -urlcache -split -f?
项目的bug和不足作者只实现了TCP-client的代码,并且x86下测试通过,但是在x64模式下连接到服务端时出现了错误。
ProxyOracle漏洞分析
2021-12-07 14:03:00
NO.1 前言2021年8月份,oracle又公开了代理漏洞ProxyOracle、ProxyShell。本文则分析ProxyOracle具体的一些攻击细节。Padding Oracle攻击根据加解密时是否用同一组密钥,可以分为对称加密和非对称加密。对称加密中又存在流加密与分组加密两种加密方法。
安装Linux系统最小化,即选包最小化,yum安装软件包也要最小化,无用的包不装。开机自启动服务最小化,即无用的服务不开启。Linux系统文件及目录的权限设置最小化,禁止随意创建、更改、删除文件。在生产环境中,删除多余的账户信息。
强网杯-WriteUp
2022-08-02 08:02:30
然后使用 admin/123登录管理员账户即可,登录后存在购买页面,经过测试,使用如下 payload 可以绕过检查,再访问主页面即可获得 flag
MSF+生成流量免杀木马
2022-01-14 11:34:16
在实战中,即便你绕过了杀毒软件的检测,也很有可能会结束在某些流量监控的设备上。MSF可以说其是每一个内网玩家的必用工具。理所当然,这款工具也自然而然地被各大安全厂商分析,捕捉其在命令执行时产生的数据和流量。当我们使用一个没有做过加密处理的原版工具时,内网中的安全设备会根据我们的流量特征进行判断,认定我们为恶意进程,从而导致控制中断。Meterpreter技巧生成后门msfvenom?
BlackHat21中,Specterops发布了Active Directory Certificate Services利用白皮书。尽管ADCS并不是默认安装,但在大型企业域中通常被广泛部署。 本文分为上下两篇,结合实战,讲述如何在域环境中利用ADCS手法拿下域控,哪些对象ACL可用于更好的权限维持,并涉及ADCS的基础架构、攻击面、后利用等。
VSole
网络安全专家