以前學重載內核時學到了手動加載一個PE文件,想在Ring3層也實現一遍,不過在GitHub上看到有現成的源碼了就不自己寫了。本篇文章就分析一下這個mmLoader,看看怎麼實現手動加載PE文件
閱讀前需要了解PE文件結構,還不瞭解的自行惡補
這個庫爲了實現注入ShellCode,自己實現了memset等常用庫函數,還自己定義了要用到的庫函數表。實際使用時,如果不用注入ShellCode使用常規的庫函數就好
雖然這個庫是用來加載DLL的,修改一下也可以用來加載EXE
加載PE文件的主要步驟:
- 將PE頭和各個節映射到內存
- 重定位
- 修復IAT
- 調用模塊入口點
映射文件到內存
這一步就是把PE文件的內容讀到內存相應的位置,要注意文件中各節的對齊和內存中的對齊是不一樣的
/// <summary>
/// Maps all the sections.
/// </summary>
/// <param name="pMemModule">The <see cref="MemModule" /> instance.</param>
/// <returns>True if successful.</returns>
BOOL MapMemModuleSections(PMEM_MODULE pMemModule, LPVOID lpPeModuleBuffer)
{
// Validate
if (NULL == pMemModule || NULL == pMemModule->pNtFuncptrsTable || NULL == lpPeModuleBuffer)
return FALSE;
// Function pointer
// VirtualAlloc的函數指針,ShellCode用的,實際使用時直接寫VirtualAlloc即可,後面類似的不再贅述
Type_VirtualAlloc pfnVirtualAlloc = (Type_VirtualAlloc)(pMemModule->pNtFuncptrsTable->pfnVirtualAlloc);
Type_VirtualFree pfnVirtualFree = (Type_VirtualFree)(pMemModule->pNtFuncptrsTable->pfnVirtualFree);
// Convert to IMAGE_DOS_HEADER
// PE文件頭部指針,即DOS頭
PIMAGE_DOS_HEADER pImageDosHeader = (PIMAGE_DOS_HEADER)(lpPeModuleBuffer);
// Get the pointer to IMAGE_NT_HEADERS
// NT頭,MakePointer是個宏,返回某指針+某偏移量後的新指針
PIMAGE_NT_HEADERS pImageNtHeader = MakePointer(
PIMAGE_NT_HEADERS, pImageDosHeader, pImageDosHeader->e_lfanew);
// Get the section count
int nNumberOfSections = pImageNtHeader->FileHeader.NumberOfSections;
// Get the section header
// 節頭
PIMAGE_SECTION_HEADER pImageSectionHeader = MakePointer(
PIMAGE_SECTION_HEADER, pImageNtHeader, sizeof(IMAGE_NT_HEADERS));
// Find the last section limit
// 計算整個模塊尺寸
DWORD dwImageSizeLimit = 0;
for (int i = 0; i < nNumberOfSections; ++i)
{
if (0 != pImageSectionHeader[i].VirtualAddress)
{
if (dwImageSizeLimit < (pImageSectionHeader[i].VirtualAddress + pImageSectionHeader[i].SizeOfRawData))
dwImageSizeLimit = pImageSectionHeader[i].VirtualAddress + pImageSectionHeader[i].SizeOfRawData;
}
}
// Align the last image size limit to the page size
// 按頁對齊
dwImageSizeLimit = (dwImageSizeLimit + pMemModule->dwPageSize - 1) & ~(pMemModule->dwPageSize - 1);
// Reserve virtual memory
// 優先使用ImageBase作爲模塊基址,分配一塊內存
LPVOID lpBase = pfnVirtualAlloc(
(LPVOID)(pImageNtHeader->OptionalHeader.ImageBase),
dwImageSizeLimit,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE);
// Failed to reserve space at ImageBase, then it's up to the system
// 這個基址不能使用,讓系統隨機選另一個基址(後面需要重定位)
if (NULL == lpBase)
{
// Reserver memory in arbitrary address
lpBase = pfnVirtualAlloc(
NULL,
dwImageSizeLimit,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE);
// Failed again, return
if (NULL == lpBase)
{
pMemModule->dwErrorCode = MMEC_ALLOCATED_MEMORY_FAILED;
return FALSE;
}
}
// Commit memory for PE header
LPVOID pDest = pfnVirtualAlloc(lpBase, pImageNtHeader->OptionalHeader.SizeOfHeaders, MEM_COMMIT, PAGE_READWRITE);
if (!pDest)
{
pMemModule->dwErrorCode = MMEC_ALLOCATED_MEMORY_FAILED;
return FALSE;
}
// Copy the data of PE header to the memory allocated
// 複製PE頭
mml_memmove(pDest, lpPeModuleBuffer, pImageNtHeader->OptionalHeader.SizeOfHeaders);
// Store the base address of this module.
pMemModule->lpBase = pDest;
pMemModule->dwSizeOfImage = pImageNtHeader->OptionalHeader.SizeOfImage;
pMemModule->bLoadOk = TRUE;
// Get the DOS header, NT header and Section header from the new PE header buffer
pImageDosHeader = (PIMAGE_DOS_HEADER)pDest;
pImageNtHeader = MakePointer(PIMAGE_NT_HEADERS, pImageDosHeader, pImageDosHeader->e_lfanew);
pImageSectionHeader = MakePointer(PIMAGE_SECTION_HEADER, pImageNtHeader, sizeof(IMAGE_NT_HEADERS));
// Map all section data into the memory
// 複製所有的節
LPVOID pSectionBase = NULL;
LPVOID pSectionDataSource = NULL;
for (int i = 0; i < nNumberOfSections; ++i)
{
if (0 != pImageSectionHeader[i].VirtualAddress)
{
// Get the section base
pSectionBase = MakePointer(LPVOID, lpBase, pImageSectionHeader[i].VirtualAddress);
if (0 == pImageSectionHeader[i].SizeOfRawData)
{
if (pImageNtHeader->OptionalHeader.SectionAlignment > 0)
{
// If the size is zero, but the section alignment is not zero then allocate memory with the aligment
pDest = pfnVirtualAlloc(pSectionBase, pImageNtHeader->OptionalHeader.SectionAlignment,
MEM_COMMIT, PAGE_READWRITE);
if (NULL == pDest)
{
pMemModule->dwErrorCode = MMEC_ALLOCATED_MEMORY_FAILED;
return FALSE;
}
// Always use position from file to support alignments smaller than page size.
mml_memset(pSectionBase, 0, pImageNtHeader->OptionalHeader.SectionAlignment);
}
}
else
{
// Commit this section to target address
pDest = pfnVirtualAlloc(pSectionBase, pImageSectionHeader[i].SizeOfRawData, MEM_COMMIT, PAGE_READWRITE);
if (NULL == pDest)
{
pMemModule->dwErrorCode = MMEC_ALLOCATED_MEMORY_FAILED;
return FALSE;
}
// Get the section data source and copy the data to the section buffer
pSectionDataSource = MakePointer(LPVOID, lpPeModuleBuffer, pImageSectionHeader[i].PointerToRawData);
mml_memmove(pDest, pSectionDataSource, pImageSectionHeader[i].SizeOfRawData);
}
// Get next section header
pImageSectionHeader[i].Misc.PhysicalAddress = (DWORD)(ULONGLONG)pDest;
}
}
return TRUE;
}
重定位
代碼中那些絕對地址是以模塊基址=ImageBase爲前提硬編碼的,如果模塊加載的基址不是ImageBase指定的基址,則需要重定位
新地址=舊地址-ImageBase+實際模塊基址
,只要算出實際模塊基址-ImageBase
,重定位時加上偏移量就行了
/// <summary>
/// Relocates the module.
/// </summary>
/// <param name="pMemModule">The <see cref="MemModule" /> instance.</param>
/// <returns>True if successful.</returns>
BOOL RelocateModuleBase(PMEM_MODULE pMemModule)
{
// Validate the parameters
if (NULL == pMemModule || NULL == pMemModule->pImageDosHeader)
return FALSE;
PIMAGE_NT_HEADERS pImageNtHeader = MakePointer(
PIMAGE_NT_HEADERS,
pMemModule->pImageDosHeader,
pMemModule->pImageDosHeader->e_lfanew);
// Get the delta of the real image base with the predefined
// 計算偏移量
LONGLONG lBaseDelta = ((PUINT8)pMemModule->iBase - (PUINT8)pImageNtHeader->OptionalHeader.ImageBase);
// This module has been loaded to the ImageBase, no need to do relocation
if (0 == lBaseDelta) return TRUE;
if (0 == pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress
|| 0 == pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size)
return TRUE;
// 重定位表
PIMAGE_BASE_RELOCATION pImageBaseRelocation = MakePointer(PIMAGE_BASE_RELOCATION, pMemModule->lpBase,
pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
if (NULL == pImageBaseRelocation)
{
pMemModule->dwErrorCode = MMEC_INVALID_RELOCATION_BASE;
return FALSE;
}
while (0 != (pImageBaseRelocation->VirtualAddress + pImageBaseRelocation->SizeOfBlock))
{
PWORD pRelocationData = MakePointer(PWORD, pImageBaseRelocation, sizeof(IMAGE_BASE_RELOCATION));
int NumberOfRelocationData = (pImageBaseRelocation->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
for (int i = 0; i < NumberOfRelocationData; i++)
{
if (IMAGE_REL_BASED_HIGHLOW == (pRelocationData[i] >> 12))
{
// 需要重定位的地址
PDWORD pAddress = (PDWORD)(pMemModule->iBase + pImageBaseRelocation->VirtualAddress + (pRelocationData[i] & 0x0FFF));
// 重定位
*pAddress += (DWORD)lBaseDelta;
}
#ifdef _WIN64
if (IMAGE_REL_BASED_DIR64 == (pRelocationData[i] >> 12))
{
PULONGLONG pAddress = (PULONGLONG)(pMemModule->iBase + pImageBaseRelocation->VirtualAddress + (pRelocationData[i] & 0x0FFF));
*pAddress += lBaseDelta;
}
#endif
}
pImageBaseRelocation = MakePointer(PIMAGE_BASE_RELOCATION, pImageBaseRelocation, pImageBaseRelocation->SizeOfBlock);
}
return TRUE;
}
修復IAT
這一步載入本模塊依賴的模塊,並將本模塊的IAT定位到依賴模塊的相應的函數上
/// <summary>
/// Resolves the import table.
/// </summary>
/// <param name="pMemModule">The <see cref="MemModule" /> instance.</param>
/// <returns>True if successful.</returns>
BOOL ResolveImportTable(PMEM_MODULE pMemModule)
{
if (NULL == pMemModule || NULL == pMemModule->pNtFuncptrsTable || NULL == pMemModule->pImageDosHeader)
return FALSE;
Type_GetModuleHandleA pfnGetModuleHandleA = (Type_GetModuleHandleA)(pMemModule->pNtFuncptrsTable->pfnGetModuleHandleA);
Type_LoadLibraryA pfnLoadLibraryA = (Type_LoadLibraryA)(pMemModule->pNtFuncptrsTable->pfnLoadLibraryA);
Type_GetProcAddress pfnGetProcAddress = (Type_GetProcAddress)(pMemModule->pNtFuncptrsTable->pfnGetProcAddress);
PIMAGE_NT_HEADERS pImageNtHeader = MakePointer(PIMAGE_NT_HEADERS, pMemModule->pImageDosHeader, pMemModule->pImageDosHeader->e_lfanew);
if (pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress == 0
|| pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size == 0)
return TRUE;
PIMAGE_IMPORT_DESCRIPTOR pImageImportDescriptor = MakePointer(PIMAGE_IMPORT_DESCRIPTOR, pMemModule->lpBase,
pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
// 遍歷導入的模塊
for (; pImageImportDescriptor->Name; pImageImportDescriptor++)
{
// Get the dependent module name
// 依賴的模塊名
PCHAR pDllName = MakePointer(PCHAR, pMemModule->lpBase, pImageImportDescriptor->Name);
// Get the dependent module handle
// 取依賴的模塊句柄,其實直接用LoadLibrary就行了,這裏兩個函數都用了而且釋放模塊時也沒有FreeLibrary,會造成內存泄漏
HMODULE hMod = pfnGetModuleHandleA(pDllName);
// Load the dependent module
if (NULL == hMod) hMod = pfnLoadLibraryA(pDllName);
// Failed
if (NULL == hMod)
{
pMemModule->dwErrorCode = MMEC_IMPORT_MODULE_FAILED;
return FALSE;
}
// Original thunk
PIMAGE_THUNK_DATA pOriginalThunk = NULL;
if (pImageImportDescriptor->OriginalFirstThunk)
pOriginalThunk = MakePointer(PIMAGE_THUNK_DATA, pMemModule->lpBase, pImageImportDescriptor->OriginalFirstThunk);
else
pOriginalThunk = MakePointer(PIMAGE_THUNK_DATA, pMemModule->lpBase, pImageImportDescriptor->FirstThunk);
// IAT thunk
PIMAGE_THUNK_DATA pIATThunk = MakePointer(PIMAGE_THUNK_DATA, pMemModule->lpBase,
pImageImportDescriptor->FirstThunk);
// 遍歷導入的函數
for (; pOriginalThunk->u1.AddressOfData; pOriginalThunk++, pIATThunk++)
{
// 取函數地址
FARPROC lpFunction = NULL;
if (IMAGE_SNAP_BY_ORDINAL(pOriginalThunk->u1.Ordinal))
{
lpFunction = pfnGetProcAddress(hMod, (LPCSTR)IMAGE_ORDINAL(pOriginalThunk->u1.Ordinal));
}
else
{
PIMAGE_IMPORT_BY_NAME pImageImportByName = MakePointer(
PIMAGE_IMPORT_BY_NAME, pMemModule->lpBase, pOriginalThunk->u1.AddressOfData);
lpFunction = pfnGetProcAddress(hMod, (LPCSTR)&(pImageImportByName->Name));
}
// Write into IAT
// 寫到IAT
#ifdef _WIN64
pIATThunk->u1.Function = (ULONGLONG)lpFunction;
#else
pIATThunk->u1.Function = (DWORD)lpFunction;
#endif
}
}
return TRUE;
}
設置各節內存保護
沒有這一步也無所謂,爲了安全和嚴謹就加上這一步吧
/// <summary>
/// Sets the memory protected stats of all the sections.
/// </summary>
/// <param name="pMemModule">The <see cref="MemModule" /> instance.</param>
/// <returns>True if successful.</returns>
BOOL SetMemProtectStatus(PMEM_MODULE pMemModule)
{
if (NULL == pMemModule || NULL == pMemModule->pNtFuncptrsTable)
return FALSE;
// 索引含義:可執行、可讀、可寫,根據這3個屬性決定內存保護
int ProtectionMatrix[2][2][2] = {
{
// not executable
{ PAGE_NOACCESS, PAGE_WRITECOPY },
{ PAGE_READONLY, PAGE_READWRITE },
},
{
// executable
{ PAGE_EXECUTE, PAGE_EXECUTE_WRITECOPY },
{ PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE },
},
};
Type_VirtualProtect pfnVirtualProtect = (Type_VirtualProtect)(pMemModule->pNtFuncptrsTable->pfnVirtualProtect);
Type_VirtualFree pfnVirtualFree = (Type_VirtualFree)(pMemModule->pNtFuncptrsTable->pfnVirtualFree);
PIMAGE_DOS_HEADER pImageDosHeader = (PIMAGE_DOS_HEADER)(pMemModule->lpBase);
ULONGLONG ulBaseHigh = 0;
#ifdef _WIN64
ulBaseHigh = (pMemModule->iBase & 0xffffffff00000000);
#endif
PIMAGE_NT_HEADERS pImageNtHeader = MakePointer(
PIMAGE_NT_HEADERS, pImageDosHeader, pImageDosHeader->e_lfanew);
int nNumberOfSections = pImageNtHeader->FileHeader.NumberOfSections;
PIMAGE_SECTION_HEADER pImageSectionHeader = MakePointer(
PIMAGE_SECTION_HEADER, pImageNtHeader, sizeof(IMAGE_NT_HEADERS));
// 遍歷各節
for (int idxSection = 0; idxSection < nNumberOfSections; idxSection++)
{
DWORD protectFlag = 0;
DWORD oldProtect = 0;
BOOL isExecutable = FALSE;
BOOL isReadable = FALSE;
BOOL isWritable = FALSE;
BOOL isNotCache = FALSE;
ULONGLONG dwSectionBase = (pImageSectionHeader[idxSection].Misc.PhysicalAddress | ulBaseHigh);
DWORD dwSecionSize = pImageSectionHeader[idxSection].SizeOfRawData;
if (0 == dwSecionSize) continue;
// This section is in this page
// 接下來根據節屬性設置頁面內存保護
DWORD dwSectionCharacteristics = pImageSectionHeader[idxSection].Characteristics;
// Discardable
if (dwSectionCharacteristics & IMAGE_SCN_MEM_DISCARDABLE)
{
pfnVirtualFree((LPVOID)dwSectionBase, dwSecionSize, MEM_DECOMMIT);
continue;
}
// Executable
if (dwSectionCharacteristics & IMAGE_SCN_MEM_EXECUTE)
isExecutable = TRUE;
// Readable
if (dwSectionCharacteristics & IMAGE_SCN_MEM_READ)
isReadable = TRUE;
// Writable
if (dwSectionCharacteristics & IMAGE_SCN_MEM_WRITE)
isWritable = TRUE;
if (dwSectionCharacteristics & IMAGE_SCN_MEM_NOT_CACHED)
isNotCache = TRUE;
protectFlag = ProtectionMatrix[isExecutable][isReadable][isWritable];
if (isNotCache) protectFlag |= PAGE_NOCACHE;
// 設置內存保護
if (!pfnVirtualProtect((LPVOID)dwSectionBase, dwSecionSize, protectFlag, &oldProtect))
{
pMemModule->dwErrorCode = MMEC_PROTECT_SECTION_FAILED;
return FALSE;
}
}
return TRUE;
}
調用TLS回調和模塊入口點
這一步也是可選的,不過如果不做有些全局變量可能未初始化。做了的話有可能失敗,因爲操作系統不認我們手動加載的模塊,調用某些函數有可能失敗…
/// <summary>
/// Executes the TLS callback function.
/// </summary>
/// <param name="pMemModule">The <see cref="MemModule" /> instance.</param>
/// <returns>True if successful.</returns>
BOOL ExecuteTLSCallback(PMEM_MODULE pMemModule)
{
if (NULL == pMemModule || NULL == pMemModule->pImageDosHeader)
return FALSE;
PIMAGE_NT_HEADERS pImageNtHeader = MakePointer(
PIMAGE_NT_HEADERS,
pMemModule->pImageDosHeader,
pMemModule->pImageDosHeader->e_lfanew);
IMAGE_DATA_DIRECTORY imageDirectoryEntryTls = pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS];
if (imageDirectoryEntryTls.VirtualAddress == 0) return TRUE;
PIMAGE_TLS_DIRECTORY tls = (PIMAGE_TLS_DIRECTORY)(pMemModule->iBase + imageDirectoryEntryTls.VirtualAddress);
PIMAGE_TLS_CALLBACK* callback = (PIMAGE_TLS_CALLBACK *)tls->AddressOfCallBacks;
if (callback)
{
while (*callback)
{
(*callback)((LPVOID)pMemModule->hModule, DLL_PROCESS_ATTACH, NULL);
callback++;
}
}
return TRUE;
}
/// <summary>
/// Calls the module entry.
/// </summary>
/// <param name="pMemModule">The <see cref="MemModule" /> instance.</param>
/// <param name="dwReason">The reason of the calling.</param>
/// <returns>True if successful.</returns>
BOOL CallModuleEntry(PMEM_MODULE pMemModule, DWORD dwReason)
{
if (NULL == pMemModule || NULL == pMemModule->pImageDosHeader)
return FALSE;
PIMAGE_NT_HEADERS pImageNtHeader = MakePointer(
PIMAGE_NT_HEADERS,
pMemModule->pImageDosHeader,
pMemModule->pImageDosHeader->e_lfanew);
Type_DllMain pfnModuleEntry = NULL;
pfnModuleEntry = MakePointer(
Type_DllMain,
pMemModule->lpBase,
pImageNtHeader->OptionalHeader.AddressOfEntryPoint);
if (NULL == pfnModuleEntry)
{
pMemModule->dwErrorCode = MMEC_INVALID_ENTRY_POINT;
return FALSE;
}
return pfnModuleEntry(pMemModule->hModule, dwReason, NULL);
}
總流程
這個函數就是把之前分析過的函數調用一遍實現加載PE文件。有些不重要而且看名字就知道幹什麼的函數就不分析了
BOOL __stdcall LoadMemModule(PMEM_MODULE pMemModule, LPVOID lpPeModuleBuffer, BOOL bCallEntry)
{
if (NULL == pMemModule || NULL == pMemModule->pNtFuncptrsTable || NULL == lpPeModuleBuffer)
return FALSE;
pMemModule->dwErrorCode = ERROR_SUCCESS;
// Verify file format
if (FALSE == IsValidPEFormat(pMemModule, lpPeModuleBuffer))
{
return FALSE;
}
// Map PE header and section table into memory
if (FALSE == MapMemModuleSections(pMemModule, lpPeModuleBuffer))
return FALSE;
// Relocate the module base
if (FALSE == RelocateModuleBase(pMemModule))
{
UnmapMemModule(pMemModule);
return FALSE;
}
// Resolve the import table
if (FALSE == ResolveImportTable(pMemModule))
{
UnmapMemModule(pMemModule);
return FALSE;
}
pMemModule->dwCrc = mml_getcrc32(
0, pMemModule->lpBase, pMemModule->dwSizeOfImage);
// Correct the protect flag for all section pages
if (FALSE == SetMemProtectStatus(pMemModule))
{
UnmapMemModule(pMemModule);
return FALSE;
}
if (FALSE == ExecuteTLSCallback(pMemModule))
return FALSE;
if (bCallEntry)
{
if (FALSE == CallModuleEntry(pMemModule, DLL_PROCESS_ATTACH))
{
// failed to call entry point,
// clean resource, return false
UnmapMemModule(pMemModule);
return FALSE;
}
}
return TRUE;
}
應用
- 重載某模塊,繞過各種hook
重載模塊的話會有其他問題,比如全局變量在內存中有兩份副本。其實也很好解決,重定位時定位到原模塊就行了。不過用來繞hook未免小題大做了,針對性地用匯編繞過部分hook就行了 - 隱藏注入模塊
你手動加載的模塊,操作系統根本不知道它的存在,模塊列表裏沒有,也不會引起任何回調 - 獲取模塊的原始內容,比如內核中的SSDT