只使用指针算法遍历一个已加载的 DLL/EXE PE 文件格式

Using only pointer arithmetic to traverse a loaded DLL/EXE for the PE File Format

我的 end-goal 正在从静态导入数据中获取 DLL 名称列表 table。

我想我可以做类似的事情,
auto data_dirs = p_loaded_image->FileHeader->OptionalHeader.DataDirectory;

然后以某种方式遍历该地址列表,然后以这种方式获取 DLL 名称;类似的东西。

所以对于 baby-steps,我只是想验证我是否可以将 p_loaded_image->FileHeader->OptionalHeader.SizeOfStackCommit; 的值与手动 pointer-math 等价物相匹配。如果没有 Access Violation 异常,我似乎无法做到这一点,这似乎证实我做错了。

我做错了什么,具体来说 我如何让我的 pointer-math 查询匹配实际加载的图像的 API return为了获得 SizeOfStackCommit 的相同值?如果你能教我这么多,我希望能从我的 DLL-name-finding WIP 的当前点取得进步。

出于 time-saving 目的,如果您的编译器支持 std::experimental::filesystem 您可以从 // Skip to here 的注释开始以避免所有控制台和文件验证样板,否则您将需要将其存根或将其更改为对旧 C++ 规范更友好的内容。

#include "Windows.h"
#include "Imagehlp.h"
#include "tchar.h"
#include "stdio.h"
#include "stdlib.h"

#include <string>
#include <vector>
#include <experimental\filesystem>

// All hard-coded values taken directly from latest PE/COFF .docx Documentation from MS:
// => http://go.microsoft.com/fwlink/p/?linkid=84140

const int MAGIC_32_NUM = 0x10b;
const int MAGIC_64_NUM = 0x20b;

// These two require C++17 || If needed, replace with older valid file-verification.
namespace fs = std::experimental::filesystem;
bool verify_loaded_file(std::string);

int _tmain(int argc, _TCHAR* argv[])
{
    std::string image_to_load;
    if (argc == 2) {
        image_to_load = argv[1];
    }
    else {
        printf("A valid path to a loadable image needs to be your only command-line parameter for %s", argv[0]);
        return -1;
    }

    bool validFile = verify_loaded_file(image_to_load);

    if (!validFile) {
        printf("A valid file path of a DLL or EXE needs to be your only command-line parameter for %s", argv[0]);
        return -1;
    }

    auto filesystem_image                   = fs::absolute(fs::path(image_to_load));
    std::string image_directory             = filesystem_image.parent_path().string();
    std::string image_name                  = filesystem_image.stem().string();
    std::string image_name_and_extension    = image_name + filesystem_image.extension().string();
    bool is64bit, is32bit                   = false;

    // To use MapAndLoad, you need to manually include Imagehlp.lib in your project.
    // The Imagehlp.h header alone does not suffice.
    LOADED_IMAGE loaded_image = { 0 };
    LOADED_IMAGE * p_loaded_image = &loaded_image;
    bool image_loaded = MapAndLoad(image_name_and_extension.c_str(), image_directory.c_str(), p_loaded_image, FALSE, TRUE);
    int error_check = GetLastError();

    if (!image_loaded) {
        printf("Something went wrong when trying to load %s0 with error code %s1", image_to_load.c_str(), error_check);
        UnMapAndLoad(p_loaded_image);
        return -1;
    }   

    int magic_number = loaded_image.FileHeader->OptionalHeader.Magic;

    if      (magic_number == MAGIC_32_NUM) { is32bit = true; }
    else if (magic_number == MAGIC_64_NUM) { is64bit = true; }
    else {
        printf("The magic number from the optional header wasn't detected as 32-bit or 64-bit\n");
        printf("Check Windows System Error Code: %s\n", magic_number);
        UnMapAndLoad(p_loaded_image);
        return -1;
    }

    // Skip to here
    UCHAR * module_base_address = p_loaded_image->MappedAddress;
    size_t coverted_base_address = size_t(module_base_address);

    size_t windows_optional_header_offset;
    if (is64bit) { windows_optional_header_offset = size_t(24); }
    else { windows_optional_header_offset = size_t(28); }

    size_t data_directory_optional_header_offset;
    if (is64bit) { data_directory_optional_header_offset = size_t(112); }
    else { data_directory_optional_header_offset = size_t(96); }

    size_t size_stack_commit_offset;
    if (is64bit) { size_stack_commit_offset = size_t(80); }
    else { size_stack_commit_offset = size_t(76); }

    // The commented out line below breaks with Access Violations, as does the line following it:
    // auto sum_for_size_stack = size_t(coverted_base_address + size_stack_commit_offset);
    auto sum_for_size_stack = size_t(coverted_base_address + 
                                    windows_optional_header_offset + 
                                    data_directory_optional_header_offset + 
                                    size_stack_commit_offset);

    auto direct_access_size_stack = p_loaded_image->FileHeader->OptionalHeader.SizeOfStackCommit;
    DWORD64 * addy = &direct_access_size_stack;

    printf("Direct: %s\n", direct_access_size_stack);
    printf("Pointer-Math: %s\n", sum_for_size_stack);

    UnMapAndLoad(p_loaded_image);
    return 0;
}

