绝顶技术:断点+内存映射组合的 CLR 超强 BUG?

本文涉及的产品
注册配置 MSE Nacos/ZooKeeper,118元/月
函数计算FC,每月15万CU 3个月
应用实时监控服务-用户体验监控,每月100OCU免费额度
简介: 你见过【断点+内存映射】制造了一个另类隐藏极深,强悍的 BUG 吗?这是一个虚拟机 CLR 的 BUG。不同于之前所遇见的 BUG 这次费时最多,但是问题已然清晰。

前言

你见过断点+内存映射,制造了一个另类隐藏极深,强悍的 BUG 吗?这是一个虚拟机 CLRBUG。不同于之前所遇见的 BUG 这次费时最多,但是问题已然清晰。本篇来看下。

友情提示:学会本篇,你就是绝级的高手,足可笑傲当世。

概括

1、问题说明

BUG 的起因在后面,先看看问题的描述。假如说遇到这样一个问题,在某个地址(以 Addr1 表示)下了一个断点,程序继续运行,就会某个地方抛出一个异常,首先确认的是这段运行的代码是完全没有问题的。也就是说这个异常只会在下了断点之后,才会抛出。查看堆栈,这个异常非常清晰明了,那就是程序运行过程中某个字段(filed1)的值为 0。而通过这个字段也就是 field1 的空值去访问 field1 的成员变量,自然是报了异常。

问题很简单,似乎马上就找到了异常出错的地方,也就是 field1==0 造成的。但为什么 field1 会为空?它在哪里被赋值的,导致它是空值?跟下断点有什么关系? 这些都没解决。

问题一:field1 在哪里被赋值的?

经过跟踪发现,field1 是通过 Windows API 的两个函数 MapViewOfFileEx,MapViewOfFile 进行内存映射来赋值的。这两个内存映射函数映射了两个内存地址。

  • MapViewOfFileEx 映射的是可读,可写,可执行的内存地址(以 pRX 来表示),也即是:
FILE_MAP_EXECUTE | FILE_MAP_READ | FILE_MAP_WRITE
  • MapViewOfFile 映射的是可读,可写的内存地址(以 pRW 来表示),也即是:
FILE_MAP_READ | FILE_MAP_WRITE

当往 pRW 内存地址写入数值,pRX 也同时写入相应的数值,这就是内存映射。这里就是 field1 被赋值的地方。

问题二:为什么会导致 field1 空值?

上面说的是,在某个地址也就是上面说的 Addr1 这个地方下了一个断点,跟踪发现,如果不在 Addr1 处下断点,那么 field1 不等于 0,也就不会报异常,如果在 Addr1 处下断点,那么 field1 等于 0,导致了异常的发生。

这个 BUG 很诡异,难道是断点造成的?

继续跟踪发现,如果在离 Addr1 偏移量很远的地址下断点,则不会导致了 field1==0,如果在 Addr1 地址上下偏移的地方下断点(也就是偏移比较近的位置),则会导致 field1 等于 0。难道 Addr1 地址的上下偏移范围跟 field1 有一定的关联?

继续跟踪发现,field 的值在 Addr1 地址的后面,它的值本身也是一个地址。每块内存都有一个起始地址,姑且叫 Base。那么 filed,Addr1,Base 的组成如下图所示:

fe6cc708eb6f6f86dea4a904ac8de294.png

可以看到 Addr1field1 的起始地址都是 Base,而 Base 则是被 MapViewOfFileEx Windows API 内存映射的起始地址。Addr1 则是被映射的这块内存里面的某个函数中的某个地址。这里假如说它是程序入口 Main 函数的函数头地址,也可以是 Main 函数中间的某个地址。如下图:

6670fe9454ff841810fdcddf18deff7a.png

因为实际上在 Addr1 处下了断点,也即是在被 MapViewOfFileEx 映射的内存地址里面下了断点。在内存映射里面下了断点,就会导致了通过 MapViewOfFile 映射的内存 pRW 赋值的时候,pRX 会被赋值不上的情况。

pRXpRW 如下图所示:

49563ee3e40702ce35dbf65306ea0a97.png

如果把这个断点,下在 MapViewOfFileEx 映射的内存范围之外,则不会存在赋值不上的情况。

这里可以确定的就是,在内存映射的范围内下断点,断点会干扰内存映射范围内的数值。

2、检测上面结论是否正确

上面只是问题的分析,如果想要检验上面所述 BUG 问题是否正确。则需要代码加以辅助证明。

下面是一段内存映射的代码:


#include<stdio.h>
#include<Windows.h>

#define DPTR(type) type*
#define VAL32(x) x
#define HIDWORD(_qw)    ((ULONG)((_qw) >> 32))
#define LODWORD(_qw)    ((ULONG)(_qw))

#define VIRTUAL_ALLOC_RESERVE_GRANULARITY (64*1024) 

