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

Windows提供的API(LoadLibrary, LoadLibraryEx)只支持从文件系统上加载DLL文件,我们无法使用这些API从内存中加载DLL。

但是有些时候,我们的确需要从内存中加载DLL,比如:

  1. 对发布的文件数量有限制。我们可以将DLL打包到exe的资源中,程序运行时从调用LoadResource等API读取DLL文件到内存中,然后从内存中加载DLL。
  2. 需要对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主要执行了下面的一些步骤:

  1. 检测DOS和PE头的合法性。
  2. 尝试在PEHeader.OptionalHeader.ImageBase位置分配PEHeader.OptionalHeader.SizeOfImage字节的内存区域。
  3. 解析Section header中的每个Section,并将它们的实际内容拷贝到第2步分配的地址空间中。拷贝的目的地址的计算方法为:IMAGE_SECTION_HEADER.VirtualAddress偏移 + 第二步分配的内存区域的起始地址
  4. 检查加载到进程地址空间的位置和之前PE文件中指定的基地址是否一致,如果不一致,则需要重定位。重定位就需要用到1.2节中的IMAGE_OPTIONAL_HEADER64.DataDirectory[5].
  5. 加载该DLL依赖的其他dll,并构建"PEHeader.OptionalHeader.DataDirectory.Image_directory_entry_import"导入表.
  6. 根据每个Section的"PEHeader.Image_Section_Table.Characteristics"属性来设置内存页的访问属性; 如果被设置为”discardable”属性,则释放该内存页。
  7. 获取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 CSDN认证博客专家 C/C++ Qt Node.js
持续学习者;
擅长开发开源组件及相关工具;
长期致力于应用各种IT新技术提升生产效率和解决实际问题;
china_jeffery@163#com
©️2020 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页
实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值