天天看點

絕頂技術:斷點+記憶體映射組合的CLR超強BUG?

作者:opendotnet

前言

你見過斷點+記憶體映射,制造了一個另類隐藏極深,強悍的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的組成如下圖所示:

絕頂技術:斷點+記憶體映射組合的CLR超強BUG?

可以看到Addr1和field1的起始位址都是Base,而Base則是被MapViewOfFileEx Windows API記憶體映射的起始位址。Addr1則是被映射的這塊記憶體裡面的某個函數中的某個位址。這裡假如說它是程式入口Main函數的函數頭位址,也可以是Main函數中間的某個位址。如下圖:

絕頂技術:斷點+記憶體映射組合的CLR超強BUG?

因為實際上在Addr1處下了斷點,也即是在被MapViewOfFileEx映射的記憶體位址裡面下了斷點。在記憶體映射裡面下了斷點,就會導緻了通過MapViewOfFile映射的記憶體pRW指派的時候,pRX會被指派不上的情況。

pRX和pRW如下圖所示:

絕頂技術:斷點+記憶體映射組合的CLR超強BUG?

如果把這個斷點,下在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              , // 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)              );                  SIZE_T sizeOfLargePage = GetLargePageMinimum();              int nCount = 10;              PVOID pAddr = VirtualAlloc(, 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調用略有不同?

結尾

作者:江湖評談

繼續閱讀