//

bool verify_loaded_file(std::string file_to_verify)
{
    if (fs::exists(file_to_verify))
    {
        size_t extension_query = file_to_verify.find(".dll", 0);
        if (extension_query == std::string::npos)
        {
            extension_query = file_to_verify.find(".DLL", 0);
            if (extension_query == std::string::npos)
            {
                extension_query = file_to_verify.find(".exe", 0);
                if (extension_query == std::string::npos)
                {
                    extension_query = file_to_verify.find(".EXE", 0);
                }
                else { return true; }

                if (extension_query != std::string::npos) { return true; }
            }
            else { return true; }
        }
        else { return true; }   
    }
    return false;
}

windows 的 PE 文件格式在白皮书附件中有其最新文档,其中包含 .docxhttp://www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx

更新 1:

我的纯 pointer-arithmetic 遍历一直工作到 end-goal 之前。达到这一点需要我消除两层复杂性。

  1. 不要用特殊的 APIs 加载图像,因为我正在尝试获取 static 导入信息;解决方案是将 EXE 加载到一个字符向量中,以便在内存中对其进行快照。

  2. 忘记 over-complicated 听起来很垃圾的 RVA,除非你需要 使用它。只需对 PE 的 header 部分使用字节偏移。 部分 是您需要使用 RVA 的地方。只需将 char 向量的元素 0 的地址视为您的基地址,所有 RVA 都是从该地址计算出来的。 docx 还告诉您何时使用偏移量与实际地址,这一点很有用。查看我添加的答案,我在其中简要介绍了使用 RVA 获取导入 table.

我的程序仍然没有按照我的意愿运行,但至少我掌握了 pointer-arithmetic 匹配指针访问器的要点,这就是这个问题的目标。

我认为我剩下的障碍与加载数据的结构和加载位置有关。您可以在 Win10 机器上构建 运行 my WIP gist,或者将 ph_file 的值更新为 OS 上本地安装的其他 64 位程序,最好是没有.idata 部分。即使导入 DLL,也不保证 .idata 部分存在。 Calculator.exe 没有,举个例子。

更新 2:

我得到了一些调试帮助,终于开始工作了。代码是 POC,因此没有太多内容被清理或优化,但它是功能性的。针对 x86/win32 和 x64 二进制文件进行了测试。 Gist here.

我不相信你指出的行(sum_for_size_stack 的计算)会导致访问冲突。它只是无符号算术,不会溢出或导致陷阱值。

我相信您从 printf 得到了访问冲突,因为您使用的 %s 格式说明符的参数不是指向以 NUL 结尾的 ASCII 字符串的指针。我不知道是什么让您想到堆栈大小存储为字符串,或者将 size_t 传递给需要 const char* 的可变参数函数是个好主意,但两者都不是。

注意printf的前提条件。 size_t 参数的正确格式字符串是 %zx.

感谢 Ben,尽管它没有解除我的阻塞,但当他发现我对与内存分配相关的指针和从地址而不是指针初始化相关的指针缺乏理解时,他为我指明了正确的方向目的。为了克服这个问题,我做了一些学习和练习,以便:

  1. 了解C/C++ 指针 结构如何在内存中工作,以及关于通过字节偏移在结构中的成员位置
  2. 使用正确的指针正确加载数据

做这两件事为纯粹的指针算术用法打开了道路。

我在这些方面的问题是,过去使用指针时,我只需要对指针有最基本的了解。这个过程需要真正理解内存分配和遍历。

总的来说,这是一次很棒的重新学习经历!

在某个时候,通过字符数组访问数据以加载文件需要使用 RVA。为此,您需要加载包含所需数据的正确 IMAGE_SECTION_HEADER 结构。您使用该结构来计算 RVA 类似这样的东西,例如导入 table:

if  (queried_section_header->PointerToRawData >= import_table_data_dir->VirtualAddress &&
    (queried_section_header->PointerToRawData < (import_table_data_dir->VirtualAddress + queried_section_header->SizeOfRawData)))
{
    DWORD import_table_offset = queried_section_header->PointerToRawData - import_table_data_dir->VirtualAddress + queried_section_header->PointerToRawData;
}

我个人没有使用这个指南来理解指针,但乍一看它看起来很有前途:http://home.netcom.com/~tjensen/ptr/pointers.htm

万一它过期了,这个快照可能仍然存在:https://web.archive.org/web/20161208002919/http://home.netcom.com/~tjensen/ptr/pointers.htm