如何在进程内找到DLL的文本部分的起始地址?(64 位)

逆向工程 视窗 x86 x86-64 视窗 10
2021-07-06 15:06:28

一年前有一个关于这个的问题,但答案并没有解释如何在 C/C++ 中做到这一点:

如何找到 .text 部分的开头?

我不是在谈论模块起始地址,我们可以使用 GetModuleHandle(module)

我说的是进程内 DLL 的文本部分的开始,所以当我注入一个进程(使用 dll 注入)时,我可以获得目标 DLL 的起始地址并修补它的部分指令,基本上我知道偏移量DLL 文件中的一条指令在磁盘上,我只想找到文本部分的开头并向其添加该偏移量,以便我可以通过注入加载它的进程来修补它

并且PE文件中那部分的偏移量与磁盘不同,例如在一个测试程序中,我检查了磁盘中的偏移量为0x300,内存中的偏移量为0x1000(32位应用程序)

那么我怎样才能做到这一点既可以在 32 位又可以在 64 位应用程序中使用呢?

3个回答

dbghelp.h 中有很多文档化的 Helper 函数可以完全解析 Pe 文件

如果您不想使用 dbghelp.h,几乎所有这些函数在 ntdll.dll 中都有一个 Rtl 等效函数,您可以动态调用它(LoadLibrary .GetProcAddres)

例如,此函数ImageNtHeader 在 ntdll.dll 中有一个等效的 RtlImageNtHeader,您可以将其与 GetprocAddress() 一起使用

下面是一个示例代码,它使用 dbghelp.h 转储给定模块中所有部分的一些相关细节,并与 msvc 2017 社区在 win 10 64 机器上用于 x64 架构 x86 您可能需要使用相关的 32 位结构

#include <windows.h>
#include <dbghelp.h>
#include <stdio.h>
#pragma comment(lib, "dbghelp.lib")
int main(void)
{
    HMODULE hMod = GetModuleHandleA("kernelbase.dll");
    if (hMod)
    {
        PIMAGE_NT_HEADERS64 NtHeader = ImageNtHeader(hMod);
        WORD NumSections = NtHeader->FileHeader.NumberOfSections;
        PIMAGE_SECTION_HEADER Section = IMAGE_FIRST_SECTION(NtHeader);
        for (WORD i = 0; i < NumSections; i++)
        {
            printf("%-8s\t%x\t%x\t%x\n", Section->Name, Section->VirtualAddress,
                   Section->PointerToRawData, Section->SizeOfRawData);
            Section++;
        }
    }
    return 0;
}

编译并链接并执行

cl /Zi /W4 /nologo /analyze /EHsc /Od %1 /link /release

TextSectAddr.exe
======================================
Name            VA      Raw     Size
======================================
.text           1000    400     102600
.rdata          104000  102a00  155e00
.data           25a000  258800  1600
.pdata          25f000  259e00  e800
.didat          26e000  268600  800
.rsrc           26f000  268e00  600
.reloc          270000  269400  22400

为了获得所需 dll 的文本部分,首先您需要它的基地址(换句话说,DOS 标头),它会引导您到它的 PE 标头(使用e_lfanew字段IMAGE_DOS_HEADER)。

在 PE 标头(或其结构化名称IMAGE_NT_HEADERS)中,您会找到一个名为的字段FileHeader,其中包含有关所需图像中存在的部分数量的信息。

要到达第一部分(同样它有一个结构化名称IMAGE_SECTION_HEADER),您需要通过将 DOS_HEADER 和 NT_HEADERS 的大小添加到图像的基地址来传递它们,然后遍历这些部分,检查它的特性字段的值

IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_EXECUTE

表示代码/文本部分。

当您到达文本部分的标题时,您可以使用该字段

VirtualAddress

获取代码段的起始地址

把它想象成这个结构

 ___________________________
|                           |
|      IMAGE_DOS_HEADER     |
|___________________________|
|                           |
|     IMAGE_NT_HEADERS      | <-- Contains FileHeader which has the number of sections exist in the dll
|___________________________|
|                           |
|   IMAGE_SECTION_HEADER    |
|___________________________|
|                           |
|   IMAGE_SECTION_HEADER    |
|___________________________|
|                           |
|   IMAGE_SECTION_HEADER    |
|___________________________|
|                           |
|   IMAGE_SECTION_HEADER    |
|___________________________|
|                           |
|   IMAGE_SECTION_HEADER    |
|___________________________|

这是使用stylo's answer获取 PE 图像部分的另一种方法以下 C 代码使用fopen(),fread()fseek()函数。所以,这段代码可以在任何类 Unix 系统中使用。

int main(int argc, char* argv[])
{
    if (argc < 2)
        return;

    FILE* file = fopen(argv[1], "rb");
    if (file == NULL)
        return;

    IMAGE_DOS_HEADER dosHeader = { 0 };
    fread(&dosHeader, sizeof dosHeader, 1, file);
    fseek(file, dosHeader.e_lfanew, SEEK_SET);

    IMAGE_NT_HEADERS ntHeader = { 0 };
    fread(&ntHeader, sizeof ntHeader, 1, file);

    IMAGE_SECTION_HEADER secHeader = { 0 };

    for (int i = 0; i < ntHeader.FileHeader.NumberOfSections; i++)
    {
        fread(&secHeader, sizeof secHeader, 1, file);
        printf("%-8s\t%x\t%x\t%x\n",
               secHeader.Name,
               secHeader.VirtualAddress,
               secHeader.PointerToRawData,
               secHeader.SizeOfRawData);
    }

    return 0;
}

那么,代码有什么作用呢?

  • fopen(3) 以二进制模式打开文件。
  • fread(3)读取IMAGE_DOS_HEADER结构。
  • fseek(3)使用valuefile流的文件位置指示符设置IMAGE_NT_HEADERS结构的开头dosHeader.e_lfanew
  • fread(3)读取IMAGE_NT_HEADERS结构。
  • 现在循环读取所有节标题。请记住,fread流的位置指示器会提前读取的总字节数。因此,无需增加secHeader.