滴水三期:day37.1-重定位表

一、引入重定位表

1.程序加载过程

  • 每一个可执行程序运行都有一个独立的4GB虚拟内存(32位),地址从0x00000000到0xFFFFFFFF,低2G为用户程序空间、高2G为操作系统内核空间;且一个PE文件有很多个PE文件组成,如用OD打开ipmsg.exe文件

    image-20230410164612695
  • 程序运行 --> 操作系统会给程序分4GB虚拟内存 --> 剩下的事情就像“拉伸贴图”一样(装载):

    image-20230410165930687
    • 先装载自身的.exe:如先把ipmsg.exe拉伸贴到ImageBase(0x00400000),分配空间大小为SizeOfImage(0x3D000)

      但并不是所有文件的ImageBase都是0x400000,这个值是可以修改的:打开VC->右键你的项目->setting->选择Link->Category设置为Output->在Base address选项中就可以自定义ImageBase,之后这个程序编译以后,ImageBase就变了

    • 再装载需要用到的.dll:如把ws2help.dll拉伸贴到它的ImageBase(0x71A10000),分配空间大小为SizeofImage(0x8000)。其他.dll也是如此

    • 最后把EIP指向EOP(AddressOfEntryPoint),这个程序就可以执行了

2.问题一:DLL装载地址冲突

  • 一般情况下一个PE文件自身的.exe的ImageBase很少会和别的PE文件ImageBase发生冲突

  • 但是.dll就不一定了:day35写过自己发布的DLL,默认情况下DLL的ImageBase为0x10000000,所以如果一个程序要用的DLL没有合理的修改分配装载起始地址,就可能出现多个DLL的ImageBase都是同一个地址,造成装载冲突

  • 解决办法:如果一个DLL在装载时,发现其他DLL已经占用这块空间了,那么这个DLL会依据模块对齐粒度,往后找空余的空间存入,所以此时这个DLL的装载起始地址和它的ImageBase就不一致了(通俗讲叫"换位置"),怎么办呢?

    模块对齐粒度:和结构体的字节对齐一样:为了提高搜索的速度(时间换空间),模块间地址也是要对齐的。模块对齐粒度为0x10000,也就是64K

3.问题二:编译后的绝对地址

  • 一个.dll.exePE文件中的全局变量,可能在编译完成后,全局变量的地址就写死了,即它生成硬编码时地址值为:ImageBase + RVA;相当于编译完成后,这个全局变量的绝对内存地址就写入PE文件了。如:这个文件中a(人为的全局变量)和%d(系统的全局变量),编译完后地址值都是写死的。

    image-20230410174131397 174039

  • 那假设如果这个PE文件在装载,没有按照预定的ImageBase装入,而是出现了问题一的情况–“换位置”,但由于这个地址值写死了,程序执行时任然会按这个绝对地址去寻找使用,就会出现找不到这个全局变量!怎么办呢?

  • 不光是全局变量,DLL中有对外提供的函数,函数的地址在编译完后函数地址也是写死的,而如果此时DLL装载时ImageBase变了,这些写死的函数地址也就用不了,怎么办呢?

4.引入

  • 所以如果一个PE文件可能会出现“换位置”的情况,那么就需要重定位表,来记录下来,有哪些地方的数据需要做修改、重新定位,保证“换位置”后,操作系统能正确找到这些数据
  • 比如问题二中:这个PE文件也没有按照ImageBase去装载,那么全局变量a的存储地址就会发生变化,但是由于硬编码已经生成:A1 30 4A 42 00,那么重定位表就会把30 4A 42 00这个数据的地址记下来,等到运行时操作系统会根据重定位表找到这个数据,做一个重定位修改,即把0x00424a30这个绝对地址做修改,进行重定位,保证全局变量a可以被准确找到(修改的操作全程有操作系统负责)
  • 为什么很多.exe不提供重定位表,.dll会提供?因为一个PE文件的.exe一般只有一个,且是最先装载,所以装载位置和ImageBase是一致的,也没人跟它抢;但是.dll有很多,就需要考虑装载的位置不是预期的位置,那么这个.dll就需要提供重定位表

