重定位的处理

2.重定位PE文件的重定位块和重定位项

当可执行映像加载到的地址与其首选基地址(IMAGE_OPTIONAL_HEADER.ImageBase)不同时,重定位对于调整可执行映像中的硬编码地址是必要的。在大多数情况下,PE 文件会被映射到除 IMAGE_OPTIONAL_HEADER.ImageBase 之外的地址,因此需要对 PE 文件中的某些硬编码地址进行调整。 通过计算得出地址差值:

// The difference between the current PE image base address and its preferable base address.
    ULONG_PTR uDeltaOffset = pPeBaseAddress - pPreferableAddress;

下面是微软SDK定义的重定位块的头部结构

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
} IMAGE_BASE_RELOCATION;

并没有对BASE_RELOCATION_ENTRY做出定义,但描述为:

每个重定位条目占用2字节(WORD)

  • 每个重定位条目占用2字节(WORD)
  • 高4位是类型(Type)
  • 低12位是偏移量(Offset)

代码具体实现可以是:

typedef struct _BASE_RELOCATION_ENTRY {
	WORD	Offset	: 12; //前12字节
	WORD	Type	: 4; //后4字节
	//WORD总共占16位
} BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY;

IMAGE_BASE_RELOCATION和BASE_RELOCATION_ENTRY在PE文件中的位置关系是:

+——————————–+——————+——————+— | IMAGE_BASE_RELOCATION (8字节) | 重定位条目1 | 重定位条目2 | … | - VirtualAddress (4字节) | (2字节) | (2字节) | | - SizeOfBlock (4字节) | | | +—————————————-+—————-+———–+— ↑ ↑ pImgBaseRelocation pImgBaseRelocation + 1

于是可以通过以下代码访问BASE_RELOCATION_ENTRY:

 pBaseRelocEntry = (PBASE_RELOCATION_ENTRY)(pImgBaseRelocation + 1);

并通过下列switch语句根据预定义Type改变重定位地址:

switch (pBaseRelocEntry->Type) {
	            case IMAGE_REL_BASED_DIR64:
	                // Adjust a 64-bit field by the delta offset.
	                *((ULONG_PTR*)(pPeBaseAddress + pImgBaseRelocation->VirtualAddress + pBaseRelocEntry->Offset)) += uDeltaOffset;
	                break;
	            case IMAGE_REL_BASED_HIGHLOW:
	                // Adjust a 32-bit field by the delta offset.
	                *((DWORD*)(pPeBaseAddress + pImgBaseRelocation->VirtualAddress + pBaseRelocEntry->Offset)) += (DWORD)uDeltaOffset;
	                break;
	            case IMAGE_REL_BASED_HIGH:
	                // Adjust the high 16 bits of a 32-bit field.
	                *((WORD*)(pPeBaseAddress + pImgBaseRelocation->VirtualAddress + pBaseRelocEntry->Offset)) += HIWORD(uDeltaOffset);
	                break;
	            case IMAGE_REL_BASED_LOW:
	                // Adjust the low 16 bits of a 32-bit field.
	                *((WORD*)(pPeBaseAddress + pImgBaseRelocation->VirtualAddress + pBaseRelocEntry->Offset)) += LOWORD(uDeltaOffset);
	                break;
	            case IMAGE_REL_BASED_ABSOLUTE:
	                // No relocation is required.
	                break;
	            default:
	                // Handle unknown relocation types.
	                printf("[!] Unknown relocation type: %d | Offset: 0x%08X \n", pBaseRelocEntry->Type, pBaseRelocEntry->Offset);
	                return FALSE;
            }

总结就是:

PE(Portable Executable)文件的重定位块是为了解决PE文件在加载到内存时,实际加载地址与首选加载地址不一致的问题。当文件不能被加载到其首选基地址(IMAGE_OPTIONAL_HEADER.ImageBase)时,就需要对文件中硬编码的地址进行调整,这些调整信息就存储在重定位块中。以下详细介绍重定位块的结构和包含的信息:

