Windows 逆向-重定位表

Oyst3r 于 2024-03-04 发布

前言

这节课的话,就是得先很清楚的对整个 exe 程序加载到内存这个过程十分的清楚,明白为什么要去有重定位表,哪些情况(数据)要用到重定位表,以及对重定位表这个结构有很清晰的了解,最后再知道操作系统是怎么去根据这张表去做一个定位的,嗯嗯清楚以上我说的这些问题后,基本就差不多喽

课堂

引入(程序加载的过程)

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

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

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

为什么要去有重定位表

听完上面的介绍,是不是觉得,出来这个一开始就加载进去的 exe 主程序位置可能不会被抢占,其他的 Dll 程序就像是那个 python 的线程一样,一起去抢位置,这样的话就会出现这样一个问题

默认情况下 DLL 的 ImageBase 为 0x10000000,所以如果一个程序要用的 DLL 没有合理的修改分配装载起始地址,就可能出现多个 DLL 的 ImageBase 都是同一个地址,造成装载冲突

编译器的解决办法:如果一个 DLL 在装载时,发现其他 DLL 已经占用这块空间了,那么这个 DLL 会依据模块对齐粒度,往后找空余的空间存入,所以此时这个 DLL 的装载起始地址和它的 ImageBase 就不一致了(通俗讲叫”换位置”),我们知道 RVA 其实是个相对的值,只要我们把 ImageBase 改了,那就行了呗,但对于一些情况真的如此吗?一个.dll.exePE 文件中的全局变量,可能在编译完成后,全局变量的地址就写死了,即它生成硬编码时地址值为:ImageBase + RVA;相当于编译完成后,这个全局变量的绝对内存地址就写入 PE 文件了,我们就不能通过单纯的改偏移去改了,下面举个例子

所以这些值是要去改变的,并不是说所有的值都要去改变

哪些情况(数据)要用到重定位表

嗯嗯上面就顺带说了

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

重定位表结构

struct _IMAGE_BASE_RELOCATION{
	DWORD VirtualAddress;
	DWORD SizeOfBlock;
    //一堆数据...(如果是最后一个这种结构,就只有RVA和SizeOfBlock,且都为0)
};

它和之前学的那些表其实还不太一样

可以这么理解:一个重定位表中可能会有多个“块”,每个块的结构都是 ①4 字节 VirtualAddress、接着 ②4 字节 SizeOfBlock、最后是 ③一堆数据每个块的大小为 SizeOfBlock

首先要知道模块,节,页这三者的概念,每一个 Dll 就是不同的模块,然后每一个模块里面都会有不一样的节,然后内存中还会分页,就是把每 1000H 个相邻的数据分成一页,这也是我们导出表为什么要这么设计的原理,现在来算个数学题,1000H 用 10 进制表示的话就是 4096

而 4096 恰好是 2 的 12 次方(也就是从 000000000000 到 111111111111),所以呢要是以每 1000H 为基准的话,其实 12 个 0 或 1 就能完全表示 1000H 所有的数,但是因为内存对齐,所以还是得用 16 个 0 或 1 去表示每 1000H 上面的数,但是前 4 位我们是不用的,一字节等于 8 比特,也就是两个字节 WORD 了,那前面这 4 个字节有什么用呢?高 4 位表示类型:值为 3,代表低 12 位 + 该块的 VirtualAddress 地址处的数据需要修改做重定位;值为 0,代表这 2 字节数据用来做数据对齐(填充用的),可以不用修改

为什么要学重定位表

破解方面

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

反调试

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

作业

之后再说吧,累了