当前位置: 首页 > article >正文

分析内存动态加载PE文件

在Windows操作系统中,PE(Portable Executable)文件格式是用于可执行文件、DLL文件和其他类型文件的格式。通常,PE文件是通过操作系统的加载器加载到内存中并执行的。加载器会解析PE文件,获取详细的装入参数,并根据这些参数将文件映射到内存中执行。然而,有时我们需要在不使用操作系统加载器的情况下加载和执行PE文件,比如在开发自定义的加载器或进行反病毒与逆向工程研究时。

通过模拟PE文件加载器的工作流程,我们可以手动将可执行文件映射到内存中,并按照规则进行展开和执行。本文将介绍如何在内存中加载和运行PE文件,包括DLL和EXE文件。我们将详细讲解关键步骤和代码实现,并分享一些注意事项和最佳实践。

获取PE文件加载到内存后的镜像大小

PE文件的镜像大小存储在NT头的可选头(Optional Header)中。我们通过获取DOS头,找到NT头的位置,然后读取可选头中的SizeOfImage字段来获取镜像大小,从而确定在内存中需要分配的空间大小。

// 获取PE文件加载到内存后的镜像大小
DWORD GetImageSize(LPVOID data)
{
    // 获取DOS头
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)data;
    // 获取NT头
    PIMAGE_NT_HEADERS32 ntHeaders = (PIMAGE_NT_HEADERS32)((ULONG_PTR)dosHeader + dosHeader->e_lfanew);
    // 返回镜像大小
    return ntHeaders->OptionalHeader.SizeOfImage;
}

将内存PE数据按节对齐映射到进程内存中

PE文件的各个节(Section)在磁盘上是按文件对齐(File Alignment)存储的,但在内存中需要按节对齐(Section Alignment)进行存储。我们需要读取节表头的信息,计算每个节在内存中的位置,然后将节的数据复制到对应位置,以保持内存布局与PE文件的预期一致。

// 将内存PE数据按节大小对齐映射到进程内存中
BOOL MapSections(LPVOID data, LPVOID baseAddress)
{
    // 获取DOS头
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)data;
    // 获取NT头
    PIMAGE_NT_HEADERS32 ntHeaders = (PIMAGE_NT_HEADERS32)((ULONG_PTR)dosHeader + dosHeader->e_lfanew);
    // 获取头部大小
    DWORD sizeOfHeaders = ntHeaders->OptionalHeader.SizeOfHeaders;
    // 获取节的数量
    WORD numberOfSections = ntHeaders->FileHeader.NumberOfSections;
    // 获取第一个节表头的地址
    PIMAGE_SECTION_HEADER sectionHeader = (PIMAGE_SECTION_HEADER)((ULONG_PTR)ntHeaders + sizeof(IMAGE_NT_HEADERS32));
    // 复制所有头和节表头到内存
    RtlCopyMemory(baseAddress, data, sizeOfHeaders);
    // 逐节复制到内存中
    for (WORD i = 0; i < numberOfSections; i++)
    {
        // 跳过空节
        if ((sectionHeader->VirtualAddress == 0) || (sectionHeader->SizeOfRawData == 0))
        {
            sectionHeader++;
            continue;
        }
        // 复制每个节的数据到目标内存
        LPVOID srcMem = (LPVOID)((ULONG_PTR)data + sectionHeader->PointerToRawData);
        LPVOID destMem = (LPVOID)((ULONG_PTR)baseAddress + sectionHeader->VirtualAddress);
        RtlCopyMemory(destMem, srcMem, sectionHeader->SizeOfRawData);
        sectionHeader++;
    }
    return TRUE;
}

更新PE文件重定位表

重定位表包含了一系列地址,这些地址需要根据实际加载的基地址进行调整。重定位表的每个条目包含一个块头(Block Header)和若干重定位项(Relocation Entry),每个重定位项描述了需要调整的地址偏移。通过遍历重定位表并计算新的地址,确保PE文件在新的基地址下能正确执行。

