2.10 PE结构:重建重定位表结构

简介: Relocation(重定位)是一种将程序中的一些地址修正为运行时可用的实际地址的机制。在程序编译过程中,由于程序中使用了各种全局变量和函数,这些变量和函数的地址还没有确定,因此它们的地址只能暂时使用一个相对地址。当程序被加载到内存中运行时,这些相对地址需要被修正为实际的绝对地址,这个过程就是重定位。在Windows操作系统中,程序被加载到内存中运行时,需要将程序中的各种内存地址进行重定位,以使程序能够正确地运行。Windows系统使用PE(Portable Executable)文件格式来存储可执行程序,其中包括重定位信息。当程序被加载到内存中时,系统会解析这些重定位信息,并将程序中的各

Relocation(重定位)是一种将程序中的一些地址修正为运行时可用的实际地址的机制。在程序编译过程中,由于程序中使用了各种全局变量和函数,这些变量和函数的地址还没有确定,因此它们的地址只能暂时使用一个相对地址。当程序被加载到内存中运行时,这些相对地址需要被修正为实际的绝对地址,这个过程就是重定位。

在Windows操作系统中,程序被加载到内存中运行时,需要将程序中的各种内存地址进行重定位,以使程序能够正确地运行。Windows系统使用PE(Portable Executable)文件格式来存储可执行程序,其中包括重定位信息。当程序被加载到内存中时,系统会解析这些重定位信息,并将程序中的各种内存地址进行重定位。

重定位表一般出现在DLL中,因为DLL都是动态加载,所以地址不固定,DLL的入口点在整个执行过程中至少要执行2次,一次是在开始时执行初始化工作,一次则是在结束时做最后的收尾工作,重定位表则是解决DLL的地址问题,为了能找到重定位表首先我们需要使用PeView工具查询DataDirectory数据目录表,在其中找到Base relocation字段,里面的0x00001800则是重定位表基地址;

我们通过使用WinHex工具定位到0x00001800即可看到重定位表信息,如下图中的1000代表的是重定位RVA地址,绿色的0104代表的则是重定位块的长度,后面则是每两个字节代表一个重定位块,0A是重定位地址,30则是重定位的类型,以此顺序向下排列。

重定位表也是分页排列的,每一页大小都是1000字节,通过使用FixRelocPage命令即可查询到当前程序中的重定位块信息,并以第一个为例,查询一下起始地址RVA为1000的页上,有哪些重定位结构,如下图所示;

其中的重定位RVA地址0000100A是用标黄色的1000加上标蓝色的0xA得到的。而修正RVA地址00003000加上模块基地址63FF0000+3000得到的则是第一个被修正的内存地址,读者可使用x64dbg跳转到该程序内自行确认。

重定位表的修复原理与IAT修复完全一致,我们需要分别读入脱壳前与脱壳后的两个程序,接着通过循环正确的重定位表信息,并依次覆盖到脱壳后的程序内,以此实现对重定位表的修复功能,实现代码如下所示;

#include <windows.h>
#include <stdio.h>

struct TypeOffset
{
   
   
  WORD Offset : 12;       // 低12位代表重定位地址
  WORD Type : 4;          // 高4位代表重定位类型
};

DWORD FileSize = 0;  // 定义文件大小
DWORD FileBase = 0;  // 保存文件的基地址

// 定义全局变量,来存储DOS,NT,Section头
PIMAGE_DOS_HEADER DosHeader = nullptr;
PIMAGE_NT_HEADERS NtHeader = nullptr;
PIMAGE_FILE_HEADER FileHead = nullptr;

// 将RVA转换为FOA的函数
DWORD RVAtoFOA(DWORD rva)
{
   
   
  auto SectionTables = IMAGE_FIRST_SECTION(NtHeader);    // 获取区段表
  WORD Count = NtHeader->FileHeader.NumberOfSections;    // 获取区段数量

  for (int i = 0; i < Count; ++i)
  {
   
   
    // 判断是否存在于区段中
    DWORD Section_Start = SectionTables[i].VirtualAddress;
    DWORD Section_Ends = SectionTables[i].VirtualAddress + SectionTables[i].SizeOfRawData;
    if (rva >= Section_Start && rva < Section_Ends)
    {
   
   
      // 找到之后计算位置并返回值
      return rva - SectionTables[i].VirtualAddress + SectionTables[i].PointerToRawData;
    }
  }
  return -1;
}

