【旧文章搬运】PE重定位表学习手记
原文发表于百度空间,2008-11-02
==========================================================================
先定义一下用到的几个变量:
char *hModule=NULL;//映射后的基址
PIMAGE_OPTIONAL_HEADER pOptHeader;//扩展头
PIMAGE_DATA_DIRECTORY pRelocTable=NULL;//指向重定位表
PIMAGE_BASE_RELOCATION pRelocBlock;//指向重定位块
WORD *pRelocData;//16位的重定位数据指针
PE头的定位和分析比较简单,不再多说。
首先,先判断重定位表是否存在:
pRelocTable=&(pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]);
判断pRelocTable->VirtualAddress和pRelocTable->Size是否为0即可。
若不为0,则重定位表存在。
ModulBase+pRelocTable->VirualAddress即可定位到重定位表。
重定位表的结构如下图所示:
重定位表由一个个的重定位块组成,如果重定位表存在的话,必定是至少有一个重定位块。
因为每个块只负责定位0x1000大小范围内的数据,因此如果要定位的数据范围比较大的话,
就会有多个重定位块存在。
每个块的首部是如下定义:
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
} IMAGE_BASE_RELOCATION;
把内存中需要重定位的数据按页的大小0x1000分为若干个块,而这个VirtualAddress就是每个块的起始RVA.只知道块的RVA当然还不行,我们要知道每一个需要重定位数据的具体地址。
每个需重定位的数据其地址及定位方式用两个字节来表示,记为RelocData,紧跟在IMAGE_BASE_RELOCATION结构之后,如图所示。
每个块中重定位信息的个数如何确定?
这个可由每个块结构中的Size来确定。Size的值是以DWORD表示的当前整个块的大小,先减去IMAGE_BASE_RELOCATION的大小,因为重定位数据是16位WORD的,再除以2,就得到个重定位数据的个数。由Size可以直接到达下一个重定位块,如图所示:0x5E850000+0x000000EC=0x5E8500EC即为第二个重定位块的地址,直至某个块首结构的VirtualAddress为0,表明重定位表结束。
每个块中重定位数据的个数确定了,如何得知具体每个需进行重定位的数据的地址呢?
每个16位重定位信息包括低12位的重定位位置和高4位的重定位类型。要得到重定位的RVA,IMAGE_BASE_RELOCATION'的'VirtualAddress'需要加上12位位置偏移量. 类型是下列之一:
IMAGE_REL_BASED_ABSOLUTE (0) 使块按照32位对齐,位置为0。 IMAGE_REL_BASED_HIGH (1) 高16位必须应用于偏移量所指高字16位。 IMAGE_REL_BASED_LOW (2) 低16位必须应用于偏移量所指低字16位。 IMAGE_REL_BASED_HIGHLOW (3) 全部32位应用于所有32位。. IMAGE_REL_BASED_HIGHADJ (4) 需要32位,高16位位于偏移量,低16位位于下一个偏移量数组元素,组合为一个带符号数,加上32位的一个数,然后加上8000然后把高16位保存在偏移量的16位域内。 IMAGE_REL_BASED_MIPS_JMPADDR (5) Unknown IMAGE_REL_BASED_SECTION (6) Unknown IMAGE_REL_BASED_REL32 (7) Unknown
以第一个重定位数据0x34AC为例,其高四位表明了重定位类型为3,即IMAGE_REL_BASED_HIGHLOW,Win32环境下的重定位基本都是这个类型的。
其低12位则表明了相对于VirtualAddress的RVA偏移量。VirtualAddress即需重定位的数据块的起始RVA,再加上这低12位的值就得到了具体的需要进行重定位处理的数据的RVA。感觉说得有点乱,总之就是VirtualAddress与每一个16位重定位数据一起可以得到一个具体要进行重定位处理的数据的RVA。
也就是说:
要进行重定位处理的数据的RVA=VirtualAddress+RelocData&0x0FFF
=0x00001000+(0x34AC)&0x0FFF
=0x000014AC
再加上模块基址,就得到了在内存中的真实地址了
由ModuleBase=0x5E830000,可得这个重定位数据的地址为:0x5E830000+0x000014AC=0x5E8314AC
转到OD的反汇编窗口,Ctrl+G跳到这个地址,可以看到:
OD自动标上了下划线,表明这是一个重定位数据,接下来的几个数据也可以一一进行对应.
至此,重定位算是搞明白了。
那如何进行修正呢?
把需要修正的数据减去IMAGE_OPTINAL_HEADER中的ImageBase,再加上当前加载的实际基址,就可以了。至此重定位完成!当然,也可以根据别的基址进行重定位。比如加载ntoskrnl.exe时,按照ntoskrnl.exe在内存中加载的实际基址0x804E0000(我系统上的数据)进行重定位之后,然后就可以干很多事情了~~比如查找原始SSDT,或者进行 InlineHook检测,搜索未导出函数等等,总之比较有用。
下面是我程序中的一段代码:
pRelocBlock=(PIMAGE_BASE_RELOCATION)(m_hModule + m_pRelocTable->VirtualAddress); //printf("After Loaded,Reloc Table=0x%08X\n",pRelocBlock); do { //处理一个接一个的重定位块,最后一个重定位块以RAV=0结束 //需要重定位的个数,是本块的大小减去块头的大小,结果是以DWORD表示的大小 //而重定位数据是16位的,那就得除以2 int numofReloc=(pRelocBlock->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION))/2; //printf("Reloc Data num=%d\n",numofReloc); //重定位数据是16位的 WORD offset=0; WORD *pRelocData = (WORD*)((char*)pRelocBlock + sizeof(IMAGE_BASE_RELOCATION)); for (i=0;i<numofReloc;i++)//循环,或直接判断*pData是否为0也可以作为结束标记 { DWORD *RelocAddress=0;//需要重定位的地址 //重定位的高4位是重定位类型, if (((*pRelocData)>>12) == IMAGE_REL_BASED_HIGHLOW)//判断重定位类型是否为IMAGE_REL_BASED_HIGHLOW { //计算需要进行重定位的地址 //重定位数据的低12位再加上本重定位块头的RAV即真正需要重定位的数据的RAV minioffset=(*pRelocData)&0xFFF;//小偏移 //模块基址+重定位基址+每个数据表示的小偏移量 RelocAddress=(DWORD*)(hModule + pRelocBlock->VirtualAddress+offset); //对需要重定位的数据进行修正 //修正方法:减去IMAGE_OPTINAL_HEADER中的基址,再加上实际基址即可 *RelocAddress=*RelocAddress - pOptHeader->ImageBase + hModule; } //指向下一个重定位数据 pRelocData++; } //指向下一个重定位块 pRelocBlock=(PIMAGE_BASE_RELOCATION)((char*)pRelocBlock + pRelocBlock->SizeOfBlock); }while (pRelocBlock->VirtualAddress);
这些工作只是自己加载PE过程的一部分,自己加载PE有很多用处,主要是因为加载的是原始的文件,只要不是固化挂钩,都可以自己加载然后与内存中的进行对比,用处还是不少的~
参考:
1.sudami的《SDT Restore v0.2 学习手记》。不过那个源码中用了太多自定义的结构,使得读起来稍有些困难,其实这些结构都是有现成定义的。
2.看雪论坛的《PE文件格式》