// 更新PE文件重定位表
BOOL ApplyRelocations(LPVOID baseAddress)
{
    // 获取DOS头
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)baseAddress;
    // 获取NT头
    PIMAGE_NT_HEADERS32 ntHeaders = (PIMAGE_NT_HEADERS32)((ULONG_PTR)dosHeader + dosHeader->e_lfanew);
    // 获取重定位表地址
    PIMAGE_BASE_RELOCATION relocTable = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)dosHeader + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
    // 如果没有重定位表,直接返回TRUE
    if ((PVOID)relocTable == (PVOID)dosHeader)
        return TRUE;

    // 遍历重定位表
    while (relocTable->VirtualAddress + relocTable->SizeOfBlock != 0)
    {
        // 获取重定位数据
        WORD *relocData = (WORD *)((PBYTE)relocTable + sizeof(IMAGE_BASE_RELOCATION));
        // 计算需要修正的地址数量
        int numberOfReloc = (relocTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
        for (int i = 0; i < numberOfReloc; i++)
        {
            // 如果需要修正的地址
            if ((DWORD)(relocData[i] & 0x0000F000) == 0x00003000)
            {
                DWORD* address = (DWORD *)((PBYTE)dosHeader + relocTable->VirtualAddress + (relocData[i] & 0x0FFF));
                DWORD delta = (DWORD)dosHeader - ntHeaders->OptionalHeader.ImageBase;
                *address += delta;
            }
        }
        // 移动到下一个重定位表
        relocTable = (PIMAGE_BASE_RELOCATION)((PBYTE)relocTable + relocTable->SizeOfBlock);
    }
    return TRUE;
}

填写PE文件导入表

PE文件的导入表描述了该文件所依赖的外部DLL和函数。我们需要读取导入表中的DLL名称,加载相应的DLL,并解析导入的函数地址。导入表包含了多个导入描述符(Import Descriptor),每个导入描述符对应一个外部DLL。通过填充导入表,可以确保PE文件在运行时能够正确调用外部函数。

// 填写PE文件导入表
BOOL LoadImports(LPVOID baseAddress)
{
    // 获取DOS头
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)baseAddress;
    // 获取NT头
    PIMAGE_NT_HEADERS32 ntHeaders = (PIMAGE_NT_HEADERS32)((ULONG_PTR)dosHeader + dosHeader->e_lfanew);
    // 获取导入表地址
    PIMAGE_IMPORT_DESCRIPTOR importTable = (PIMAGE_IMPORT_DESCRIPTOR)((ULONG_PTR)dosHeader + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
    // 遍历导入表
    while (importTable->OriginalFirstThunk != 0)
    {
        // 获取导入的DLL名称
        char *dllName = (char *)((ULONG_PTR)dosHeader + importTable->Name);
        // 尝试加载DLL
        HMODULE hDll = GetModuleHandle(dllName);
        if (hDll == NULL)
        {
            hDll = LoadLibrary(dllName);
            if (hDll == NULL)
            {
                importTable++;
                continue;
            }
        }

        // 获取导入表的原始和实际地址
        PIMAGE_THUNK_DATA32 importNameArray = (PIMAGE_THUNK_DATA32)((ULONG_PTR)dosHeader + importTable->OriginalFirstThunk);
        PIMAGE_THUNK_DATA32 importFuncAddrArray = (PIMAGE_THUNK_DATA32)((ULONG_PTR)dosHeader + importTable->FirstThunk);
        // 遍历导入表
        for (DWORD i = 0; importNameArray[i].u1.AddressOfData != 0; i++)
        {
            // 获取导入的函数名
            PIMAGE_IMPORT_BY_NAME importByName = (PIMAGE_IMPORT_BY_NAME)((ULONG_PTR)dosHeader + importNameArray[i].u1.AddressOfData);
            FARPROC funcAddress = NULL;
            // 判断是按序号还是按名称导入
            if (importNameArray[i].u1.Ordinal & 0x80000000)
            {
                funcAddress = GetProcAddress(hDll, (LPCSTR)(importNameArray[i].u1.Ordinal & 0x0000FFFF));
            }
            else
            {
                funcAddress = GetProcAddress(hDll, (LPCSTR)importByName->Name);
            }
            // 填写函数地址
            importFuncAddrArray[i].u1.Function = (ULONG_PTR)funcAddress;
        }
        importTable++;
    }
    return TRUE;
}

更新PE文件的加载基地址

PE文件的基地址存储在NT头的可选头中。我们需要将其更新为实际加载的基地址,以确保文件中的所有相对地址都能正确解析。这一步确保了PE文件在内存中能够正确定位其所有数据和代码段。

