查看原文
其他

PE文件格式之重定位表

ilyzqe 看雪学院 2019-05-26

一、重定向表的作用


重定向表在网上有很多种解释,我也说一下自己对这个表的解释。程序运行的时候一般有两种方式来调用函数就是(OD的那个CALL),一个是基址+偏移,另一种就是写死的函数地址,比如CALL 0x78441354这样的,对于这样一个exe程序,它有两个dll:a.dll,b.dll。如果两个dll的基址(ImageBase)都是10000000h,并且a.dll先加载占用了这个地址,那么当b.dll加载时,就会加载到其他位置,比如12000000H的位置。


但是在b.dll中有些地址是根据ImageBase固定的,被写死了,而且是绝对地址、不是相对偏移地址。比如b.dll中存在一个call 0X01034560,这是一个绝对地址,其相对于ImageBase的地址为δ = 0X01034560 - 0X01000000 = 0X34560H;而此时的内存中b.dll存在的地址是1200000H开始的内存,加载器分配的ImageBase和b.dll中原来默认的ImageBase(1000000H)相差了200000H,因此该call的值也应该加上这个差值,被修正为0X01234560H,


那么:δ = 0X01234560H - 0X01200000H = 0X34560H则相对不变。否则call的地址不修正,会导致call指令跳转的地址不是实际要跳转的地址,获取不到正确的函数指令,程序则不能正常运行。


由于一个dll中需要修正的地址不止一两个(可能有很多),所以用一张表记录那些“写死”的地址,将来加载进内存时,可能需要一一修正,这张表称作为重定位表,一般每一个PE文件都有一个重定位表。当加载器加载程序时,如果加载器为某PE(.exe、.dll)分配的基址与其自身默认记录的ImageBase不相同,那么该程序文件加载完毕后就需要修正重定位表中的所有需要修正的地址。


如果加载器分配的基址和该程序文件中记录默认的ImageBase相同,则不需要修正,重定位表对于该dll也是没有效用的。比如test.exe和a.dll的重定位表都是不起作用的(由于一般情况.exe运行时被第一个加载,所以exe文件一般没有重定位表,但是不代表所有exe都没有重定位表)。同理,如果先加载b.dll,后加载a.dll,那么b.dll的重定位表就不起作用了。 




二、重定位表的结构解析:


重定向表位于数据目录项的第6位:

typedef struct _IMAGE_BASE_RELOCATION {

   DWORD   VirtualAddress;//RVA

   DWORD   SizeOfBlock;

} IMAGE_BASE_RELOCATION,* PIMAGE_BASE_RELOCATION;

#define IMAGE_SIZEOF_BASE_RELOCATION         8


该结构体有两个成员:一个是地址,一个是大小。如下图所示:一个重定位表由多个大小SizeOfBlock的Block组成,(不同块的SizeOfBlock大小不一)。


每一个块记录了1000H即4KB大小的内存中需要重定位信息的地址(一页大小),这些地址以VirtualAdress为该页的基址,偏移地址占两个字节(1000H最多需要12bit即可:0~FFFH)。


所以两个字节的低12位为偏移地址,而高4位就是一个标记,当此标记为0011(3)时低12为才有效,否则该2个字节可能是为了对齐而产生的,并且为对齐而产生的字节其值全为0。 


不会插入表格,贴个图,大家凑活着看,这个重定位表是通过页表来存储的信息的,而且有16个二进制位来表示,高四位是用来存储标记是否需要重定向的标记,VirtualAddress就相当于基址低12位就相当于偏移,这样就能表示出一块需要重定位的函数地址数据,如果标记是0那么就代表这个数据是用来填充保证对齐的数据无意义。




三、解析


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 

但是这个地方查资料,在吾爱上有个大牛说重定向表是多少个就是多少个并不存在结束的标记位,直至某个块首结构的VirtualAddress为 0,表明重定位表结束。这个地方没明白有点懵懂……


