首页 > 技术文章 > 重定位表

ShiningArmor 2019-11-08 17:48 原文

1.程序的加载
1)exe程序的加载过程
操作系统会给程序分配一个独立的4gb的虚拟空间;地址从0开始到ffffffff;
其中高2g是内核来使用的,程序无法直接读写高2g的内存,如果想操作该内存需要遵守一定的格式也就是所谓的驱动程序; 
然后像贴图一样将程序加载到这段空间;
首先将exe从ImageBase处开始将程序放入这段空间,大小为ImageSize;
一个程序是由一堆pe文件组成的;exe引用了一些dll,还需要加载dll;
    例如:找到了一个叫ker32的dll;
    操作系统会先读它的ImageBase,看到值为71B10000;然后从71B10000处开始给ker32分配空间;
当所有pe文件都贴到这段空间后,将eip的值指向exe的入口点,程序就可以运行了;
 
一般7开头的大部分都是系统提供的dll;
因为在高空间运行可以避免与其它dll冲突,省得移来移去;
 
特别说明:
    1】一般情况下,EXE都是可以按照ImageBase的地址进行加载的.因为Exe拥有自己独立的4GB 的虚拟内存空间                                        
    但DLL 不是  DLL是有EXE使用它,才加载到相关EXE的进程空间的.                                        
    2】为了提高搜索的速度,模块间地址也是要对齐的 模块地址对齐为10000H 也就是64K                                        
 
2.为何要重定向表
操作系统通常先加载exe,因此exe通常不会有地址冲突;
但dll不是,可能加载地址重复;
系统dll为了保证加载成功,在一台机器上的加载地址一般都是错开的; 
自己写的或引用的第三方dll可能有相同的ImageBase,可能造成地址冲突;
 
例如:当一个dll加载到10000000后,
    加载下一个dll,它的ImageBase还是10000000;
    但10000000已经被占用了,只能换地方加载;
    可能造成的影响是:地址换了可能找不到;
 
例如:一个程序
int main(int argc, char* argv[])
{
    printf("hello world");
}
作用是在控制台输出字符串“hello world”;
反汇编代码:
可以看到:“hello world”是字符串常量,编译器编译完后,它的地址是以绝对地址写死的方式写到exe里面;
如果是RVA方式保存,当加载的地方更换后,可以根据偏移来找到该字符串;
但用绝对地址时,当pe文件不是从ImageBase处开始存放时,找的的可能并不是“hello world”;
 
 
总结:
    1】也就是说,如果程序能够按照预定的ImageBase来加载的话,那么就不需要重定位表            
    这也是为什么exe很少有重定位表,而DLL大多都有重定位表的原因            
    2】一旦某个模块没有按照ImageBase进行加载,那么所有类似上面中的地址就都需要修正,否则,引用的地址就是无效的.            
    3】一个EXE中,需要修正的地方会很多,那我们如何来记录都有哪些地方需要修正呢?            
    答案就是重定位表            
 
3.关于重定位表
重定位表用来记录程序中因为地址冲突而需要修改的绝对地址的位置;
比如说:a.dll加载到程序中时因为其ImageBase处的地址被占用了;
    此时a.dll中使用绝对地址的地方必须要被修改;因为这种情况下根据绝对地址会寻址失败;
    因此dll文件必须提供一个重定位表,来让系统知道万一出现不按ImageBase加载时哪些地方的地址需要被修改;
 
例如:程序中有4个地址要修改,分别是8012、8023、8034、8045;
    重定位表结构中的VirtualAddress值为8000宽度4字节;
    其中具体要修改的地址的偏移记录在具体项中,每个具体项2个字节;
    例如:其中一个真实的RVA = 8000 +12;
    这样做的目的是为了节省空间,原来要4x4=16个字节的空间,现在只要4+4x2=12个字节的空间;需要修改的地方越多节省的空间越多;
   根据程序的大小不同,重定位表可能会有多个块,因为需要修改的地址可能相隔较大;
    每个重定位表只保存表示一页中需要修改的地址;
    内存中的页大小是1000H;也就是说一个具体项的值要能够取在0~1000H之间的所有值;
    1000H = 2的12次方;也就是说具体项的值至少需要12位的数据宽度;
    数据宽度以字节来表示,每个字节8位;不会以12位来表示,最合适的就是2字节即16位;
    因此具体项的值高4位是不算在RVA中;实际RVA = VirtualAddress + 具体项的低12位;
    具体项高4位的作用:如果高4位的值是3带表这个地方需要修改,否则不用管;因为有些地方可能是内存对齐用的垃圾数据;
    因此具体项使用时首先判断高四位是否是0011;
 
SizeOfBlock的作用:
    表示当前块的大小;
    当找到重定位表第一个块后,用第一个块的起始地址+当前块大小SizeOfBlock = 第二个块的起始地址;
    依次类推,找到VirtualAddress和SizeOfBlock的值都为0的块时,该块即为最后一个块;
    也可以利用SizeOfBlock来计算该块中多少个具体项:(SizeOfBlock-8)/2;
    前面两个属性各占4个字节,块总字节-8个字节为具体项总大小,每个具体项2个字节,除得到块具体项数量;
 
1)找到重定位表
数据目录的第6个结构就是重定位表;
 
数据目录:
typedef struct _IMAGE_DATA_DIRECTORY {                
    DWORD   VirtualAddress;                //内存偏移
    DWORD   Size;                //大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;        
通过 VirtualAddress可找到重定位表;
注意这里保存的是内存镜像地址RVA,如果到文件中找需要转成文件镜像地址FOA;
 
重定位表结构:
typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION ,* PIMAGE_BASE_RELOCATION;
 
2)重定位表解析
 
1、通过IMAGE_DATA_DIRECTORY结构的VirtualAddress                
属性 找到第一个IMAGE_BASE_RELOCATION                
                
2、判断一共有几块数据:                
最后一个结构的VirtualAddress与SizeOfBlock都为0                
                
3、具体项 宽度:2字节                
也就是这个数据                
内存中的页大小是1000H 也就是说2的12次方 就可以表示                
一个页内所有的偏移地址 具体项的宽度是16字节 高四位                
代表类型:值为3 代表的是需要修改的数据 值为0代表的是                
用于数据对齐的数据,可以不用修改.也就是说 我们只关注                
高4位的值为3的就可以了.                
                
4、VirtualAddress 宽度:4字节                
当前这一个块的数据,每一个低12位的值+VirtualAddress 才是                
真正需要修复的数据的RVA                
真正的RVA = VirtualAddress + 具体项的低12位                
                
5、SizeOfBlock 宽度:4字节                
当前块的总大小                
具体项的数量 = (SizeOfBlock - 8)/2     
 
 
 

推荐阅读