// 更新PE文件的加载基地址
BOOL UpdateImageBase(LPVOID baseAddress)
{
    // 获取DOS头
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)baseAddress;
    // 获取NT头
    PIMAGE_NT_HEADERS32 ntHeaders = (PIMAGE_NT_HEADERS32)((ULONG_PTR)dosHeader + dosHeader->e_lfanew);
    // 设置新的基地址
    ntHeaders->OptionalHeader.ImageBase = (ULONG_PTR)baseAddress;
    return TRUE;
}

获取内存DLL的导出函数

PE文件的导出表描述了该文件导出的函数。我们需要读取导出表,查找指定的函数名,并获取函数的地址。导出表包含了多个导出名称(Export Name)、序号(Ordinal)和函数地址(Function Address)。通过遍历导出表,可以模拟GetProcAddress函数,获取内存中加载的DLL的导出函数地址。

// 模拟GetProcAddress获取内存DLL的导出函数
LPVOID MemoryGetProcAddress(LPVOID lpMappedBase, PCHAR lpszFuncName)
{
    // 获取DOS头
    PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)lpMappedBase;
    // 获取NT头
    PIMAGE_NT_HEADERS pNtHdr = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDosHdr + pDosHdr->e_lfanew);
    // 获取导出表地址
    PIMAGE_EXPORT_DIRECTORY pExpDir = (PIMAGE_EXPORT_DIRECTORY)((ULONG_PTR)pDosHdr + pNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

    // 获取导出表中的各个数据项
    PDWORD pNames = (PDWORD)((ULONG_PTR)pDosHdr + pExpDir->AddressOfNames);
    PCHAR pFuncName = NULL;
    PWORD pOrdinals = (PWORD)((ULONG_PTR)pDosHdr + pExpDir->AddressOfNameOrdinals);
    PDWORD pFuncs = (PDWORD)((ULONG_PTR)pDosHdr + pExpDir->AddressOfFunctions);
    LPVOID lpFunc = 0;

    // 遍历导出表,查找函数名
    for (DWORD i = 0; i < pExpDir->NumberOfNames; i++)
    {
        pFuncName = (PCHAR)((ULONG_PTR)pDosHdr + pNames[i]);
        if (lstrcmpi(pFuncName, lpszFuncName) == 0)
        {
            // 返回函数地址
            lpFunc = (LPVOID)((ULONG_PTR)pDosHdr + pFuncs[pOrdinals[i]]);
            break;
        }
    }
    return lpFunc;
}

释放内存中加载的DLL

通过调用VirtualFree函数,我们可以释放之前分配的内存,以避免内存泄漏。确保内存资源在使用完毕后能够被正确释放。

// 释放内存中加载的DLL
BOOL FreeMemoryLibrary(LPVOID lpMappedBase)
{
    // 释放内存
    return ::VirtualFree(lpMappedBase, 0, MEM_RELEASE);
}

加载并获取PE文件OEP入口

依次调用上述各个函数,通过分配内存空间、映射节、更新重定位表和导入表、获取OEP位置等,完成PE文件从加载到获取OEP入口地址的全过程,最终将加载好的PE文件入口返回给调用者。