typedef DPTR(IMAGE_DOS_HEADER) PTR_IMAGE_DOS_HEADER;
typedef DPTR(IMAGE_NT_HEADERS) PTR_IMAGE_NT_HEADERS;
typedef long long int64_t;
typedef unsigned long long uint64_t;
static const uint64_t MaxDoubleMappedSize = 2048ULL * 1024 * 1024 * 1024;

typedef unsigned __int64 ULONG_PTR, * PULONG_PTR;
typedef ULONG_PTR TADDR;

extern "C" IMAGE_DOS_HEADER __ImageBase;
typedef UINT32  COUNT_T;

template <typename T> inline T ALIGN_UP(T val, size_t alignment)
{
   
    return (T)ALIGN_UP((size_t)val, alignment);
}

void* GetClrModuleBase()
{
   
    return (void*)&__ImageBase;
}

IMAGE_NT_HEADERS* FindNTHeaders(TADDR m_base)
{
   
    return PTR_IMAGE_NT_HEADERS(m_base + VAL32(PTR_IMAGE_DOS_HEADER(m_base)->e_lfanew));
}
COUNT_T GetVirtualSize(TADDR base)
{
   
    return FindNTHeaders(base)->OptionalHeader.SizeOfImage;
}
void main()
{
   
    size_t pMaxExecutableCodeSize = (size_t)MaxDoubleMappedSize;

    void* pHandle = CreateFileMapping(
        INVALID_HANDLE_VALUE,    // use paging file
        NULL,                    // default security
        PAGE_EXECUTE_READWRITE | SEC_RESERVE,  // read/write/execute access
        HIDWORD(MaxDoubleMappedSize),                       // maximum object size (high-order DWORD)
        LODWORD(MaxDoubleMappedSize),   // maximum object size (low-order DWORD)
        NULL);

    SIZE_T sizeOfLargePage = GetLargePageMinimum();
    int nCount = 10;
    PVOID pAddr = VirtualAlloc(NULL, sizeOfLargePage * nCount, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

    MEMORY_BASIC_INFORMATION mbi;
    VirtualQuery(pAddr, &mbi, sizeof mbi);

    void* base = GetClrModuleBase();
    SIZE_T base1 = (SIZE_T)base;
    SIZE_T size = GetVirtualSize((TADDR)base1);
    SIZE_T reach = 0x7FFF0000u;
    BYTE* g_preferredRangeMin = (base1 + size > reach) ? (BYTE*)(base1 + size - reach) : (BYTE*)0;
    BYTE* g_preferredRangeMax = (base1 + reach > base1) ? (BYTE*)(base1 + reach) : (BYTE*)-1;

    BYTE* pStart;
    pStart = g_preferredRangeMin + (g_preferredRangeMax - g_preferredRangeMin) / 8;
    pStart += 0x1000 * 0x00000003;
    BYTE* tryAddr = pStart; //(BYTE*)ALIGN_UP((BYTE*)pStart, VIRTUAL_ALLOC_RESERVE_GRANULARITY);

    BYTE* pRX = (BYTE*)MapViewOfFileEx((HANDLE)pHandle,
        FILE_MAP_EXECUTE | FILE_MAP_READ | FILE_MAP_WRITE,
        HIDWORD((int64_t)0),
        LODWORD((int64_t)0),
        0x0000000000010000,
        g_preferredRangeMax);

    VirtualAlloc(pRX, 0x0000000000010000, MEM_COMMIT, PAGE_EXECUTE_READ);

        MEMORY_BASIC_INFORMATION mbInfo;
    VirtualQuery((LPCVOID)pRX, &mbInfo, sizeof(mbInfo));

    void* pRW = (BYTE*)MapViewOfFile((HANDLE)pHandle,
        FILE_MAP_READ | FILE_MAP_WRITE,
        HIDWORD((int64_t)0),
        LODWORD((int64_t)0),
        0x0000000000010000);

    VirtualAlloc(pRW, 0x0000000000010000, MEM_COMMIT, PAGE_READWRITE);

    char abc[] = "abc";
    memcpy(pRW, abc, 3);
    VirtualQuery((LPCVOID)pRX, &mbInfo, sizeof(mbInfo));
}

以上例子,进行了一个内存模拟映射。通过以上例子,观察发现。当在 pRX 所在地址范围内下断点,则会导致当往 pRW 里面赋值的时候,pRX 赋值不上的情况,如下 pRX 地址处汇编代码:

Address:00007ff739180000() //pRX Address
00007FF73917FFFC  ?? ?????? 
00007FF73917FFFD  ?? ?????? 
00007FF73917FFFE  ?? ?????? 
00007FF73917FFFF  ?? ?????? 
00007FF739180000  add         byte ptr [rax],al  
00007FF739180002  add         byte ptr [rax],al  
00007FF739180004  add         byte ptr [rax],al  
00007FF739180006  add         byte ptr [rax],al

这里来到了 pRX 的地址 00007ff739180000 处,在 pRX 地址向后偏移 2 个字节处下断点,也即 00007FF739180002

然后在 pRW 地址处进行赋值,如下 pRW 处内存展示:

Address:0x000001BEE1610000 //pRW Memory
0010000000000000 0010000000000000 0000000000000000 0000000000000000 
0000000000000000 0000000000000000

这里的 pRW 地址是 0x000001BEE1610000

往它的第一个八字节赋值了:0010000000000000。然后看下 pRX 的的内存,如下:

Addres:0x00007FF739180000  //pRX Memory
0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000

可以看到在被 MapViewOfFileEx 映射的内存范围内下断点之后,pRW 的赋值并不能更改 pRX 的值。这就导致了开头的异常 BUG

3、代码还原

通过以上理论分析和代码分析,基本上确定了,这个 BUG 就是 断点+内存映射 造成的。如果把断点下在内存映射的范围内的某个一个地址上,则会导致内存赋值的失败。如果不下断点,或者断点不在内存映射范围内,则不存在这种情况。这应该是微软 Windows 内核的一个 BUG

以上就是全部用户态的 BUG 展示了,如果想要更深一些,则需要追踪 Windows内核,这个有时间再研究。

这个 BUG 起因于,CLR 调用 C# 入口 Main 的汇编代码里面下的断点,运行到 .Ctor 然后报了异常。这个异常的排查过程如上所示,但是依然有疑惑。就是为啥通过 VS 调试 C# 源代码则不会报这个异常。难道 VS 直接运行 C# 源代码跟 CLR 调用略有不同?

转载声明:

目录
相关文章
|
2月前
|
KVM 虚拟化
KVM的热添加技术之内存
文章介绍了KVM虚拟化技术中如何通过命令行调整虚拟机内存配置,包括调小和调大内存的步骤,以及一些相关的注意事项。
74 4
KVM的热添加技术之内存
|
2月前
|
存储 缓存 Linux
用户态内存映射
【9月更文挑战第20天】内存映射不仅包括物理与虚拟内存间的映射,还涉及将文件内容映射至虚拟内存,使得访问内存即可获取文件数据。mmap 系统调用支持将文件或匿名内存映射到进程的虚拟内存空间,通过多级页表机制实现高效地址转换,并利用 TLB 加速映射过程。TLB 作为页表缓存,存储频繁访问的页表项,显著提升了地址转换速度。
|
1月前
|
Linux C++
Linux c/c++文件虚拟内存映射
这篇文章介绍了在Linux环境下,如何使用虚拟内存映射技术来提高文件读写的速度,并通过C/C++代码示例展示了文件映射的整个流程。
46 0
|
2月前
|
存储 安全 Linux
将文件映射到内存,像数组一样访问
将文件映射到内存,像数组一样访问
31 0
|
2月前
ARM64技术 —— MMU处于关闭状态时,内存访问是怎样的?
ARM64技术 —— MMU处于关闭状态时,内存访问是怎样的?
|
2月前
|
消息中间件 Linux 容器
共享内存的创建和映射过程
【9月更文挑战第1天】消息队列、共享内存及信号量在使用前需生成key并获取唯一ID,均通过`xxxget`函数实现。
|
4月前
|
机器学习/深度学习 存储 缓存
操作系统中的内存管理技术
在数字世界的复杂架构中,操作系统扮演着枢纽的角色,其中内存管理作为其核心组件之一,保障了计算资源的高效利用与稳定运行。本文将深入探讨操作系统中内存管理的关键技术,包括虚拟内存、分页和分段机制,以及现代操作系统如何通过这些技术优化性能和提高系统稳定性。通过具体实例和数据分析,我们将揭示这些技术如何在实际应用中发挥作用,并讨论它们面临的挑战及未来发展方向。 【7月更文挑战第16天】
85 6
|
4月前
|
存储 缓存 Java
Android性能优化:内存管理与LeakCanary技术详解
【7月更文挑战第21天】内存管理是Android性能优化的关键部分,而LeakCanary则是进行内存泄漏检测和修复的强大工具。
|
4月前
|
物联网 云计算
操作系统中的内存管理技术解析
【7月更文挑战第13天】本文将深入探讨操作系统中至关重要的内存管理技术,包括虚拟内存、分页和分段机制等核心概念。我们将从内存管理的基本原理出发,逐步过渡到高级技术如交换空间和文件映射,最后讨论现代操作系统中内存管理面临的挑战与未来发展方向。文章旨在为读者提供对操作系统内存管理全面而深入的理解。
67 7
|
4月前
|
存储 缓存 NoSQL
Java中的内存数据库与缓存技术
Java中的内存数据库与缓存技术