絕頂技術(shù):斷點(diǎn)+內(nèi)存映射組合的CLR超強(qiáng)Bug?
前言
你見過斷點(diǎn)+內(nèi)存映射,制造了一個另類隱藏極深,強(qiáng)悍的BUG嗎?這是一個虛擬機(jī)CLR的BUG。不同于之前所遇見的BUG這次費(fèi)時最多,但是問題已然清晰。本篇來看下。
友情提示:學(xué)會本篇,你就是絕級的高手,足可笑傲當(dāng)世。
概括
1.問題說明
BUG的起因在后面,先看看問題的描述。假如說遇到這樣一個問題,在某個地址(以Addr1表示)下了一個斷點(diǎn),程序繼續(xù)運(yùn)行,就會某個地方拋出一個異常,首先確認(rèn)的是這段運(yùn)行的代碼是完全沒有問題的。也就是說這個異常只會在下了斷點(diǎn)之后,才會拋出。查看堆棧,這個異常非常清晰明了,那就是程序運(yùn)行過程中某個字段(filed1)的值為0。而通過這個字段也就是field1的空值去訪問field1的成員變量,自然是報了異常。
問題很簡單,似乎馬上就找到了異常出錯的地方,也就是field1==0造成的。但為什么field1會為空?它在哪里被賦值的,導(dǎo)致它是空值?跟下斷點(diǎn)有什么關(guān)系?這些都沒解決。
問題一:field1在哪里被賦值的?
經(jīng)過跟蹤發(fā)現(xiàn),field1是通過Windows API的兩個函數(shù)MapViewOfFileEx,MapViewOfFile進(jìn)行內(nèi)存映射來賦值的。這兩個內(nèi)存映射函數(shù)映射了兩個內(nèi)存地址。
MapViewOfFileEx映射的是可讀,可寫,可執(zhí)行的內(nèi)存地址(以pRX來表示)。也即是:
FILE_MAP_EXECUTE | FILE_MAP_READ | FILE_MAP_WRITE
MapViewOfFile映射的是可讀,可寫的內(nèi)存地址(以pRW來表示),也即是:
FILE_MAP_READ | FILE_MAP_WRITE
當(dāng)往pRW內(nèi)存地址寫入數(shù)值,pRX也同時寫入相應(yīng)的數(shù)值,這就是內(nèi)存映射。這里就是field1被賦值的地方。
問題二:為什么會導(dǎo)致field1空值?
上面說的是,在某個地址也就是上面說的Addr1這個地方下了一個斷點(diǎn),跟蹤發(fā)現(xiàn),如果不在Addr1處下斷點(diǎn),那么field1不等于0,也就不會報異常,如果在Addr1處下斷點(diǎn),那么field1等于0,導(dǎo)致了異常的發(fā)生。
這個BUG很詭異,難道是斷點(diǎn)造成的?
繼續(xù)跟蹤發(fā)現(xiàn),如果在離Addr1偏移量很遠(yuǎn)的地址下斷點(diǎn),則不會導(dǎo)致了field1==0,如果在Addr1地址上下偏移的地方下斷點(diǎn)(也就是偏移比較近的位置),則會導(dǎo)致field1等于0。難道Addr1地址的上下偏移范圍跟field1有一定的關(guān)聯(lián)?
繼續(xù)跟蹤發(fā)現(xiàn),field的值在Addr1地址的后面,它的值本身也是一個地址。每塊內(nèi)存都有一個起始地址,姑且叫Base。那么filed,Addr1,Base的組成如下圖所示:
圖片
可以看到Addr1和field1的起始地址都是Base,而Base則是被MapViewOfFileEx Windows API內(nèi)存映射的起始地址。Addr1則是被映射的這塊內(nèi)存里面的某個函數(shù)中的某個地址。這里假如說它是程序入口Main函數(shù)的函數(shù)頭地址,也可以是Main函數(shù)中間的某個地址。如下圖:
圖片
因為實際上在Addr1處下了斷點(diǎn),也即是在被MapViewOfFileEx映射的內(nèi)存地址里面下了斷點(diǎn)。在內(nèi)存映射里面下了斷點(diǎn),就會導(dǎo)致了通過MapViewOfFile映射的內(nèi)存pRW賦值的時候,pRX會被賦值不上的情況。
pRX和pRW如下圖所示:
圖片
如果把這個斷點(diǎn),下在MapViewOfFileEx映射的內(nèi)存范圍之外,則不會存在賦值不上的情況。
這里可以確定的就是,在內(nèi)存映射的范圍內(nèi)下斷點(diǎn),斷點(diǎn)會干擾內(nèi)存映射范圍內(nèi)的數(shù)值。
2.檢測上面結(jié)論是否正確
上面只是問題的分析,如果想要檢驗上面所述BUG問題是否正確。則需要代碼加以輔助證明。
下面是一段內(nèi)存映射的代碼:
#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));
}
以上例子,進(jìn)行了一個內(nèi)存模擬映射。通過以上例子,觀察發(fā)現(xiàn)。當(dāng)在pRX所在地址范圍內(nèi)下斷點(diǎn),則會導(dǎo)致當(dāng)往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個字節(jié)處下斷點(diǎn),也即00007FF739180002。
然后在pRW地址處進(jìn)行賦值,如下pRW處內(nèi)存展示:
Address:0x000001BEE1610000 //pRW Memory
0010000000000000 0010000000000000 0000000000000000 0000000000000000
0000000000000000 0000000000000000
這里的pRW地址是0x000001BEE1610000。
往它的第一個八字節(jié)賦值了:0010000000000000。然后看下pRX的的內(nèi)存,如下:
Addres:0x00007FF739180000 //pRX Memory
0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000
可以看到在被MapViewOfFileEx映射的內(nèi)存范圍內(nèi)下斷點(diǎn)之后,pRW的賦值并不能更改pRX的值。這就導(dǎo)致了開頭的異常BUG。
3.代碼還原
通過以上理論分析和代碼分析,基本上確定了,這個BUG就是斷點(diǎn)+內(nèi)存映射造成的。如果把斷點(diǎn)下在內(nèi)存映射的范圍內(nèi)的某個一個地址上,則會導(dǎo)致內(nèi)存賦值的失敗。如果不下斷點(diǎn),或者斷點(diǎn)不在內(nèi)存映射范圍內(nèi),則不存在這種情況。這應(yīng)該是微軟Windows內(nèi)核的一個BUG。以上就是全部用戶態(tài)的BUG展示了,如果想要更深一些,則需要追蹤Windows內(nèi)核,這個有時間再研究。
這個BUG起因于,CLR調(diào)用C#入口Main的匯編代碼里面下的斷點(diǎn),運(yùn)行到.Ctor然后報了異常。這個異常的排查過程如上所示,但是依然有疑惑。就是為啥通過VS調(diào)試C#源代碼則不會報這個異常。難道VS直接運(yùn)行C#源代碼跟CLR調(diào)用略有不同?