// 加载PE文件
LPVOID LoadPE(LPVOID data, DWORD size)
{
    // 获取镜像大小
    DWORD imageSize = GetImageSize(data);
    // 分配内存
    LPVOID baseAddress = VirtualAlloc(NULL, imageSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (baseAddress == NULL)
        return NULL;
    // 清零内存
    RtlZeroMemory(baseAddress, imageSize);
    // 映射内存镜像
    if (!MapSections(data, baseAddress))
        return NULL;
    // 更新重定位表
    if (!ApplyRelocations(baseAddress))
        return NULL;
    // 更新导入表
    if (!LoadImports(baseAddress))
        return NULL;
    // 设置内存保护
    DWORD oldProtect = 0;
    if (!VirtualProtect(baseAddress, imageSize, PAGE_EXECUTE_READWRITE, &oldProtect))
        return NULL;
    // 设置基地址
    if (!UpdateImageBase(baseAddress))
        return NULL;
    return baseAddress;
}

装载并执行PE文件

首先以执行DLL文件为案例,创建一个DLL文件,包含三个导出函数:MessageAddFunctionSubFunction。这些函数将被导出并在后续步骤中动态加载和使用。

#include <Windows.h>
#include <iostream>

// 导出 Message 函数
extern "C"__declspec(dllexport) BOOL Message(char *lpszText, char *lpszCaption)
{
    printf("lpszText = %s \n", lpszText);
    printf("lpszCaption = %s \n", lpszCaption);
    return TRUE;
}

// 导出 AddFunction 函数
extern "C"__declspec(dllexport) INT AddFunction(INT x, INT y)
{
    return x + y;
}

// 导出 SubFunction 函数
extern "C"__declspec(dllexport) INT SubFunction(INT x, INT y)
{
    return x - y;
}

// DLL入口点
BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

将上述代码保存为hook.cpp,然后使用Visual Studio或其他C++编译器编译为DLL文件。例如,在Visual Studio中,可以创建一个新的DLL项目,将上述代码文件添加到项目中,并生成项目。这将生成一个hook.dll文件。

如下代码则是调用DLL中的导出函数的实例,其中InvokeDllMain用于获取DLL的入口并调用该入口函数,以此来实现对DLL的初始化。在主函数中通过使用ReadFileD://hook.dll下的DLL文件读入到内存中,当读入后继续使用LoadPE函数,将文件在内存中进行布局并最终返回一个入口基地址,当有了入口基地址以后,就可以通过调用MemoryGetProcAddress并传入导出函数名来获取到该函数所在模块中的入口地址。当有了入口地址则可通过函数指针的方式调用这些导出函数。

// 定义DllMain函数指针类型
typedef BOOL(__stdcall *typedef_DllMain)(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved);

// 调用DLL的入口函数DllMain
BOOL InvokeDllMain(LPVOID lpMappedBase)
{
    // 获取DOS头
    PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)lpMappedBase;
    // 获取NT头
    PIMAGE_NT_HEADERS pNtHdr = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDosHdr + pDosHdr->e_lfanew);
    // 获取DllMain入口点
    typedef_DllMain DllMain = (typedef_DllMain)((ULONG_PTR)pDosHdr + pNtHdr->OptionalHeader.AddressOfEntryPoint);
    // 调用DllMain入口点
    BOOL bRet = DllMain((HINSTANCE)lpMappedBase, DLL_PROCESS_ATTACH, NULL);
    return bRet;
}

int main(int argc, char *argv[])
{
    char dllFileName[] = "D://hook.dll";

    // ---------------------------------------------------------
    // 打开文件,若失败则返回
    // ---------------------------------------------------------
    HANDLE dllFile = CreateFile(dllFileName,
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_ARCHIVE,
        NULL
        );
    if (INVALID_HANDLE_VALUE == dllFile)
    {
        return 0;
    }

    // ---------------------------------------------------------
    // 打开PE文件并将其加载到内存中
    // ---------------------------------------------------------
    // 获取DLL文件大小
    DWORD dllFileSize = GetFileSize(dllFile, NULL);

    // 分配内存以存储DLL数据
    BYTE *dllData = new BYTE[dllFileSize];
    DWORD bytesRead = 0;

    // 读取DLL文件数据
    ReadFile(dllFile, dllData, dllFileSize, &bytesRead, NULL);

    // 关闭文件句柄
    CloseHandle(dllFile);

    // ---------------------------------------------------------
    // 加载PE文件并获得入口地址
    // ---------------------------------------------------------

    // 调用加载功能
    LPVOID dllBaseAddress = LoadPE(dllData, dllFileSize);
    if (dllBaseAddress == NULL)
    {
        delete[] dllData;
        return 0;
    }

    // 调用DllMain函数
    if (!InvokeDllMain(dllBaseAddress))
    {
        // 失败了则释放内存
        FreeMemoryLibrary(dllBaseAddress);
        delete[] dllData;
        return 0;
    }

    // ---------------------------------------------------------
    // 调用DLL功能
    // ---------------------------------------------------------

    // 调用第一个Message函数
    typedef BOOL(*typedef_Message)(char *lpszText, char *lpszCaption);

    typedef_Message Message = (typedef_Message)MemoryGetProcAddress(dllBaseAddress, "Message");
    if (Message != NULL)
    {
        Message("hello lyshark", "Message");
    }

    // 调用第二个AddFunction函数
    typedef INT(*typedef_AddFunction)(INT x, INT y);

    typedef_AddFunction AddFunction = (typedef_AddFunction)MemoryGetProcAddress(dllBaseAddress, "AddFunction");

    int add_sum = AddFunction(10, 20);
    printf("AddFunction = %d \n", add_sum);

    // 调用第三个SubFunction函数
    typedef INT(*typedef_SubFunction)(INT x, INT y);

    typedef_SubFunction SubFunction = (typedef_SubFunction)MemoryGetProcAddress(dllBaseAddress, "SubFunction");

    int sub_sum = SubFunction(100, 20);
    printf("SubFunction = %d \n", sub_sum);

    // ---------------------------------------------------------
    // 释放内存
    // ---------------------------------------------------------
    FreeMemoryLibrary(dllBaseAddress);
    delete[] dllData;
    system("pause");
    return 0;
}