(链接:https://www.52pojie.cn/thread-562035-1-1.html

         


上代码:

PIMAGE_DOS_HEADER  pDosHeader = NULL;
    PIMAGE_NT_HEADERS  pNTHeader = NULL;
    PIMAGE_FILE_HEADER pFileHeader = NULL;
    PIMAGE_OPTIONAL_HEADER pOptionalHeader = NULL;
    PIMAGE_DATA_DIRECTORY pDataDirectory = NULL;
    PIMAGE_BASE_RELOCATION     pRelocation = NULL;
    PIMAGE_SECTION_HEADER   pSectionHeader = NULL;
 
    DWORD RelocationFOA = 0;
 
    LPVOID pFileBuffer  = NULL;
 
    //打开文件
    ReadPEFile(INDLLPATH,&pFileBuffer);
     
    //判断文件是否打开成功
    if (!pFileBuffer){
        printf("文件打开失败/n");
        free(pFileBuffer);
    }
 
 
    //给各个头赋值
 
    pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
    pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
    pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTHeader + 4);
    pOptionalHeader = (PIMAGE_OPTIONAL_HEADER)(((DWORD)pFileHeader) + IMAGE_SIZEOF_FILE_HEADER);
    pDataDirectory = (PIMAGE_DATA_DIRECTORY)((DWORD)pOptionalHeader + 96);
    pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
 
    for (size_t i = 0; i < 5; i++, pDataDirectory++);
     
     
    RelocationFOA = RVATOFOA(pDataDirectory->VirtualAddress, RelocationFOA, pFileBuffer);
     
     
    //定位到重定位表
    pRelocation = (PIMAGE_BASE_RELOCATION)((DWORD)pFileBuffer+RelocationFOA);
     
    BYTE secName[9] = { 0 };
    for (int i = 1; pRelocation->VirtualAddress && pRelocation->SizeOfBlock; i++)
    {
        DWORD size = (pRelocation->SizeOfBlock - 8) / 2;
        //先判断这个页属于哪个节里边 打印信息
        DWORD upper = 0;
        DWORD lower = 0;
        DWORD FOA = 0;
 
        FOA = RVATOFOA(pRelocation->VirtualAddress, FOA, pFileBuffer);
        for (size_t j = 0; j < pFileHeader->NumberOfSections; j++,pSectionHeader++)
        {
            lower = RVATOFOA(pSectionHeader->VirtualAddress, lower,pFileBuffer);
            upper = RVATOFOA(pSectionHeader->VirtualAddress + pSectionHeader->Misc.VirtualSize, upper, pFileBuffer);
            if (FOA >= lower&&FOA <= upper){
                memcpy(secName, pSectionHeader->Name, 9);   //将节的名字复制到名字空间
                break;
            }
        }
 
        //打印出所有的重定向表的所在节的名称
        printf("\t第%d个重定向表\t\t所在的节的名称%s\t\t\n",(i+1),secName);
        //定位到第一个块的首字节
        WORD* RelocaAddr = (WORD*)((BYTE*)pRelocation + 8);
        printf("=================第%d个页中所有的重定向数据============================\n",i);
        for (DWORD  t = 0; t < size ; t++)
        {
                //先判断是真正的重定向函数的基址,还是用来填充做为对齐的数据
                //确定重定向表的函数物理偏移
                //低12位是函数的偏移   物理地址=基址+偏移
            DWORD RelocaAdd = (RelocaAddr[t] & 0X0FFF) + FOA;
            WORD test = RelocaAddr[t] >> 12;
            if (test == 0){
                printf("第%d位\t\t类型[%d]\n", t + 1, test);
            }
            else
            {
                printf("\t\t第%d位\t\t类型[%d]\t\t地址[%x]\n",t+1,test,RelocaAdd);
            }
        }
        memset(secName, 0, 9);
        //进行下一页的判断
        pRelocation = (PIMAGE_BASE_RELOCATION)((BYTE*)pRelocation + pRelocation->SizeOfBlock);
         
    }
     
 
}

       


运行结果:


大概就是这个思路,如果有什么不对的地方希望前辈可以指正。



- End -


看雪ID:ilyzqe     

https://bbs.pediy.com/user-774936.htm





本文由看雪论坛 ilyzqe 原创

转载请注明来自看雪社区






热门技术文章推荐:






戳原文,看看大家都是怎么说的?


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存