二、重定位表结构

1.找重定位表

  • 找到可选PE头中的数据目录项(结构体数组,有16个元素):找到第6个结构体,就是重定位表的数据目录

    175256
  • 根据重定位表数据目录的VirtualAddress,将RVA转成FOA,得到重定位表在ImageBuffer中的起始地址

2.重定位表结构

  • 重定位表的结构比较特殊,直接看图理解:

    struct _IMAGE_BASE_RELOCATION{
    	DWORD VirtualAddress;
    	DWORD SizeOfBlock;
        //一堆数据...(如果是最后一个这种结构,就只有RVA和SizeOfBlock,且都为0)
    };
    
    image-20230410184717549
  • 可以这么理解:一个重定位表中可能会有多个“块”,每个块的结构都是①4字节VirtualAddress、接着②4字节SizeOfBlock、最后是③一堆数据每个块的大小为SizeOfBlock(字节)。

  • 最后一个块的RVA和SizeOfBlock都是0x00000000,表示重定位表结束

1)SizeOfBlock
  • 表示每个块的大小,单位为字节
2)具体项

看不懂的话,可以把VirtualAddress和具体项结合起来理解

  • 一堆数据中,每两个字节叫一个具体项
  • 一块中有多少个高4位为0011的具体项,就表示这个当中有多少个地方需要做重定位修改
  • 具体项占2字节,高4位表示类型:值为3,代表==低12位 + 该块的VirtualAddress==地址处的数据需要修改做重定位;值为0,代表这一2字节数据用来做数据对齐(填充用的),可以不用修改。
  • 故我们只用关注高4位值为3的具体项就可以了。具体项的低12位表示要修改的地方相对于所在页的偏移地址(RVA)
  • 一块中一共有多少个具体项:用(此块的SizeofBlock - 4 - 4 )/ 2
3)VirtualAddress
  • 宽度为4字节

  • 为什么需要这个值:假如现在有10000个地方需要重定位,又因为4GB虚拟内存地址需要32位二进制数才能表示的下,即4字节,所以10000个地方要修改,就需要记录下这10000个地方的地址,一个地址4字节,共需大小10000 * 4 = 40000字节空间,按照这种方法记录,重定位表就需要40000 + 8字节,太大了!!

  • 现在如果把一个PE文件分页(操作系统确实会这么干),内存中一个页的大小为0x1000字节,相当于把文件分成了一小页一小页的。

  • 那么一页中如果有要重定位的地方,重定位表就会给这个安排一这个块的VirtualAddress存储此页的偏移起始地址(RVA),由于一页的大小只有0x1000字节(4096),每个字节内存用一个地址表示,即用12位二进制数就可以表示的下4096个地址(比上面的32位二进制数要少的多!),前面学过内存对齐的概念,所以把这个值用16位存放,故具体项占16位,多出来的高4位可以用来表示其他的含义,低12位表示需要修改的地方相对于所在页的偏移地址

  • 综上所述:一页中如果有要重定位的地方,重定位表给此页安排一块(一块对应一页);此块的VirtualAddress存储此页的起始地址具体项占16位,高4位表示类型,低12位表示要修改的地方相对于所在页的偏移地址

  • 每一个数据项低12位的值 + VirtualAddress才是真正需要修复重定位的数据的RVA

3.页、块、节的关系

  • 一个PE文件(可执行程序)运行时,装入虚拟内存中会对程序进行分页

  • 重定位表会分块

  • 一个程序有不同的节,跟分页和分块没有任何关系

  • 可以用LordPE打开一个有重定位表的PE文件,查看重定位表

    190457

    会发现:这个重定位表分了很多块,第175块中记录的是偏移地址为0xAF000的页中要修改重定位的地方,这些要修改的地方在.text节中;第176块中记录的是偏移地址为0xB000的页中要修改重定位的地方,这些要修改的地方在.data节中

三、为什么学重定位表

本人目前也不太会这些,所以就将海东老师的原话整理出来了,后面如果学习到此内容会进行修正补充