相对于DLL文件的加载,加载EXE文件的流程与DLL文件保持一致,读者可自行编译一个不带有资源文件的ConsoleApplication应用程序,并动态调用它执行,代码如下所示;

int main(int argc, char *argv[])
{
    char exeFileName[] = "D://ConsoleApplication.exe";

    // ---------------------------------------------------------
    // 打开文件,若失败则返回
    // ---------------------------------------------------------
    HANDLE exeFile = CreateFile(exeFileName,
        GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_ARCHIVE,
        NULL
        );
    if (INVALID_HANDLE_VALUE == exeFile)
    {
        return 0;
    }

    // ---------------------------------------------------------
    // 打开PE文件并将其加载到内存中
    // ---------------------------------------------------------
    // 获取EXE文件大小
    DWORD exeFileSize = GetFileSize(exeFile, NULL);
    
    // 分配内存以存储EXE数据
    BYTE *exeData = new BYTE[exeFileSize];
    DWORD bytesRead = 0;
    
    // 读取EXE文件数据
    ReadFile(exeFile, exeData, exeFileSize, &bytesRead, NULL);

    // 关闭文件句柄
    CloseHandle(exeFile);

    // 加载并运行EXE
    LPVOID exeBaseAddress = LoadPE(exeData, exeFileSize);
    if (exeBaseAddress == NULL)
    {
        delete[] exeData;
        return 0;
    }

    // ---------------------------------------------------------
    // 执行EXE入口点
    // ---------------------------------------------------------
    // 读入EXE基地址
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)exeBaseAddress;
    PIMAGE_NT_HEADERS32 ntHeaders = (PIMAGE_NT_HEADERS32)((ULONG_PTR)dosHeader + dosHeader->e_lfanew);

    // 得到入口地址
    LPVOID exeEntry = (LPVOID)((ULONG_PTR)dosHeader + ntHeaders->OptionalHeader.AddressOfEntryPoint);

    // 跳转到入口点处执行
    ((void(*)())exeEntry)();

    delete[] exeData;
    system("pause");
    return 0;
}

http://www.kler.cn/news/309369.html

相关文章:

  • 第十一章 【后端】商品分类管理微服务(11.3)——商品管理模块 yumi-etms-goods
  • NLP与文本生成:使用GPT模型构建自动写作系统
  • 建筑机器人通用操作系统设计方案
  • Js中call、apply和bind的区别
  • C语言 | Leetcode C语言题解之第412题Fizz Buzz
  • 鸿蒙开发(NEXT/API 12)【网络连接管理】 网络篇
  • 实现浏览器的下拉加载功能(类似知乎)
  • maven项目下使用Jacoco测试覆盖率
  • vue3使用panolens.js实现全景,带有上一个下一个,全屏功能
  • 风力发电厂智能化转型5G工业路由器物联网应用解决方案
  • 大数据-133 - ClickHouse 基础概述 全面了解
  • C#基础(12)递归函数
  • 测试工程师学历路径:从功能测试到测试开发
  • MUNIK谈ASPICE系列专题分享(六)企业为什么要做ASPICE?
  • 5.内容创作的未来:ChatGPT如何辅助写作(5/10)
  • 计算机人工智能前沿进展-大语言模型方向-2024-09-15
  • Nacos服务治理
  • 电学基础概念详解及三相电公式汇总
  • AI写作神器:助力体制内小白轻松完成材料撰写,减少慌张茫然
  • unity的学习
  • linux驱动开发-内核并发 poll 和 lock
  • 深度解码:机器学习与深度学习的界限与交融
  • CMakeLists.txt的学习了解
  • 【LabVIEW学习篇 - 25】:JKI状态机
  • I2C/IIC学习笔记
  • nonlocal本质讲解(前篇)——从滤波到Nonlocal均值滤波
  • Java项目实战II基于Spring Boot的宠物商城网站设计与实现
  • linux-软件包管理-包管理工具(Debian 系)
  • 【C++入门学习】7. 类型
  • Java项目实战II基于Java+Spring Boot+MySQL的服装厂服装生产管理系统的设计与实现