絕頂技術:斷點+內存映射組合的CLR超強Bug?
前言
你見過斷點+內存映射,制造了一個另類隱藏極深,強悍的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調用略有不同?