重定位表的整体布局

重定位信息存储在.reloc节中,重定位表由多个重定位块(Base Relocation Block)组成。每个重定位块描述一个4KB(4096字节)的内存页面。

重定位块的结构

1. 重定位块头部(IMAGE_BASE_RELOCATION 结构)

每个重定位块以 IMAGE_BASE_RELOCATION 结构开头,该结构定义在 Windows 头文件中,其结构如下:

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD VirtualAddress; // RVA to the base address of the section this block describes.
    DWORD SizeOfBlock;    // The total size of the block, including the block header and all entries (discussed below).
} IMAGE_BASE_RELOCATION, *PIMAGE_BASE_RELOCATION;
  • VirtualAddress:这是一个相对虚拟地址(RVA),表示该重定位块所描述的内存页面的起始地址。也就是说,这个 RVA 指向需要进行重定位操作的内存页面的基地址。
  • SizeOfBlock:表示整个重定位块的大小,包括块头部(IMAGE_BASE_RELOCATION 结构本身)和后续的所有重定位条目(BASE_RELOCATION_ENTRY 结构数组)。通过这个值可以确定该重定位块在内存中占用的字节数,从而可以正确遍历块内的所有重定位条目。

2. 重定位条目(BASE_RELOCATION_ENTRY 结构数组)

重定位块头部之后紧跟着一个 BASE_RELOCATION_ENTRY 结构数组,每个结构代表一个重定位条目。BASE_RELOCATION_ENTRY 结构定义如下:

typedef struct _BASE_RELOCATION_ENTRY {
    WORD Offset : 12; // Specifies where the base relocation is to be applied.
    WORD Type : 4;    // Indicates the type of base relocation to be applied.
} BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY;
  • Offset:占用 12 位,指定了从当前重定位块的 VirtualAddress 开始的偏移量。通过这个偏移量,可以确定需要进行重定位操作的具体内存位置。例如,如果 VirtualAddress 是某个页面的起始地址,Offset 则表示在这个页面内需要调整地址的具体偏移位置。
  • Type:占用 4 位,指示了要应用的基址重定位类型。不同的重定位类型决定了如何对相应的地址进行调整。常见的重定位类型如下:
    • IMAGE_REL_BASED_DIR64:用于 64 位地址的重定位,需要将整个 64 位的地址加上基地址的偏移量。
    • IMAGE_REL_BASED_HIGHLOW:用于 32 位地址的重定位,将 32 位地址加上基地址偏移量的低 32 位。
    • IMAGE_REL_BASED_HIGH:用于调整 32 位地址的高 16 位,只需要加上基地址偏移量的高 16 位。
    • IMAGE_REL_BASED_LOW:用于调整 32 位地址的低 16 位,只需要加上基地址偏移量的低 16 位。
    • IMAGE_REL_BASED_ABSOLUTE:表示该条目不需要进行重定位,通常用于占位或预留。

重定位块的遍历过程

在进行重定位操作时,需要遍历所有的重定位块和每个块内的重定位条目。大致的遍历过程如下:

  1. 从重定位表的起始位置开始,获取第一个重定位块的头部(IMAGE_BASE_RELOCATION 结构)。
  2. 根据 VirtualAddressSizeOfBlock 信息,确定该重定位块所描述的页面和块的大小。
  3. 从块头部之后开始,依次读取每个 BASE_RELOCATION_ENTRY 结构,根据 Type 字段确定重定位类型,根据 Offset 字段确定要调整的具体内存位置,然后进行相应的地址调整。
  4. 当遍历完当前重定位块内的所有条目后,根据 SizeOfBlock 移动到下一个重定位块的头部,重复上述步骤,直到所有重定位块都被处理完毕。

通过这种方式,可以确保 PE 文件在加载到任意内存地址时,其内部的硬编码地址都能被正确调整,从而保证程序的正常运行。