1.破解方面

  • 加密壳:如果想对一个程序加加密壳之前,需要先将程序的各种表—导出表,导入表,重定位表等移动到新增的节当中,移走后对剩下的数据加密(所有的头和节表不能加密!DOS头,NT头,节表)。为什么呢?因为我们知道这些表其实分散在程序的某个节当中,如果直接对整个程序的数据加密,那么最后操作系统也找不到各种表了,无法加载用到的各种DLL了(比如找不到导入表,那么操作系统进行装载时,无法知道有哪些DLL要装到虚拟内存中),所以程序就无法执行
  • 所以就需要对各种表非常熟悉,才知道从哪里移

2.反反调试

  • 反调试:有些游戏公司为了避免别人调试,会在驱动层(0环)把很多函数加上钩子(hook),比如说有一个函数叫openprocess(),用来打开进程。而如果想调试任何进程,都需要先打开进程,像使用OD–>点击附加,本质上就是使用了0环的openprocess()这个函数,那么由于游戏公司给这个函数加上了钩子,在调用这个函数时,游戏就会判断这个函数的参数是否是游戏进程本身,是的话就返回一个0(NULL),不让别人看到它。所以使用OD点击附加–>找进程打开时,会发现游戏的进程根本不在这里面
  • 反反调试:即过游戏驱动,一个比较常用的方式叫----内核重载,即把内核程序(0环)kernel.exe拉伸,把拉伸后的数据往内存中复制一份,只不过这个程序是0环的程序,用的是0环的语法,不能像我们平时在3环写程序用的语法。但是现在不允许把拉伸后的数据往内存复制,因为此时原来的kernel.exe已经把位置占着了,所以只能在它的后面一个模块中复制,这种情况不就和上文中的问题一一样,“换位置”了。那么这个程序中的所有绝对地址都不能用了,都必须要自己根据重定位表把要修正的数据修正重定位。那么我们再想用程序就不用它提供的加过钩子的内核,而使用我们自己复制的这一份

四、作业

1.重定位表这样设计的好处

2.打印重定位表信息

#include "stdafx.h"
#include <stdlib.h>

typedef unsigned short WORD;
typedef unsigned int DWORD;
typedef unsigned char BYTE;

#define MZ 0x5A4D
#define PE 0x4550
#define IMAGE_SIZEOF_SHORT_NAME 8

//DOS头
struct _IMAGE_DOS_HEADER {
	WORD e_magic;  //MZ标记
	WORD e_cblp;
	WORD e_cp;
	WORD e_crlc;
	WORD e_cparhdr;
	WORD e_minalloc;
	WORD e_maxalloc;
	WORD e_ss;
	WORD e_sp;
	WORD e_csum;
	WORD e_ip;
	WORD e_cs;
	WORD e_lfarlc;
	WORD e_ovno;
	WORD e_res[4];
	WORD e_oemid;
	WORD e_oeminfo;
	WORD e_res2[10];
	DWORD e_lfanew;  //PE文件真正开始的偏移地址
};

//标准PE头
struct _IMAGE_FILE_HEADER {
	WORD Machine;  //文件运行平台
	WORD NumberOfSections;  //节数量
	DWORD TimeDateStamp;  //时间戳
	DWORD PointerToSymbolTable;
	DWORD NumberOfSymbols;
	WORD SizeOfOptionalHeader;  //可选PE头大小
	WORD Characteristics;  //特征值
};

//数据目录
struct _IMAGE_DATA_DIRECTORY{
    DWORD VirtualAddress;
    DWORD Size;
};

