Windows注入与拦截(6) -- 从内存中加载DLL

xiaoxiao2021-02-28  43

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; }

完整的示例代码见:https://gitee.com/china_jeffery/MemoryModule

另外,Stephen Fewer 的ReflectiveDLLInjection提供了反射注入的完整解决方案,其中的LoadLibraryR也实现了和本文类似的功能。

china_jeffery 认证博客专家 C/C Qt Node.js 持续学习者;擅长开发开源组件及相关工具;长期致力于应用各种IT新技术提升生产效率和解决实际问题;china_jeffery@163#com
转载请注明原文地址: https://www.6miu.com/read-2622192.html

最新回复(0)