这一章我们继续来了解数据目录,我们来了解数据目录的第6个结构:重定位表。
一.进程4GB空间
在了解重定位表之前,我们先来了解进程4GB空间,这有助于我们理解为什么PE中需要重定位表。
当我们双击一个exe程序时,操作系统会为它分配虚拟的4GB空间(注意这里是虚拟的4GB空间,不同于物理4GB空间,如果是真实的4GB空间的话,我们的机器运行不了几个程序就会挂掉)。
那么操作系统是如何分配这4GB的空间的呢?我们来简单解析一下:
首先我们给出一张图,帮助我们理解:
1.低2G为用户空间,前后64K不会被分配,0-FFFF用于做各种无效检查(空指针就指向这里)
2.后64K :7FFF0000 - 8000000 用来做内核的交互。在这一部分,会加载exe和dll,通常情况下,exe会调用dll文件,而dll文件会调用其他的dll文件。
因为打开的是该exe进程,则这块空间最先由 exe 占第一块空间。所以一般情况下,exe都是可以按照 ImageBase 的地址进行加载的,因为exe有自己独立的4GB虚拟空间。而 ImageBase 通常是 400000H。但dll不是 dll是有exe或其他 dll使用它,它才加载到相关EXE的进程空间的。
3.高2G空间是内核使用的。
4.为了提高搜索速度,模块之间也是要对齐的,模块地址对齐为1000h,也就是64k。
二.为什么要有重定位表
我们打开一个exe程序,观察它的反汇编:
这里我们查看的时printf函数的地址,通过前面章节的学习,我们知道E8是call的硬编码,E8后面跟着的四字节数据就是函数地址,我们会发现:这个函数地址是直接被编译器写入文件的,那么我们来做一个假设:dll每次被加载的基址不同,如果程序继续通过这个地址去找函数的话,那不是就出错了吗?
这时候重定位表的作用就体现出来了:重定位表记录下文件中需要改变的地址的位置,程序在运行的时候,通过重定位表修改函数地址和其他需要修改的值,以保证程序正常运行。
也就是说,如果程序每次都按照ImageBase来加载的话,那么就不需要改变函数地址,也就不需要重定位表了,这个是为什么exe中很少有重定位表(因为exe有自己独立的4GB空间,exe在每次加载的时候通常可以按照预定的IamgeBase来加载)的原因。
如果某个模块没有按照预定的ImageBase加载,那么像函数地址这些都需要修改,而重定位表记录这些需要修改的数据。
三.重定位表
1.如何定位重定位表:
前一章我们已经提及数据目录,数据目录的第6个结构,就记录了重定位表的地址。
2.重定位表的结构:
我们来看看重定位表的结构:
typedef struct _IMAGE_BASE_RELOCATION{ DWORD VirtualAddress;//重定位内存页的起始RVA DWORD SizeOfBlock;//重定位块的大小 } IMAGE_BASE_RELOCATION; typedef IMAGE_BASE_RELOCATION UNALIGNED *PIMAGE_BASE_RELOCATION;
需要注意的是,如果要在FileBufer中找到重定位表,需要将数据目录第6个结构中的VirtualAddress转换为FOA。
在重定位内存页中,有若干个上述IMAGE_BASE_RELCOATION结构,而在每个结构后,有一大块数据,就记录了需要修改的地址,被称为具体项。
我们给出图示来帮助我们了解:
解释说明:
1.通过IMAGE_DATA_DIRECTORY结构的第6个结构的VirtualAddress属性,找到第一个IMAGE_BASE_RELOCATION;
2.最后一个块:最后一块的VirtualAddress和SizeOfBlock的值都为0
3.具体项:
内存页中的大小为1000h,也就是说,用2的12次方就可以表示一个内存页中所有的地址偏移
具体项的宽度为16位,用低12位表示需要修改的数据的地址,高3位代表类型,高三位总是为011或000,011表示该项需要修改,000表示该项不需要修改,我们只需要关注高三位为011的就可以了
4.VirtualAddress:宽度为4字节
具体项中的地址为偏移,加上VirtualAddress才是真正要修改的地址
5.SizeOfBlock:当前块的总大小具体项的数量=(SizeOfBlock的值-8)/2
四.作业
根据以上对重定位表的介绍,我们可以打印出重定位表的信息(不需要打印具体项,但是要打印出具体项的数量)
这里给出程序源码:
main.c
#include <stdio.h> #include <windows.h> #include "PEFunction.h" int main(int argc, char** argv) { char filename[50]; char* FileBuffer=NULL; printf("*******************************打印PE重定位表程序*******************************\n"); printf("---------------------------版权所有:https://blog.csdn.net/qq_73985089?type=blog\n"); printf("----------------------------------------------------------如需引用,请告知原作者!"); printf("请输入您需要按操作的文件路径:"); scanf("%s",filename); FileBuffer=ReadToFileBuffer(filename); ShowBaseRelocation(FileBuffer); free(FileBuffer); return 0; }
PEFunction.h
/********1.打开文件函数(将文件二进制读取到FileBuffer) *******/ /*函数说明: 该函数需要一个char类型的指针,指向想要打开的文件路径 该函数返回一个char类型的指针,指向FileBuffer 该函数完成文件的打开,并且将文件的二进制读取到FIleBuffer 该函数读取完成后会将文件关闭,避免出现误操作 该函数会通过移动文件指针获取文件大小 获取的文件大小用于动态申请内存 该函数会动态申请内存,用于存放FileBuffer */ char* ReadToFileBuffer(char* filename); /**********************2.获取PE导出表数据函数******************** 该函数需要一个指针,指向FileBuffer 该函数在找到导出表之前,需要定义DOS结构指针,标准PE头结构指针和可选PE头指针,以方便找到导出表 该函数会在控制台输出函数地址表,函数名称表和函数导出序号表 */ void ShowExportDirectory(char* FileBuffer); /***********************3.RVA转换为FOA函数************************* 该函数需要一个指针,指向FileBuffer 该函数需要一个整数,为要转换的RVA 该函数返回一个整数,为转换后的FOA地址*/ int RVAToFOA(IN LPVOID pFileBuffer, IN DWORD dwRva); /***********************4.根据函数名称查找函数地址函数*********************** 该函数需要一个指针,指向要打开的文件路径 该函数返回一个整数,指向得到的函数地址 */ void GetAddressOfFunctionByName(char* filename); /***********************5.根据函数序号查找函数地址函数*********************** 该函数需要一个指针,指向要打开的文件路径 该函数返回一个整数,指向得到的函数地址 */ void GetAddressOfFunctionByOrdinal(char* filename); /***********************6.打印PE重定位表函数*********************** 该函数需要一个指针,指向FIleBuffer 该函数会在控制台打印出重定位表信息(重定位表偏移,重定位表大小,但不打印重定位表具体项*/ void ShowBaseRelocation(char* FileBuffer);
Function.c
/**************************************************************/ //*********************自编PE函数库****************************/ #include <stdio.h> #include <windows.h> /********1.打开文件函数(将文件二进制读取到FileBuffer) *******/ /*函数说明: 该函数需要一个char类型的指针,指向想要打开的文件路径 该函数返回一个char类型的指针,指向FileBuffer 该函数完成文件的打开,并且将文件的二进制读取到FIleBuffer 该函数读取完成后会将文件关闭,避免出现误操作 该函数会通过移动文件指针获取文件大小 获取的文件大小用于动态申请内存 该函数会动态申请内存,用于存放FileBuffer */ char* ReadToFileBuffer(char* filename){ FILE* fp; char* FileBuffer=NULL; int length; if((fp=fopen(filename,"rb"))==NULL){ printf("%s打开失败!",filename); exit(0); }else{ printf("文件打开成功,正在计算文件大小...\n"); } fseek(fp,0,SEEK_END); length=ftell(fp); fseek(fp,0,SEEK_SET); printf("文件大小:%d字节\n正在申请FileBuffer内存...\n",length); FileBuffer=(char*)malloc(length); if(FileBuffer!=NULL){ printf("FileBuffer内存申请成功。准备向FileBuffer写入数据...\n"); } if((fread(FileBuffer,length,1,fp))==0){ printf("写入数据失败!\n"); exit(0); }else{ printf("写入数据成功。\n"); } if(fclose(fp)){ printf("文件关闭失败!\n"); exit(0); }else{ printf("文件关闭成功。\n"); } return FileBuffer; } /**********************2.获取PE导出表数据函数******************** 该函数需要一个指针,指向FileBuffer 该函数在找到导出表之前,需要定义DOS结构指针,标准PE头结构指针和可选PE头指针,以方便找到导出表 该函数会在控制台输出函数地址表,函数名称表和函数导出序号表 */ void ShowExportDirectory(char* FileBuffer){ IMAGE_DOS_HEADER* pDosHeader=NULL; IMAGE_FILE_HEADER* pFileHeader=NULL; IMAGE_OPTIONAL_HEADER32* pOptionalHeader=NULL; IMAGE_EXPORT_DIRECTORY* pExportDirectory=NULL; pDosHeader=(IMAGE_DOS_HEADER*)FileBuffer; if(*((PDWORD)((DWORD)FileBuffer + pDosHeader->e_lfanew))!= IMAGE_NT_SIGNATURE){ printf("不是有效的PE标志!\n"); }else{ printf("检测到有效的PE标志。\n"); } pFileHeader=(IMAGE_FILE_HEADER*)(FileBuffer+pDosHeader->e_lfanew+4); pOptionalHeader=(IMAGE_OPTIONAL_HEADER32*)((DWORD)pFileHeader+20); pExportDirectory = (IMAGE_EXPORT_DIRECTORY*)(FileBuffer+RVAToFOA(FileBuffer,pOptionalHeader->DataDirectory[0].VirtualAddress)); printf("导出表RVA:%x\n",pOptionalHeader->DataDirectory[0].VirtualAddress); printf("导出表大小:%x字节\n",pOptionalHeader->DataDirectory[0].Size); printf("导出表FOA:%d\n",RVAToFOA(FileBuffer,pOptionalHeader->DataDirectory[0].VirtualAddress)); printf("*******************************导出表*********************************\n"); printf("TimeDataStamp(经加密):%d\n",pExportDirectory->TimeDateStamp); printf("Name(导出表文件名字符串):%s\n",FileBuffer+RVAToFOA(FileBuffer,pExportDirectory->Name)); printf("Base(函数起始序号):%d\n",pExportDirectory->Base); printf("NumberOfFunction(导出函数总数):%d\n",pExportDirectory->NumberOfFunctions); printf("NumberOfNames(以名称导出函数的总数):%d\n",pExportDirectory->NumberOfNames); printf("*****************************函数地址表********************************\n"); int i=0; char* AddressOfFunction; AddressOfFunction=(char*)(FileBuffer+RVAToFOA(FileBuffer,pExportDirectory->AddressOfFunctions)); for(i=0;i<pExportDirectory->NumberOfFunctions;i++){ printf("下标:%d 函数地址:0x%x\n",i,*(PWORD)((DWORD)AddressOfFunction+i*4)); } printf("*****************************函数名称表********************************\n"); int* AddressOfNames=NULL; char* name=NULL; AddressOfNames=(int*)(FileBuffer+RVAToFOA(FileBuffer,pExportDirectory->AddressOfNames)); for(i=0;i<pExportDirectory->NumberOfNames;i++){ name=(char*)(FileBuffer+RVAToFOA(FileBuffer,*AddressOfNames)); printf("下标:%d 函数名称:%s\n",i,name); AddressOfNames++; } printf("*******************************函数序号表*******************************\n"); char *AddressOfNameOrdinals=NULL; char *Ordinal=NULL; AddressOfNameOrdinals=(int*)(FileBuffer+RVAToFOA(FileBuffer,pExportDirectory->AddressOfNameOrdinals)); for(i=0;i<pExportDirectory->NumberOfNames;i++){ Ordinal=(int*)(FileBuffer+RVAToFOA(FileBuffer,*AddressOfNameOrdinals)); printf("下标:%d 函数序号(加Base):%d\n",i,*(AddressOfNameOrdinals+i*2)+pExportDirectory->Base); } } /***********************3.RVA转换为FOA函数************************* 该函数需要一个指针,指向FileBuffer 该函数需要一个整数,为要转换的RVA 该函数返回一个整数,为转换后的FOA地址*/ int RVAToFOA(IN LPVOID pFileBuffer, IN DWORD dwRva) { PIMAGE_DOS_HEADER pDosHeader = NULL; PIMAGE_FILE_HEADER pFileHeader = NULL; PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL; PIMAGE_SECTION_HEADER pSectionHeader = NULL; PIMAGE_SECTION_HEADER pNextSectionHeader = NULL; //DOS头 pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer; // 强转 DOS_HEADER 结构体指针 //PE头 pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pFileBuffer + pDosHeader->e_lfanew + 4); //NT头地址 + 4 为 FileHeader 首址 //可选PE头 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);//SIZEOF_FILE_HEADER为固定值且不存在于PE文件字段中 if (dwRva < pOptionalHeader->SizeOfHeaders) //偏移小于头的大小,内存偏移则为文件偏移 { return dwRva; } //首个节表 pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader); //下一个节表 pNextSectionHeader = pSectionHeader + 1; //循环遍历节表 int i; for (i = 1; i < pFileHeader->NumberOfSections; i++, pSectionHeader++, pNextSectionHeader++)//注意这里i从1开始 i < NumberOfSections { //注意这里的pSectionHeader已经是加了基址的,不是偏移, 是绝对地址。而dwRva是偏移地址 if (dwRva >= pSectionHeader->VirtualAddress && dwRva < pNextSectionHeader->VirtualAddress)//大于当前节的内存偏移而小于下一节的内存偏移 { //则dwRva属于当前节,则dwRva - VirtualAddress为dwRva基于当前节的偏移。此偏移加上当前节的文件起始偏移地址 则为dwRva在文件中的偏移 DWORD PointerToRawData = pSectionHeader->PointerToRawData; DWORD VirtualAddress = pSectionHeader->VirtualAddress; DWORD aa = pSectionHeader->PointerToRawData + dwRva - pSectionHeader->VirtualAddress; return pSectionHeader->PointerToRawData + dwRva - pSectionHeader->VirtualAddress ; } } //出循环后pSectionHeader指向最后一个节表 //大于当前节(最后一节)的内存偏移且小于内存映射大小 if (dwRva >= pSectionHeader->VirtualAddress && dwRva < pOptionalHeader->SizeOfImage) { //同上return return pSectionHeader->PointerToRawData + dwRva - pSectionHeader->VirtualAddress; } else //大于内存映射大小 { printf("dwRva大于内存映射大小\n"); return -1; } } /***********************4.根据函数名称查找函数地址函数*********************** 该函数需要一个指针,指向要打开的文件路径 该函数返回一个整数,指向得到的函数地址 */ void GetAddressOfFunctionByName(char* filename){ char* FileBuffer=NULL; char inname[5]; int i; FileBuffer=ReadToFileBuffer(filename); IMAGE_DOS_HEADER* pDosHeader=NULL; IMAGE_FILE_HEADER* pFileHeader=NULL; IMAGE_OPTIONAL_HEADER32* pOptionalHeader=NULL; IMAGE_EXPORT_DIRECTORY* pExportDirectory=NULL; pDosHeader=(IMAGE_DOS_HEADER*)FileBuffer; if(*(PWORD)((DWORD)FileBuffer+pDosHeader->e_lfanew)==IMAGE_NT_SIGNATURE){ printf("检测到有效的PE标志。\n"); }else{ printf("未检测到有效的PE标志!\n"); exit(0); } pFileHeader=(IMAGE_FILE_HEADER*)((DWORD)FileBuffer+pDosHeader->e_lfanew+4); pOptionalHeader=(IMAGE_OPTIONAL_HEADER32*)((DWORD)pFileHeader+20); pExportDirectory = (IMAGE_EXPORT_DIRECTORY*)(FileBuffer+RVAToFOA(FileBuffer,pOptionalHeader->DataDirectory[0].VirtualAddress)); int* AddressOfNames=NULL; char* name=NULL; AddressOfNames=(int*)(FileBuffer+RVAToFOA(FileBuffer,pExportDirectory->AddressOfNames)); printf("请输入您需要查找的函数名:"); scanf("%s",inname); printf("正在查找中...\n"); for(i=0;i<pExportDirectory->NumberOfNames;i++){ name=(int*)(FileBuffer+RVAToFOA(FileBuffer,RVAToFOA(FileBuffer,*AddressOfNames))); if(!(strcmp("Plus",name))){ printf("下标为%d的函数名符合。\n",i); break; } AddressOfNames++; } char* Ordinal=NULL; Ordinal=(char*)(FileBuffer+RVAToFOA(FileBuffer,pExportDirectory->AddressOfNameOrdinals)+i*2); char* AddressOfFunction=NULL; AddressOfFunction=(char*)(FileBuffer+RVAToFOA(FileBuffer,pExportDirectory->AddressOfFunctions)+*Ordinal*4); printf("Ordinal(函数导出序号(未加Base)):%d AddressOfFunction:0x%x",*Ordinal,*(PWORD)((DWORD)AddressOfFunction)); } /***********************5.根据函数序号查找函数地址函数*********************** 该函数需要一个指针,指向要打开的文件路径 该函数返回一个整数,指向得到的函数地址 */ void GetAddressOfFunctionByOrdinal(char* filename){ char* FileBuffer=NULL; int InOrdinal=0; IMAGE_DOS_HEADER* pDosHeader=NULL; IMAGE_FILE_HEADER* pFileHeader=NULL; IMAGE_OPTIONAL_HEADER32* pOptionalHeader=NULL; IMAGE_EXPORT_DIRECTORY* pExportDirectory=NULL; FileBuffer=ReadToFileBuffer(filename); char* AddressOfFunction; pDosHeader=(IMAGE_DOS_HEADER*)FileBuffer; if(*(PWORD)((DWORD)FileBuffer+pDosHeader->e_lfanew)==IMAGE_NT_SIGNATURE){ printf("检测到PE标志。"); }else{ printf("未检测到PE标志!\n"); exit(0); } pFileHeader=(IMAGE_FILE_HEADER*)(FileBuffer+pDosHeader->e_lfanew+4); pOptionalHeader=(IMAGE_OPTIONAL_HEADER32*)((DWORD)pFileHeader+20); pExportDirectory = (IMAGE_EXPORT_DIRECTORY*)(FileBuffer+RVAToFOA(FileBuffer,pOptionalHeader->DataDirectory[0].VirtualAddress)); printf("请输入您需要查找的函数导出序号:"); scanf("%d",&InOrdinal); InOrdinal=InOrdinal-pExportDirectory->Base; AddressOfFunction=(char*)FileBuffer+RVAToFOA(FileBuffer,pExportDirectory->AddressOfFunctions+InOrdinal*4); printf("函数导出序号(未加Base):%d 函数地址:0x%x",InOrdinal,*(PWORD)((DWORD)AddressOfFunction)); } /***********************6.打印PE重定位表函数*********************** 该函数需要一个指针,指向FIleBuffer 该函数会在控制台打印出重定位表信息(重定位表偏移,重定位表大小,但不打印重定位表具体项*/ void ShowBaseRelocation(char* FileBuffer){ int i=1; IMAGE_DOS_HEADER* pDosHeader=NULL; IMAGE_OPTIONAL_HEADER32* pOptionalHeader=NULL; IMAGE_BASE_RELOCATION* pBaseRelocation=NULL; pDosHeader=(IMAGE_DOS_HEADER*)FileBuffer; if(*(PWORD)((DWORD)FileBuffer+pDosHeader->e_lfanew)==IMAGE_NT_SIGNATURE){ printf("检测到有效的PE标志。\n"); }else{ printf("未检测到有效的PE标志!\n"); exit(0); } pOptionalHeader=(IMAGE_OPTIONAL_HEADER32*)((DWORD)FileBuffer+pDosHeader->e_lfanew+24); pBaseRelocation=(IMAGE_BASE_RELOCATION*)((DWORD)FileBuffer+RVAToFOA(FileBuffer,pOptionalHeader->DataDirectory[5].VirtualAddress)); printf("%x",pOptionalHeader->DataDirectory[5].VirtualAddress); while(pBaseRelocation->VirtualAddress&&pBaseRelocation->SizeOfBlock){ printf("************ 第%d块 ************\n",i); printf("VirtulaAddress(重定位表在文件中的偏移):0x%x\n",RVAToFOA(FileBuffer,pBaseRelocation->VirtualAddress)); printf("Size(该块重定位表的大小):%d字节\n",pBaseRelocation->SizeOfBlock); printf("重定位表具体项数目:%d\n\n",(pBaseRelocation->SizeOfBlock-8)/2); pBaseRelocation = (IMAGE_BASE_RELOCATION*)(pBaseRelocation->SizeOfBlock + (DWORD)pBaseRelocation); i++; } }