//可选PE头
struct _IMAGE_OPTIONAL_HEADER {
	WORD Magic;  //文件类型
	BYTE MajorLinkerVersion;
	BYTE MinorLinkerVersion;
	DWORD SizeOfCode;   //代码节文件对齐后的大小
	DWORD SizeOfInitializedData;  //初始化数据文件对齐后的大小
	DWORD SizeOfUninitializedData;  //未初始化数据文件对齐后大小
	DWORD AddressOfEntryPoint;  //程序入口点(偏移量)
	DWORD BaseOfCode;  //代码基址
	DWORD BaseOfData;  //数据基址
	DWORD ImageBase;   //内存镜像基址
	DWORD SectionAlignment;  //内存对齐粒度
	DWORD FileAlignment;  //文件对齐粒度
	WORD MajorOperatingSystemVersion;
	WORD MinorOperatingSystemVersion;
	WORD MajorImageVersion;
	WORD MinorImageVersion;
	WORD MajorSubsystemVersion;
	WORD MinorSubsystemVersion;
	DWORD Win32VersionValue;
	DWORD SizeOfImage;  //文件装入虚拟内存后大小
	DWORD SizeOfHeaders;  //DOS、NT头和节表大小
	DWORD CheckSum;  //校验和
	WORD Subsystem;
	WORD DllCharacteristics;
	DWORD SizeOfStackReserve;  //预留堆栈大小
	DWORD SizeOfStackCommit;  //实际分配堆栈大小
	DWORD SizeOfHeapReserve;  //预留堆大小
	DWORD SizeOfHeapCommit;  //实际分配堆大小
	DWORD LoaderFlags;
	DWORD NumberOfRvaAndSizes;  //目录项数目
	_IMAGE_DATA_DIRECTORY DataDirectory[16]; //数据目录
};

//NT头
struct _IMAGE_NT_HEADERS {
	DWORD Signature;  //PE签名
	_IMAGE_FILE_HEADER FileHeader;
	_IMAGE_OPTIONAL_HEADER OptionalHeader;
};

//节表
struct _IMAGE_SECTION_HEADER{
	BYTE Name[IMAGE_SIZEOF_SHORT_NAME];  //节表名
	union{
		DWORD PhysicalAddress;
		DWORD VirtualSize;  //内存中未对齐大小
	}Misc;
	DWORD VirtualAddress;  //该节在内存中偏移地址
	DWORD SizeOfRawData;  //该节在硬盘上文件对齐后大小
	DWORD PointerToRawData;  //该节在硬盘上文件对齐后偏移地址
	DWORD PointerToRelocations;
	DWORD PointerToLinenumbers;
	WORD NumberOfRelocations;
	WORD NumberOfLinenumbers;
	DWORD Characteristics;  //该节特征属性
};

//重定位表
struct _IMAGE_BASE_RELOCATION{
	DWORD VirtualAddress;
	DWORD SizeOfBlock;
    //具体项
};

/*计算文件大小函数
参数:文件绝对路径
返回值:返回文件大小(单位字节)
*/
int compute_file_size(char* filePath){
	FILE* fp = fopen(filePath,"rb");
    if(!fp){
		printf("打开文件失败");
		exit(0);
	}
    fseek(fp,0,2);
	int size = ftell(fp);
	//fseek(fp,0,0); 单纯计算文件大小,就不需要还原指针了
	fclose(fp);
	return size;
}

/*将文件读入FileBuffer函数
参数:文件绝对路径
返回值:FileBuffer起始地址
*/
char* to_FileBuffer(char* filePath){
    FILE* fp = fopen(filePath,"rb");
    if(!fp){
		printf("打开文件失败");
		exit(0);
	}
    int size = compute_file_size(filePath);
    
	char* mp = (char*)malloc(sizeof(char) * size);  //分配内存空间
    if(!mp){
        printf("分配空间失败");
        fclose(fp);
        exit(0);
    }
    
    int isSucceed = fread(mp,size,1,fp);
    if(!isSucceed){
        printf("读取数据失败");
        free(mp);
        fclose(fp);
        exit(0);
    }
    
    fclose(fp);
    return mp;
}

