PE结构-重定位表

首先,先来说一下为什么需要重定位表,先来观察一段程序

00401000 >/$  6A 00            push    0                                ; /Style = MB_OK|MB_APPLMODAL
00401002  |.  68 1D204000      push    0040201D                         ; |Title = "PE"
00401007  |.  68 10204000      push    00402010                         ; |Text = "Hello World!"
0040100C  |.  6A 00            push    0                                ; |hOwner = NULL
0040100E  |.  E8 07000000      call    <jmp.&USER32.MessageBoxA>        ; \MessageBoxA
00401013  |.  6A 00            push    0                                ; /ExitCode = 0
00401015  \.  E8 06000000      call    <jmp.&KERNEL32.ExitProcess>      ; \ExitProcess
0040101A   $- FF25 08204000    jmp     dword ptr [<&USER32.MessageBoxA>>;  user32.MessageBoxA
00401020   .- FF25 00204000    jmp     dword ptr [<&KERNEL32.ExitProces>;  kernel32.ExitProcess

假设上述程序的建议装载地址为0x400000,然而当操作系统装载时,该地址被占用,所以实际地址装载地址为0x500000。

好了,那么你会发现,上述的代码片段中,是不是有些地址需要被修正呢,如第二行代码,其中的40201D肯定就不是原来的值了,50201D才为正确的值,OK,那么下面我们来列出上述程序所有需要被修正的地址

相对虚拟地址        需修正的地址        修正后地址
1003                40201D             50201D
1008                402010             502010
101C                402008             502008
1022                402000             502000

好了,观察这些地址,我们其实可以明白为什么这些地址需要被修正了,因为这些地址在代码中是写死的,或者说在二进制层面是固定的值,而且是虚拟地址,所以一旦基址发生变化后,这些地址都需要修正。

其实重定位表中记录的其实就是上面的相对虚拟地址,我们可以根据这个地址找到需要修正的地址并对其进行修正,那么如何修正呢?这里应该也比较明显,应该为原地址+(实际装载地址-原装载地址),后面后面括号部分其实就是个差值,把这部分差值加上就可以修正了,下面来模拟一下上面修正过程,就那1003来说吧,然后原装载地址为0x400000,实际地址装载地址为0x500000

假设已从重定位表中取出1003
    1.计算修正地址VA 
        1003 + 500000 = 501003
    2.取四字节内容,因为上面地址指向的内容是需要修正的
        *(PDWORD)501003 = 40201D
    3.修正上面取出的内容(加上实际装载和原装载的偏移)
        40201D + (500000 - 400000) = 50201D
    4.写回修正后的地址
        *(PDWORD)501003 = 50201D

我们先来手工修改一下ImageBase和地址,来试一试是否可以修正成功

OD载入再次观察代码

00501000 >/$  6A 00            push    0                                ; /Style = MB_OK|MB_APPLMODAL
00501002  |.  68 1D205000      push    0050201D                         ; |Title = "PE"
00501007  |.  68 10205000      push    00502010                         ; |Text = "Hello World!"
0050100C  |.  6A 00            push    0                                ; |hOwner = NULL
0050100E  |.  E8 07000000      call    <jmp.&USER32.MessageBoxA>        ; \MessageBoxA
00501013  |.  6A 00            push    0                                ; /ExitCode = 0
00501015  \.  E8 06000000      call    <jmp.&KERNEL32.ExitProcess>      ; \ExitProcess
0050101A   $- FF25 08205000    jmp     dword ptr [<&USER32.MessageBoxA>>;  user32.MessageBoxA
00501020   .- FF25 00205000    jmp     dword ptr [<&KERNEL32.ExitProces>;  kernel32.ExitProcess

可以将这段代码与上面代码进行对比,对比后就会更清楚了。

好了明白了重定位的意义后,我们就可以来理解一下这个重定位表的结构了,也就是如何记录上述的那些地址

上面有提到过重定位表中记录的是VA,不过这个VA需要用多少字节来存储呢,这里的话我们想想,2字节,貌似不够,比如其真正地址为5FFFFF,那么其VA为FFFFF,明显已经超过2字节了,所以应该需要四字节,那么我们算一下总共大小

4 * sizeof DWORD = 16

好了,看着很小,只有16字节,不过是因为我们这个程序需要重定位的值比较少而已,一般都是大量的。那么如何可以优化呢,我们可以按页来划分,一页为0x1000,此时2字节就能存下了,那么页基址还是需要使用四字节来记录的

页基址: 1000
偏移: 3  8  1C  22
因为偏移肯定在页的范围内,所以只需要使用2字节记录即可

sizeof DWORD + 4 * sizeof WORD = 12

这里上下两差异是大不太,因为这里基数太小的原故,你可以把上面的4改成一个大一点的基数,那么差异就毕竟明显了,几乎可以说是折半了。

这里的这个结构就比较像重定位表的结构了,不过我们先再来想一个问题,一页的范围为[0,0xFFF],那么其实使用12位记录即可,2字节占16字节,所以是不是会多出来4位呢,这里的话操作系统使用高四位另做他用,起到状态位的效果

#define IMAGE_REL_BASED_ABSOLUTE              0  //无效重定位项,通常用于对齐
#define IMAGE_REL_BASED_HIGHLOW               3  //高低位修正,也就是四字节,windows 32位情况
#define IMAGE_REL_BASED_DIR64                 10 //64位情况

这里我们只需要关注上几种即可,其余的是微软考虑其他操作系统儿准备的,比如可能有些操作系统只需要修正高位或者低位等等情况。

所以当我们在取这个VA地址的时候,切记需要验证状态位,然后取低12位当做页内偏移进行处理。

好了,重点其实差不多都罗列出来了,下面可以来看这个结构了

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress; //页基址 4字节
    DWORD   SizeOfBlock;    //总共块大小
//  WORD    TypeOffset[1];  //偏移数组
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

首先对于页基址和偏移数组都好理解,那么这个块大小是什么呢?其实因为这里的偏移数组是个不确定大小的(虽然结构体里面把这个字段注释掉了,但是在内存构造中这个偏移数组就跟在后面),因为每一页中有多少需要修正并不知道,所以才有了块大小,这里的块大小也包含VirtualAddress和SizeOfBlock两个字段的大小

(SizeOfBlock - sizeof IMAGE_BASE_RELOCATION) / sizeof WORD
(SizeOfBlock - 8) / 2

所以这个块大小减去这两个字段的大小,然后除以2就是需要修正的个数了。

这里的SizeOfBlock作用二就是定位下一块的修正块,也就是该块的地址加上该块的大小,这样子就定位到了下一块的数据。

那么何时结束呢,这里就用找数据目录表了,重定位表位于数据目录的第五项,所以上面的结构我们需要从数据目录的偏移过去

#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress; //重定位块的首地址
    DWORD   Size; //总共重定位块的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

OK,下面说说如何结束,这里数据目录项的Size是有意义的,也就是全部重定位块占用的总大小,我们需要在遍历每一块重定位块的时候减去SizeOfBlock,这样子减到零说明遍历就结束了

下面来分析一个简单的DLL模块,首先,来看一下数据目录表的第五项

做一下地址转换得出,重定位块位于0xA00,总大小为0xC,这里我们可以先预测一下,一个重定位块最少需要8字节(页内无偏移修正,不过话说回来,要是没修正的话也不会存在该块..),所以可以确定重定位块数只有一块,偏移有2项((0xC-0x8)/2)

下面来看一下数据

预测还是正确的,这里的页基址为0x1000,块大小为0xC,然后计算偏移项数为2,下面来分析下这两项,基址为0x10000000

0x3036
    1.取状态 (0x3036&0xF000) >> 12
        结果为3,说明是个32位的四字节修正
    2.计算修正地址
        计算页内偏移 0x3036 & 0x0FFF = 0x36
        加上页基址   0x1000 + 0x36 = 0x1036
        加上实际基址 0x10000000 + 0x1036 = 0x10001036
    3.对该地址处的四字节内容进行修正
        *(PDWORD)0x10001036 += (实际装载地址-建议装载地址)

0x0000
    1. 同上取状态
        结果为0,说明无效重定位项,直接略过即可

供2项,遍历结束
        

数据目录项中的Size 减去 SizeOfBlock,结果为0,遍历修正结束。

OK,下面我们使用OD打开来验证一下0x10001036项的地址是否需要修正

看结果是正确的,在OD中,需要修正的地址都会有下划线的标识(也是根据重定位识别的)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章