PE格式是Windows环境下可执行文件(如:exe,dll)的格式,而Windows下面的程序,例如动态链接库无法加载到它本身期望加载的地址的时候,便会发生重定位。那么,重定位是如何实现的呢?
一.PE文件格式的结构
PE文件主要是由PE头和PE体组成的,其中,PE头主要由DOS头,DOS存根,NT头:签名,NT头:文件头,NT头:可选头,节头组成,而剩下的各节区组成PE体。其结构大致如下图所示:
二.重定位表的地址及实现
当程序发生重定位对相关代码进行修改的时候,系统必须知道哪里的代码需要修改,这时候就需要一种方法能够快速地确定哪里需要修改。最简单的方法莫过于提供一张重定位表来指示需要修改的地址和值。
其中,值可以不提供,因为可以利用下面的式子计算出来:
Val= ValOld – ExpectedBase + ActualBase
所以,关键的地方在于确定哪里需要修改。通过PE结构的相关资料可以知道,PE结构的程序在编译的时候就已经提供了一张重定位表。在WinNt.h中,定义了NT头:可选头的类型——结构体IMAGE_OPTIONAL_HEADER32如下所示:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;</p>
在这个结构体的最后一部份,是一个由16个IMAGE_DATA_DIRECTORY(见上表)结构组成的数组,其对应的内容大致如下:
DataDirectory[0] EXPORT Directory
DataDirectory[1] IMPORT Directory
DataDirectory[2] RESOURCE Directory
DataDirectory[3] EXCEPTION Directory
DataDirectory[4] SECURITY Directory
DataDirectory[5] BASERELOC Directory
DataDirectory[6] DEBUG Directory
DataDirectory[7] COPYRIGHT Directory
DataDirectory[8] GLOBALPTR Directory
DataDirectory[9] TLS Directory
DataDirectory[10] LOAD_CONFIG Directory
DataDirectory[11] BOUND_IMPORT Directory
DataDirectory[12] IAT Directory
DataDirectory[13] DELAY_IMPORT Directory
DataDirectory[14] COM_DESCRIPTOR Directory
DataDirectory[15] RESERVED Directory
根据相关文档得知,我要找的重定位表的地址就在这个数组的第6个元素中(数组索引为5,红色部分)。可是,该如何验证,它就是我所要找的重定位表呢?
以下面的程序为例:
// TestRelocation.cpp
#include <cstdio>
int a;
int main()
{
a = 0;
a++;
printf("a = %d\n", a);
printf("&a = 0x%x\n", &a);
return 0;
}
将该工程进行编译,生成TestRelocation.exe,使用HxD-十六进制编辑器打开,可以看到如下所示的相关代码
为了更直观的查看PE文件的相关信息,我再使用PEView载入TestRelocation.exe,如下所示:
选择IMAGE_DATA_DIRECTORY项,可以看到DataDirectory数组的相关内容:
前面说过,IMAGE_OPTIONAL_HEADER中的DataDirectory是一个由IMGE_DATA_DIRECTORY结构组成的数组,因此可以得出,0x0001B000这个地址指向了重定位表的地址,而下面的0x0000035C则是重定位表的大小。我在HxD中转到0x0001B000,却发觉这个地址是不存在的,如下图所示:
由上图可以看到,十六进制文件在0x000079FF处就结束了,根本不存在0x0001B000这个远大于0x000079FF的文件地址!难道是前面的分析错了吗?的确,前面的分析出了问题,0x0001B000这个地址并不是重定位表的文件地址,而是重定位表在内存中的虚拟地址,即当程序执行时,文件映射到内存中的位置。那么,重定位表在文件中的真实地址是多少呢?
前面将PE结构的时候说过,在NT头:可选头之后,还有节头,如下图所示。而我需要的信息就隐藏在这里面。
每一个节头,也是一个结构体,在WinNt.h中,其被定义为IMAGE_SECTION_HEADER,相关信息如下:
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
<span style="color:#ff0000;">DWORD VirtualAddress;</span>
<span style="color:#ff0000;">DWORD SizeOfRawData;</span>
<span style="color:#ff0000;"> DWORD PointerToRawData;</span>
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
在这里,我只关心红色显示的部分VirtualAddress,SizeOfRawData和PointerToRawData三部分。其中,VirtualAddress表示节头所对应的节映射到内存中的地址,SizeOfRawData表示对应的节的文件大小,PointerToRawData表示对应的节在文件中的地址。下面直接给出RVA与RAW的转换公式:
RAW = RVA – VirtualAddress + PointerToRawData
由此,可以计算出重定位表在文件中的地址为0x00007400,属于.reloc节(其实,.reloc节就是VC++中生成的重定位节区)。在PEView中转到.reloc节区,如下所示:
由PEView提供的信息可以看到,这张表里有一个地址0x00011000,大小0x000000F0,下面跟着一段数据。对整张表进行分析,可以看到整张表是由多个这样的结构组成的。
在WinNt.h中,相关的结构体IMAGE_BASE_RELOCATION义如下:
// Based relocation format.
//
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
// WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
//
// Based relocation types.
//
#define IMAGE_REL_BASED_ABSOLUTE 0
#define IMAGE_REL_BASED_HIGH 1
#define IMAGE_REL_BASED_LOW 2
#define IMAGE_REL_BASED_HIGHLOW 3
#define IMAGE_REL_BASED_HIGHADJ 4
#define IMAGE_REL_BASED_MIPS_JMPADDR 5
#define IMAGE_REL_BASED_MIPS_JMPADDR16 9
#define IMAGE_REL_BASED_IA64_IMM64 9
#define IMAGE_REL_BASED_DIR64 10
由该结构可以知道,VirtualAdress指向一个基准地址,SizeOfBlock指出后面的块的中大小,TypeOffset提供了两部分的信息,高四位指向类型,低12位的值是一个偏移地址。
前面的截图中所对应的内容转换过来就是0x000113E0,0x000113E9,0x000113F1,0x000113F8处的内容需要重定位。
下面对次进行验证,使用OD载入TestRelocation.exe,如下图所示:
可以看出,表中的每一个地址都指向一个需要进行重定位处理的地址。同理,可以对重定位表中剩下的内容进行验证,重定位表中所指向的地址均需要进行重定位操作(注:重定位表中的地址均是指虚拟地址,而我进行实验时,程序被加载到的地址是0x00DA0000,如下所示,故上图中4个需要重定位的地址转化后应该为0x00DB13E0,0x00DB13E9,0x00DB13F1,0xDB13F8)。
通过以上的分析,我就确定了PE文件中重定位表的地址以及是如何通过重定位表来对相关代码进行重定位。
三.重定位表的作用
加载动态链接库的时候,如果一个动态链接库期望加载的地址已经加载了别的模块,这时候就会发生重定位。由上面可以知道,VC++生成的PE格式文件的重定位表在.reloc节,那么,如果我把.reloc节给删除掉,那是否相应的动态链接库就只能加载到它期望加载的地址,一旦改地址被占用,就会加载失败?
以程序(MyDll)为例。
#include <Windows.h>
#include "MyDll.h"
#include <cstdio>
int a;
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD dwReason, LPVOID lpReserved)
{
switch (dwReason)
{
case DLL_PROCESS_ATTACH:
a = 0;
break;
}
return TRUE;
}
void change()
{
_asm
{
push eax
mov eax, a
inc eax
mov a, eax
pop eax
}
printf("&a = %x\na = %d\n", &a, a);
}
#pragma once
#define MYLIB extern "C" __declspec(dllexport)
MYLIB void change();
#include <Windows.h>
#include <iostream>
int main()
{
using namespace std;
typedef void(*pfChange)();
// 载入第一个DLL
HMODULE hDll = LoadLibrary(TEXT("MyDll.dll"));
if (hDll == NULL)
{
cout << "1: error of LoadLibrary\n";
DWORD dwError = GetLastError();
cout << dwError;
return 0;
}
pfChange pFunc = (pfChange)GetProcAddress(hDll, "change");
if (pFunc == NULL)
{
cout << "1: error of GetProcAddress\n";
DWORD dwError = GetLastError();
cout << dwError;
FreeLibrary(hDll);
return 0;
}
pFunc();
// 载入第二个DLL
HMODULE hDll2 = LoadLibrary(TEXT("MyDll2.dll"));
if (hDll2 == NULL)
{
cout << "2: error of LoadLibrary\n";
DWORD dwError = GetLastError();
cout << dwError;
return 0;
}
pfChange pFunc2 = (pfChange)GetProcAddress(hDll2, "change");
if (pFunc2 == NULL)
{
cout << "2: error of GetProcAddress\n";
DWORD dwError = GetLastError();
cout << dwError;
FreeLibrary(hDll2);
return 0;
}
pFunc2();
FreeLibrary(hDll);
FreeLibrary(hDll2);
system("pause");
return 0;
}
设定MyDll.dll的基址为0x00600000,如下所示:
然后生成相应的动态链接库MyDll.dll,复制MyDll.dll并重命名为MyDll2.h,如下所示:
这样做,保证了TestMyDll.exe用函数LoadLibrary()加载MyDll2.dll必定要发生重定位,且易于观察,否则,应该无法加载MyDll2.dll,生成TestMyDll.exe,并且运行。运行结果如下:
跟踪调试,可以发现确实发生了重定位,如下所示:
现在来对MyDll2.dll进行修改,删除其.reloc节。具体步骤如下:
1. 删除.reloc节
使用PEView载入MyDll2.dll,确定.reloc节的位置,如下所示:
使用HxD载入MyDll2.dll,将从0x00006E00位置开始的所有内容都删除掉,如下所示:
删除后如下:
2. 修改其余信息
首先,在NT头:文件头中,有一项记录了PE文件中节区的总数,因为我删除了.reloc节区,所以必须对其进行修改,如下所示:
在HxD中找到相应的地址,如下所示:
修改后的结果如下所示:
接着,还应该修改文件在内存中的映射大小,如下所示:
由相应的节头可以得出,.reloc节在内存中所占大小,如下所示:
根据NT头:可选头中的SectionAlignment可以知道,.reloc节在内存中拓展后的大小应该为1000,将Size of Image减去1000,结果如下:
至此,修改完成,下面进行验证。
具体步骤如下:
一.先将TestMyDll.cpp中载入第一个动态链接库的代码注释掉,使其只载入MyDll2.dll,编译运行,结果如下:
可见,修改后的MyDll2.dll如果加载到imageBase的话,是能够正常运行的。
二. 将TestMyDll.cpp的注释去掉,使其在加载MyDll.dll之后,再加载MyDll2.dll,编译运行,结果如下:\
可以看到,MyDll2.dll无法载入,错误代号998,用VS自带的错误查找查询,结果如下:
综上所述,可以看出重定位表的重要作用。一个模块一旦无法重定位,那么如果期望加载的地址已经加载了其它模块,那么它就无法再加载了。
四.验证重定位表的作用
这次,我通过自己构造一段数据,并通过重定位表来对它进行重定位,以此来验证重定位表的作用。我仍然以程序(详见工程:MyDll)为例。
生成MyDll.dll之后,复制一份,并将其重命名为MyDll2.dll,我来修改MyDll2.dll。如下所示:
使用PEView载入MyDll2.dll,先来确定我要在哪写入我自己的数据,以及要写入的数据是什么样的。首先,理论上在哪里构造我的数据都是可以的(只要不影响原先的指令即可),其次,因为重定位是对一个地址重定位,所以在32位环境下,构造的数据可以是由4个字节组成。
为了简便,我直接在.text节的末尾加上我要构造的数据,如下所示:
因此,构造的地址为从0x00003050开始的四个字节,构造的数据不妨为0x12345678,修改后的结果如下所示:
为了能够看到这样修改后的结构,先来确定0x00003050处的内容会被加载到内存中的什么位置。由.text节头的相关信息可以计算出,0x00003050加载到内存后的RVA为0x00013C50。运行VS,在pFunc2函数处设置断点,如下所示:
按F5直接进行调试,查看此时的内存窗口,由于我的MyDll2.dll被加载到地址0x00A6000处,如下所示:
此时我需定位到的地址应该为0x00A73C50,如下所示:
我之前的修改成功了,这样子,我需要测试重定位的内容就好了。接下来,我来修改重定位表的内容。
1. 添加对0x00003050处的重定位的相关信息
先由NT头:可选头和节头中的相关信息定位到重定位表的地址,如下所示:
接着我找到重定位表的末尾,如下所示:
接着修改其内容,由重定位表的结构可以知道,我们要添加的内容有3部分,分为一个4字节的基地址,一个4个字节的块大小,一个包含了重定位类型和偏移地址的2字节内容。由于我要重定位的数据块的地址为0x00013C50,所以要填充的内容应该为00 30 01 00 0A 00 00 00 50 3C ,修改后的结果如下:
2. 修改NT头:节头中重定位表大小的信息
直接00000330加上我们添加了的数据的大小即可,修改后应该为0000033A,如下所示:
至此,修改完成。接下来进行验证,我们所添加的内容是否有效。
先使用PEView打开Mydll2.dll,观察其内容:
接着,重新使用vs运行TestMyDll.exe,在pFunc2()处停下来后,先查看一下MyDll2.dll被加载到哪里,如下所示:
这次被加载到的地址为0x009F0000,因此要查看的内存地址因该为0x00A03C50,在查看之前,先来计算如果发生重定位,它的值应该为多少。计算如下:
val = 12345678 – 600000 + 9F0000 = 12735678
接下来,转到内存0x00A03C50处查看,如下所示:
可以看到,结果与猜想一模一样,因此也验证了重定位表的作用——重定位表可以根据程序实际加载的地址,快速地对相关代码中有关地址的信息进行修改,甚至可以自己构造重定位的信息,并通过重定位来修改相关的指令。
五.总结
通过以上的分析学习,我也初步地了解了PE文件格式以及重定位表,但是还有很多不是很了解的地方,需要一一学习以及验证。