/*虚拟内存偏移地址->文件偏移地址函数
参数:FileBuffer起始地址,RVA地址值
返回值:RVA对应的FOA,返回0则表示非法RVA
*/
DWORD RVA_to_FOA(char* fileBufferp,DWORD RVA){
	_IMAGE_DOS_HEADER* _image_dos_header = NULL;
	_IMAGE_FILE_HEADER* _image_file_header = NULL;
	_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
	_IMAGE_SECTION_HEADER* _image_section_header = NULL;

	_image_dos_header = (_IMAGE_DOS_HEADER*)fileBufferp;
	_image_file_header = (_IMAGE_FILE_HEADER*)(fileBufferp + _image_dos_header->e_lfanew + 4);
	_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
	_image_section_header = (_IMAGE_SECTION_HEADER*)((char*)_image_optional_header + 
		_image_file_header->SizeOfOptionalHeader);

	bool flag = 0; //用来判断最终RVA值是否在节中的非空白区
	if(RVA < _image_section_header->VirtualAddress)
		return RVA;
	for(int i = 0;i < _image_file_header->NumberOfSections;i++){
		if(RVA >= _image_section_header->VirtualAddress 
			&& RVA < _image_section_header->VirtualAddress + _image_section_header->Misc.VirtualSize){
			flag = 1;
			break;
		}else{
			_image_section_header++;
		}
	}
	if(!flag)
		return 0;
	DWORD mem_offset_from_section = RVA - _image_section_header->VirtualAddress;
	return _image_section_header->PointerToRawData + mem_offset_from_section;	
}

/*打印重定位表函数
参数:指定可执行文件绝对路径
返回值:无
*/
void relocationTable_print(char* filePath){
	char* fileBufferp = to_FileBuffer(filePath);

	_IMAGE_DOS_HEADER* _image_dos_header = NULL;
	_IMAGE_FILE_HEADER* _image_file_header = NULL;
	_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
	_IMAGE_DATA_DIRECTORY* _image_data_directory = NULL;
	_IMAGE_BASE_RELOCATION* _image_base_relocation = NULL;
	
	_image_dos_header = (_IMAGE_DOS_HEADER*)fileBufferp;
	_image_file_header = (_IMAGE_FILE_HEADER*)(fileBufferp + _image_dos_header->e_lfanew + 4);
	_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
	//指向数据目录结构体数组的第6个结构体
    _image_data_directory = (_IMAGE_DATA_DIRECTORY*)(_image_optional_header->DataDirectory + 5);
	
	//判断是否有重定位表
	if(_image_data_directory->VirtualAddress == 0){
		printf("此PE文件没有重定位表");
		getchar();
		exit(0);
	}
	
	//找到重定位表
	_image_base_relocation = (_IMAGE_BASE_RELOCATION*)(RVA_to_FOA(fileBufferp,_image_data_directory->VirtualAddress) + fileBufferp);
	
	//打印重定位表
	printf("***************重定位表****************\n");
	for(int i = 0;_image_base_relocation->VirtualAddress != 0;i++){
		printf("*********第%d块*********\n",i + 1);
		printf("VirtualAddress:%08X   SizeOfBlock:%08X\n",_image_base_relocation->VirtualAddress,_image_base_relocation->SizeOfBlock);
		printf("类型\tRVA\n");
		for(DWORD j = 0;j < (_image_base_relocation->SizeOfBlock - 8) / 2;j++){
            //这里使用右移12位的方式打印2字节宽度的高4位
			if((*((WORD*)((char*)_image_base_relocation + 8 + j * 2)) >> 12) == 0){
				printf("%d\n",*((WORD*)((char*)_image_base_relocation + 8 + j * 2)) >> 12);
				continue;
			}
            //这里用与运算把高4位变成0后输出2字节宽度的值,即为低12位的值(别忘了还要加VirtualAddress)
			printf("%d\t%08X\n",*((WORD*)((char*)_image_base_relocation + 8 + j * 2)) >> 12,_image_base_relocation->VirtualAddress + (*((WORD*)((BYTE*)_image_base_relocation + 8 + j * 2)) & 0x0FFF));	
		}
		_image_base_relocation = (_IMAGE_BASE_RELOCATION*)((char*)_image_base_relocation + _image_base_relocation->SizeOfBlock);
	}
}

int main(int argc, char* argv[])
{
	char* filePath = "C:/WINDOWS/vmmreg32.dll";  //选一个有重定位表的PE文件(VC6的控制台没法打印太多的数据,所以选一个重定位表小一点的PE文件)
	relocationTable_print(filePath);
	return 0;
}

验证一下:使用LordPE打开C盘WINDOWS文件夹下的一个dll----vmmreg32.dll,查看其重定位表以及每一块中的具体项

000750 000919
  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值