Windows注入與攔截(6) -- 從記憶體中載入DLL
Windows提供的API(LoadLibrary
, LoadLibraryEx
)只支援從檔案系統上載入DLL檔案,我們無法使用這些API從記憶體中載入DLL。
但是有些時候,我們的確需要從記憶體中載入DLL,比如:
- 對釋出的檔案數量有限制。我們可以將DLL打包到exe的資源中,程式執行時從呼叫
LoadResource
等API讀取DLL檔案到記憶體中,然後從記憶體中載入DLL。 - 需要對DLL進行壓縮或加密等。解壓和解密之後的內容首先都是存放在記憶體之中的,我們從記憶體中載入DLL會更加便捷。
本文主要介紹如何實現從記憶體中載入DLL,並呼叫DLL提供介面函式(必須是純C介面)。
雖然“從記憶體中載入DLL”和“Windows的注入與攔截”之間沒有直接關係,但還是選擇放在《Windows注入與攔截》系列文章之中,主要是為了後面介紹的“無痕注入”(也叫反射注入)作鋪墊。
一. PE格式
從記憶體中載入DLL就是解析PE格式並將DLL內容按照該格式要求存放到程序的虛擬地址空間的過程。所以對PE格式的瞭解對理解整個載入過程比較重要。建議對照《PE檔案格式》中的PE格式圖來閱讀本文內容和程式碼。
PE檔案大致由下面幾部分組成,本文不會詳細的介紹PE格式的每一個細節,只會針對“從記憶體中載入DLL”所需要掌握的PE知識來進行介紹。若需要詳細瞭解PE格式,可以參考:《Windows PE權威指南》
+----------------+
| DOS header |
| |
| DOS stub |
+----------------+
| PE header |
+----------------+
| Section header |
+----------------+
| Section 1 |
+----------------+
| Section 2 |
+----------------+
| . . . |
+----------------+
| Section n |
+----------------+
1.1 DOS header、stub
DOS頭的存在主要是為了向後相容,它位於dos stub的前面,通常用於顯示一個“該程式不能允許在DOS模式”的錯誤提示。
我們用16進位制工具開啟任意一個exe檔案就可以看到如下圖的字串常量:
DOS頭的結構體定義如下:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
我們只需要關注e_lfanew
欄位,它表示PE頭的偏移位置,我們用這個欄位來定位PE頭的起始地址。
1.2 PE header
PE頭的結構體定義如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Signature
欄位為IMAGE_NT_SIGNATURE
常量,可以用來檢查PE內容是否合法。
FileHeader
欄位包含了可執行檔案的物理格式或屬性,如符號資訊,所需CPU,檔案資訊標誌(dll還是exe),檔案建立時間等,結構體定義如下:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
OptionalHeader
欄位包含一些邏輯上的資訊,如作業系統版本、入口點、基地址、映像大小等,結構體定義如下:
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
OptionalHeader
最後的DataDirectory
包含了16(IMAGE_NUMBEROF_DIRECTORY_ENTRIES
)個IMAGE_DATA_DIRECTORY
邏輯元件,每個元件的功能分別如下:
===== ==========================
Index Description
===== ==========================
0 Exported functions
----- --------------------------
1 Imported functions
----- --------------------------
2 Resources
----- --------------------------
3 Exception informations
----- --------------------------
4 Security informations
----- --------------------------
5 Base relocation table
----- --------------------------
6 Debug informations
----- --------------------------
7 Architecture specific data
----- --------------------------
8 Global pointer
----- --------------------------
9 Thread local storage
----- --------------------------
10 Load configuration
----- --------------------------
11 Bound imports
----- --------------------------
12 Import address table
----- --------------------------
13 Delay load imports
----- --------------------------
14 COM runtime descriptor
===== ==========================
對於從記憶體中載入DLL,我們只需要關注Index為0,1,5的元件。
1.3 Section header
Section
頭儲存在OptionalHeader
的後面,Section
頭包含n個IMAGE_SECTION_HEADER
結構體,具體的個數可以通過PEHeader.FileHeader.NumberOfSections
欄位得到。
微軟提供了IMAGE_FIRST_SECTION
巨集來獲取第一個IMAGE_SECTION_HEADER
結構體的地址,這樣我們就可以遍歷到所有Section.
IMAGE_SECTION_HEADER
結構體定義如下:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
二. DLL檔案的載入步驟
我們要模擬PE載入器從記憶體中載入DLL,我們首先要知道Windows載入DLL檔案的步驟,以及需要準備那些結構體等。
當我們呼叫LoadLibrary
時,windows主要執行了下面的一些步驟:
- 檢測DOS和PE頭的合法性。
- 嘗試在
PEHeader.OptionalHeader.ImageBase
位置分配PEHeader.OptionalHeader.SizeOfImage
位元組的記憶體區域。 - 解析
Section header
中的每個Section
,並將它們的實際內容拷貝到第2步分配的地址空間中。拷貝的目的地址的計算方法為:IMAGE_SECTION_HEADER.VirtualAddress偏移 + 第二步分配的記憶體區域的起始地址
。 - 檢查載入到程序地址空間的位置和之前PE檔案中指定的基地址是否一致,如果不一致,則需要重定位。重定位就需要用到1.2節中的
IMAGE_OPTIONAL_HEADER64.DataDirectory[5]
. - 載入該DLL依賴的其他dll,並構建
"PEHeader.OptionalHeader.DataDirectory.Image_directory_entry_import"
匯入表. - 根據每個Section的
"PEHeader.Image_Section_Table.Characteristics"
屬性來設定記憶體頁的訪問屬性; 如果被設定為”discardable”屬性,則釋放該記憶體頁。 - 獲取DLL的入口函式指標,並使用
DLL_PROCESS_ATTACH
引數呼叫。
三. 程式碼實現
本程式碼參考了fancycode/MemoryModule,修復原有程式碼的若干BUG,擴充了部分功能,並針對第二節介紹的步驟添加了詳細的註釋。
3.1 介面定義
#ifndef __MEMORY_MODULE_HEADER
#define __MEMORY_MODULE_HEADER
#include <Windows.h>
typedef void *HMEMORYMODULE;
#ifdef __cplusplus
extern "C" {
#endif
HMEMORYMODULE MemoryLoadLibrary(const void *);
FARPROC MemoryGetProcAddress(HMEMORYMODULE, const char *);
void MemoryFreeLibrary(HMEMORYMODULE);
#ifdef __cplusplus
}
#endif
#endif // __MEMORY_MODULE_HEADER
HMEMORYMODULE
是一個自定義結構體,該結構體分配在程序的預設堆上面,呼叫者需要儲存該結構體指標,在後面獲取介面地址和釋放DLL時需要傳入該指標。
typedef struct {
PIMAGE_NT_HEADERS headers;
unsigned char *codeBase;
HMODULE *modules;
int numModules;
int initialized;
} MEMORYMODULE, *PMEMORYMODULE;
3.2 MemoryLoadLibrary
函式
HMEMORYMODULE MemoryLoadLibrary(const void *data)
{
PMEMORYMODULE result;
PIMAGE_DOS_HEADER dos_header; // DOS頭
PIMAGE_NT_HEADERS old_header; // PE頭
unsigned char *code, *headers;
SIZE_T locationDelta;
DllEntryProc DllEntry;
BOOL successfull;
// 獲取DOS頭指標,並檢查DOS頭
dos_header = (PIMAGE_DOS_HEADER)data;
if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) {
#if DEBUG_OUTPUT
OutputDebugStringA("Not a valid executable file.\n");
#endif
return NULL;
}
// 獲取PE頭指標,並檢查PE頭
old_header = (PIMAGE_NT_HEADERS)&((const unsigned char *)(data))[dos_header->e_lfanew];
if (old_header->Signature != IMAGE_NT_SIGNATURE) {
#if DEBUG_OUTPUT
OutputDebugStringA("No PE header found.\n");
#endif
return NULL;
}
// 在"PEHeader.OptionalHeader.ImageBase"處預定"PEHeader.OptionalHeader.SizeOfImage"位元組的空間
code = (unsigned char *)VirtualAlloc((LPVOID)(old_header->OptionalHeader.ImageBase),
old_header->OptionalHeader.SizeOfImage,
MEM_RESERVE,
PAGE_READWRITE);
if (code == NULL) {
// try to allocate memory at arbitrary position
code = (unsigned char *)VirtualAlloc(NULL,
old_header->OptionalHeader.SizeOfImage,
MEM_RESERVE,
PAGE_READWRITE);
if (code == NULL) {
#if DEBUG_OUTPUT
OutputLastError("Can't reserve memory");
#endif
return NULL;
}
}
// 在程序的預設堆上分配"sizeof(MEMORYMODULE)"位元組的空間用於存放MEMORYMODULE結構體
// 方便函式末尾將該結構體指標當作返回值返回
result = (PMEMORYMODULE)HeapAlloc(GetProcessHeap(), 0, sizeof(MEMORYMODULE));
result->codeBase = code;
result->numModules = 0;
result->modules = NULL;
result->initialized = 0;
// 一次性從code地址處將整個映像所需的記憶體區域都分配
VirtualAlloc(code,
old_header->OptionalHeader.SizeOfImage,
MEM_COMMIT,
PAGE_READWRITE);
// 原作者的程式碼中此處會再次呼叫VirtualAlloc從code處分配SizeOfHeaders大小的記憶體,
// 但這步操作屬於多餘的,因為上一步已經在code處分配了所需的整個記憶體區域了,
// 所以直接將此處更改為 headers = code;
//
//headers = (unsigned char *)VirtualAllocEx(process, code,
// old_header->OptionalHeader.SizeOfHeaders,
// MEM_COMMIT,
// PAGE_READWRITE);
headers = code;
// 拷貝DOS頭 + DOS STUB + PE頭到headers地址處
memcpy(headers, dos_header, dos_header->e_lfanew + old_header->OptionalHeader.SizeOfHeaders);
result->headers = (PIMAGE_NT_HEADERS)&((const unsigned char *)(headers))[dos_header->e_lfanew];
// 更新"MEMORYMODULE.PIMAGE_NT_HEADERS"結構體中的基地址
result->headers->OptionalHeader.ImageBase = (POINTER_TYPE)code;
// 從dll檔案內容中拷貝每個section(節)的資料到新的記憶體區域
CopySections(data, old_header, result);
// 檢查載入到程序地址空間的位置和之前PE檔案中指定的基地址是否一致,如果不一致,則需要重定位
locationDelta = (SIZE_T)(code - old_header->OptionalHeader.ImageBase);
if (locationDelta != 0) {
PerformBaseRelocation(result, locationDelta);
}
// 載入依賴dll,並構建"PEHeader.OptionalHeader.DataDirectory.Image_directory_entry_import"匯入表
if (!BuildImportTable(result)) {
goto error;
}
// 根據每個Section的"PEHeader.Image_Section_Table.Characteristics"屬性來設定記憶體頁的訪問屬性;
// 如果被設定為"discardable"屬性,則釋放該記憶體頁
FinalizeSections(result);
// 獲取DLL的入口函式指標,並呼叫
if (result->headers->OptionalHeader.AddressOfEntryPoint != 0) {
DllEntry = (DllEntryProc) (code + result->headers->OptionalHeader.AddressOfEntryPoint);
if (DllEntry == 0) {
#if DEBUG_OUTPUT
OutputDebugStringA("Library has no entry point.\n");
#endif
goto error;
}
// notify library about attaching to process
successfull = (*DllEntry)((HINSTANCE)code, DLL_PROCESS_ATTACH, 0);
if (!successfull) {
#if DEBUG_OUTPUT
OutputDebugStringA("Can't attach library.\n");
#endif
goto error;
}
result->initialized = 1;
}
return (HMEMORYMODULE)result;
error:
// cleanup
MemoryFreeLibrary(result);
return NULL;
}