Windows提供的API(LoadLibrary
, LoadLibraryEx
)只支持从文件系统上加载DLL文件,我们无法使用这些API从内存中加载DLL。
但是有些时候,我们的确需要从内存中加载DLL,比如:
LoadResource
等API读取DLL文件到内存中,然后从内存中加载DLL。本文主要介绍如何实现从内存中加载DLL,并调用DLL提供接口函数(必须是纯C接口)。
虽然“从内存中加载DLL”和“Windows的注入与拦截”之间没有直接关系,但还是选择放在《Windows注入与拦截》系列文章之中,主要是为了后面介绍的“无痕注入”(也叫反射注入)作铺垫。
从内存中加载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 |
+----------------+
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头的起始地址。
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的组件。
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;
我们要模拟PE加载器从内存中加载DLL,我们首先要知道Windows加载DLL文件的步骤,以及需要准备那些结构体等。
当我们调用LoadLibrary
时,windows主要执行了下面的一些步骤:
PEHeader.OptionalHeader.ImageBase
位置分配PEHeader.OptionalHeader.SizeOfImage
字节的内存区域。Section header
中的每个Section
,并将它们的实际内容拷贝到第2步分配的地址空间中。拷贝的目的地址的计算方法为:IMAGE_SECTION_HEADER.VirtualAddress偏移 + 第二步分配的内存区域的起始地址
。IMAGE_OPTIONAL_HEADER64.DataDirectory[5]
."PEHeader.OptionalHeader.DataDirectory.Image_directory_entry_import"
导入表."PEHeader.Image_Section_Table.Characteristics"
属性来设置内存页的访问属性; 如果被设置为”discardable”属性,则释放该内存页。DLL_PROCESS_ATTACH
参数调用。本代码参考了fancycode/MemoryModule,修复原有代码的若干BUG,扩充了部分功能,并针对第二节介绍的步骤添加了详细的注释。
#ifndef __MEMORY_MODULE_HEADER
#define __MEMORY_MODULE_HEADER
#include
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;
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;
}
完整的示例代码见:https://gitee.com/china_jeffery/MemoryModule
另外,Stephen Fewer 的ReflectiveDLLInjection提供了反射注入的完整解决方案,其中的
LoadLibraryR
也实现了和本文类似的功能。