Windows提供的API(LoadLibrary, LoadLibraryEx)只支持从文件系统上加载DLL文件,我们无法使用这些API从内存中加载DLL。
但是有些时候,我们的确需要从内存中加载DLL,比如:
对发布的文件数量有限制。我们可以将DLL打包到exe的资源中,程序运行时从调用LoadResource等API读取DLL文件到内存中,然后从内存中加载DLL。需要对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主要执行了下面的一些步骤:
检测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,扩充了部分功能,并针对第二节介绍的步骤添加了详细的注释。
HMEMORYMODULE是一个自定义结构体,该结构体分配在进程的默认堆上面,调用者需要保存该结构体指针,在后面获取接口地址和释放DLL时需要传入该指针。
typedef struct { PIMAGE_NT_HEADERS headers; unsigned char *codeBase; HMODULE *modules; int numModules; int initialized; } MEMORYMODULE, *PMEMORYMODULE;完整的示例代码见:https://gitee.com/china_jeffery/MemoryModule
另外,Stephen Fewer 的ReflectiveDLLInjection提供了反射注入的完整解决方案,其中的LoadLibraryR也实现了和本文类似的功能。
china_jeffery 认证博客专家 C/C Qt Node.js 持续学习者;擅长开发开源组件及相关工具;长期致力于应用各种IT新技术提升生产效率和解决实际问题;china_jeffery@163#com