1. 程式人生 > >Windows注入與攔截(6) -- 從記憶體中載入DLL

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