// 打开PE文件
bool OpenPeFile(LPCSTR FileName)
{
   
   
  // 打开文件
  HANDLE Handle = CreateFileA(FileName, GENERIC_READ, NULL,NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (Handle == INVALID_HANDLE_VALUE)
    return false;

  // 获取文件大小
  FileSize = GetFileSize(Handle, NULL);

  // 读取文件数据
  DWORD OperSize = 0;
  FileBase = (DWORD)new BYTE[FileSize];
  ReadFile(Handle, (LPVOID)FileBase, FileSize, &OperSize, NULL);

  // 获取DOS头并判断是不是一个有效的DOS文件
  DosHeader = (PIMAGE_DOS_HEADER)FileBase;
  if (DosHeader->e_magic != IMAGE_DOS_SIGNATURE)
    return false;

  // 获取 NT 头并判断是不是一个有效的PE文件
  NtHeader = (PIMAGE_NT_HEADERS)(FileBase + DosHeader->e_lfanew);
  if (NtHeader->Signature != IMAGE_NT_SIGNATURE)
    return false;

  // 判断是不是一个32位文件
  if (NtHeader->OptionalHeader.Magic != 0x010B)
    return false;

  CloseHandle(Handle);
  return true;
}

// 修复重定位表
void RepairFixReloc(char new_file[])
{
   
   
  DWORD base = NtHeader->OptionalHeader.ImageBase;
  // 1. 获取重定位表的 rva
  DWORD RelocRVA = NtHeader->OptionalHeader.DataDirectory[5].VirtualAddress;
  // 2. 获取重定位表
  auto Reloc = (PIMAGE_BASE_RELOCATION)(FileBase + RVAtoFOA(RelocRVA));

  // 3. 遍历重定位表中的重定位块,以0结尾
  while (Reloc->SizeOfBlock != 0)
  {
   
   
    // 3.1 输出分页基址
    printf("[↓] 分页基址: 0x%08X \n\n", Reloc->VirtualAddress);
    // 3.2 找到重定位项
    auto Offset = (TypeOffset*)(Reloc + 1);

    // 3.3 计算重定位项的个数
    // Reloc->SizeOfBlock 保存的是整个重定位块的大小 结构体 + 重定位项数组
    // Reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) 得到单个数组大小
    // 上面的结果 \ 2 = 重定位项的个数,原因是重定位项的大小为两个字节
    DWORD Size = (Reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2;

    // 3.4 遍历所有的重定位项
    for (DWORD i = 0; i < Size; ++i)
    {
   
   
      DWORD Type = Offset[i].Type;                  // 获取重定位类型,只关心为3的类型
      DWORD pianyi = Offset[i].Offset;              // 获取重定位的偏移值
      DWORD rva = pianyi + Reloc->VirtualAddress;   // 获取要重定位的地址所在的RVA
      DWORD foa = RVAtoFOA(rva);                    // 获取要重定位的地址所在的FOA
      DWORD fa = foa + FileBase;                    // 获取要重定位的地址所在的fa
      DWORD addr = *(DWORD*)fa;                     // 获取要重定位的地址
      DWORD new_addr = addr - base + 0x1500000;     // 计算重定位后的数据: addr - oldbase + newbase

      // 将重定位后的数据写回缓冲区(文件)
      if (Offset[i].Type == 3)
        *(DWORD*)fa = new_addr;

      printf("\t [->] 重定位RVA: 0x%08X | 重定位FOA: 0x%08X | 重定位地址: 0x%08X | 修正地址: 0x%08X \n", rva, foa, addr, new_addr);
    }
    // 找到下一个重定位块
    Reloc = (PIMAGE_BASE_RELOCATION)((DWORD)Reloc + Reloc->SizeOfBlock);
  }
  // 保存修正后的文件
  NtHeader->OptionalHeader.ImageBase = 0x1500000;

  // 打开一个新文件
  HANDLE new_handle = CreateFileA(new_file, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  if (new_handle == INVALID_HANDLE_VALUE)
    return;

  DWORD OperSize = 0;
  // 保存修正好的程序
  BOOL ret = WriteFile(new_handle, (LPVOID)FileBase, FileSize, &OperSize, NULL);
  if (ret == TRUE)
  {
   
   
    printf("\n\n");
    CloseHandle(new_handle);

    printf("[*] 修复 %s 文件 \t 写入基址: %08X \t 总长度: %d \t 写入长度: %d \n", new_file, FileBase, FileSize, OperSize);
  }
}

void Banner()
{
   
   
  printf(" ____        _ _     _   ____      _            \n");
  printf("| __ ) _   _(_) | __| | |  _ \\ ___| | ___   ___ \n");
  printf("|  _ \\| | | | | |/ _` | | |_) / _ \\ |/ _ \\ / __|\n");
  printf("| |_) | |_| | | | (_| | |  _ <  __/ | (_) | (__ \n");
  printf("|____/ \\__,_|_|_|\\__,_| |_| \\_\\___|_|\\___/ \\___|\n");
  printf("                                                \n");
  printf("Reloc 重定位表快速修复工具 \t By: LyShark \n");
  printf("Usage: BuildFix [原文件位置] [修复后文件位置] \n\n\n");
}

int main(int argc, char* argv[])
{
   
   
  Banner();
  if (argc == 3)
  {
   
   
    bool flag = OpenPeFile(argv[1]);
    if (true == flag)
    {
   
   
      RepairFixReloc(argv[2]);
    }
  }
  return 0;
}

运行上述程序,读者可自行传入脱壳前的程序与脱壳后的程序,此时则会实现自动替换,如下图所示;

本文作者: 王瑞
本文链接: https://www.lyshark.com/post/3c1b31b5.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

目录
相关文章
|
存储 算法 编译器
2.7 PE结构:重定位表详细解析
重定位表(Relocation Table)是Windows PE可执行文件中的一部分,主要记录了与地址相关的信息,它在程序加载和运行时被用来修改程序代码中的地址的值,因为程序在不同的内存地址中加载时,程序中使用到的地址也会受到影响,因此需要重定位表这个数据结构来完成这些地址值的修正。当程序需要被加载到不同的内存地址时,相关的地址值需要进行修正,否则程序运行会出现异常。而重定位表就是记录了在程序加载时需要修正的地址值的相关信息,包括修正地址的位置、需要修正的字节数、需要修正的地址的类型等。重定位表中的每个记录都称为一项(entry),每个entry包含了需要修正的地址值的详细信息,通常是以可变
172 0
2.7 PE结构:重定位表详细解析
|
存储 Windows
2.6 PE结构:导出表详细解析
导出表(Export Table)是Windows可执行文件中的一个结构,记录了可执行文件中某些函数或变量的名称和地址,这些名称和地址可以供其他程序调用或使用。当PE文件执行时Windows装载器将文件装入内存并将导入表中登记的DLL文件一并装入,再根据DLL文件中函数的导出信息对可执行文件的导入表(IAT)进行修正。
211 1
|
安全 编译器 API
2.5 PE结构:导入表详细解析
导入表(Import Table)是Windows可执行文件中的一部分,它记录了程序所需调用的外部函数(或API)的名称,以及这些函数在哪些动态链接库(DLL)中可以找到。在Win32编程中我们会经常用到导入函数,导入函数就是程序调用其执行代码又不在程序中的函数,这些函数通常是系统提供给我们的API,在调用者程序中只保留一些函数信息,包括函数名机器所在DLL路径。
180 1
|
Windows
2.8 PE结构:资源表详细解析
在Windows PE中,资源是指可执行文件中存放的一些固定不变的数据集合,例如图标、对话框、字符串、位图、版本信息等。PE文件中每个资源都会被分配对应的唯一资源ID,以便在运行时能够方便地查找和调用它们。PE文件中的资源都被组织成一个树形结构,其中最顶层为根节点(Root),下一级为资源类型(Type),再下一级为资源名称(Name),最终是实际的资源内容。PIMAGE_RESOURCE_DIRECTORY是Windows PE可执行文件中的一个结构类型,用于描述资源(Resource)的树形结构,其中包括了每个资源的类型(Type)、名称(Name)和语言(Language),以及指向下一
117 0
2.8 PE结构:资源表详细解析
|
存储 Windows
2.4 PE结构:节表详细解析
节表(Section Table)是Windows PE/COFF格式的可执行文件中一个非常重要的数据结构,它记录了各个代码段、数据段、资源段、重定向表等在文件中的位置和大小信息,是操作系统加载文件时根据节表来进行各个段的映射和初始化的重要依据。节表中的每个记录则被称为`IMAGE_SECTION_HEADER`,它记录了一个段的各种属性信息和在文件中的位置和大小等信息,一个文件可以由多个`IMAGE_SECTION_HEADER`构成。
142 0
2.4 PE结构:节表详细解析
|
存储 API 索引
2.9 PE结构:重建导入表结构
脱壳修复是指在进行加壳保护后的二进制程序脱壳操作后,由于加壳操作的不同,有些程序的导入表可能会受到影响,导致脱壳后程序无法正常运行。因此,需要进行修复操作,将脱壳前的导入表覆盖到脱壳后的程序中,以使程序恢复正常运行。一般情况下,导入表被分为IAT(Import Address Table,导入地址表)和INT(Import Name Table,导入名称表)两个部分,其中IAT存储着导入函数的地址,而INT存储着导入函数的名称。在脱壳修复中,一般是通过将脱壳前和脱壳后的输入表进行对比,找出IAT和INT表中不一致的地方,然后将脱壳前的输入表覆盖到脱壳后的程序中,以完成修复操作。
72 0
|
存储 安全 API
2.1 PE结构:文件映射进内存
PE结构是`Windows`系统下最常用的可执行文件格式,理解PE文件格式不仅可以理解操作系统的加载流程,还可以更好的理解操作系统对进程和内存相关的管理知识,在任何一款操作系统中,可执行程序在被装入内存之前都是以文件的形式存放在磁盘中的,在早期DOS操作系统中,是以COM文件的格式存储的,该文件格式限制了只能使用代码段,堆栈寻址也被限制在了64KB的段中,由于PC芯片的快速发展这种文件格式极大的制约了软件的发展。
601 0
|
存储 Windows
驱动开发:内核解析PE结构导出表
在笔者的上一篇文章`《驱动开发:内核特征码扫描PE代码段》`中`LyShark`带大家通过封装好的`LySharkToolsUtilKernelBase`函数实现了动态获取内核模块基址,并通过`ntimage.h`头文件中提供的系列函数解析了指定内核模块的`PE节表`参数,本章将继续延申这个话题,实现对PE文件导出表的解析任务,导出表无法动态获取,解析导出表则必须读入内核模块到内存才可继续解析,所以我们需要分两步走,首先读入内核磁盘文件到内存,然后再通过`ntimage.h`中的系列函数解析即可。
226 0
|
存储 API 数据安全/隐私保护
PE格式:导入表与IAT内存修正
关于Dump内存原理,我们可以使用调试API启动调试事件,然后再程序的OEP位置写入CC断点让其暂停在OEP位置,此时程序已经在内存解码,同时也可以获取到程序的OEP位置,转储就是将程序原封不动的读取出来并放入临时空间中,然后对空间中的节表和OEP以及内存对齐进行修正,最后将此文件在内存保存出来即可。
306 0
PE格式:导入表与IAT内存修正
|
存储 C语言 Perl
为什么要使用交叉引用?西门子S7-200 SMART的交叉引用表、字节使用表、位使用表如何操作?
本篇我们来学习西门子S7-200 SMART的交叉引用表、字节使用表、位使用表如何操作。首先我们先来看为什么要使用交叉引用:通过交叉引用窗口可以查看程序中参数赋值和存储器使用情况,避免重复赋值。
为什么要使用交叉引用?西门子S7-200 SMART的交叉引用表、字节使用表、位使用表如何操作?