前言
你见过断点+内存映射,制造了一个另类隐藏极深,强悍的 BUG
吗?这是一个虚拟机 CLR
的 BUG
。不同于之前所遇见的 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
的组成如下图所示:
可以看到 Addr1
和 field1
的起始地址都是 Base
,而 Base
则是被 MapViewOfFileEx Windows API
内存映射的起始地址。Addr1
则是被映射的这块内存里面的某个函数中的某个地址。这里假如说它是程序入口 Main
函数的函数头地址,也可以是 Main
函数中间的某个地址。如下图:
因为实际上在 Addr1
处下了断点,也即是在被 MapViewOfFileEx
映射的内存地址里面下了断点。在内存映射里面下了断点,就会导致了通过 MapViewOfFile
映射的内存 pRW
赋值的时候,pRX
会被赋值不上的情况。
pRX
和 pRW
如下图所示:
如果把这个断点,下在 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
调用略有不同?
转载声明: