目录
栈与重定位表
实现重定位的方法:
代码重定位的关键问题是代码中使用了大量的绝对地址。如果将这些绝对地址改成相对地址,那么重定位的问题也就很自然地解决了。
看下面的例子:(用相对偏移间接表示全局变量)
以上代码对应的字节码为:
上述代码不存在重定位问题。为什么呢? 分析如下:
call 指令会将返回地址压入栈中。当整段代码在没有移动的情况下执行时,call @F 指令执行以后栈中的返回地址就是 @@: 标号的地址 00401009h,下一句 pop 指令将返回地址弹出到 ebx 中,再接下来 ebx 减去 00401009h,现在 ebx=0,所以,mov eax,[ebx+offset dwVari] 等价于 mov eax,dwVari。
现在假设已经把代码移动到了00801000h 处。@@: 标号处对应的地址为 00801009h,全局变量 dwVari 所对应的地址为 : 00801000h。
当 call 指令执行以后,压入栈的地址为 :00801009h,pop 到 ebx 中的数据就是它,经过 sub ebx,00401009 以后,再经过指令 ebx+offset dwVari 对地址进行相加,最终,eax 获得的数据逻辑地址为:
00801009h - 00801009h + 00801000h = 00801000h
而这个地址刚好是移动后全局变量 dwVari 的 VA。
通过以上简单的计算得出这样的结论 :
虽然代码移动了位置,但指令还是访问到了正确的变量所在的内存地址。
对代码重新定位,就可以实现在一个内存进程中合理地插入附加代码,实现代码的有效拼接,而不会因为插入导致数据或代码移位而造成内存访问错误。
同时,代码的重定位也可以帮助我们将自己设计的具有独立功能模块的代码,注册到另外一个运行的程序内存空间,并作为这个运行的程序的一个子线程,来达到隐藏自己的目的。通过这种方法运行的程序,别人无法使用任务管理器查看到它,也无法使用第三方的工具找到它。他们所能了解到的只是正常程序多了一个线程,多占用了内存,仅此而已。
重定位编程:
本小节主要从两个方面讨论重定位源程序编码,并对重定位编程进行演示:
口一个是不存在导入表的源程序。
口 一个是既不存在导入表,也不存在重定位表的源程序。
随书文件中 chapter6 目录下的 HelloWorld0.asm 和 HelloWorldl.asm 即是本小节讨论的两个源程序。前者没有使用重定位技术,后者使用了重定位技术。
第一个源程序:代码清单 6-4 无导入表但存在重定位代码的 HelloWorld (chatper6\HelloWorld0.asm):
之所以说该程序生成的最终 PE 里不存在导入表,是因为该程序没有让 Windows 加载器替我们加载动态链接库代码。在该程序中用到的所有外来函数,代码都是自己加载相关的动态链接库,并从加载的动态链接库中搜索找到函数所在的内存地址。这些原本需要Windows 加载器完成的操作由程序自己完成了,关于动态加载的技术请参照第 11 章。因为多了自行加载动态链接库函数的功能,所以使用动态加载技术的代码一般都比较长。
以函数 MessageBoxA 的调用为例:
正常情况下,调用函数前需要先静态引入该函数所在的动态链接库库文件和包含文件,如下所示:
口include user32.inc
口includelib user32.lib
然后在程序中使用 invoke 指令调用该函数。在以上程序中没有使用这种方法,该程序的做法是 :
首先,在 21 一 22 行声明该函数 (_messageBox ) 的定义(其作用与指定包含文件的作用是一样的);
其次,在 171 一 173 行调用 LoadLibraryA 函数,将动态链接库 user32.dll 加载到进程地址空间,
175 一 177 行则通过调用自定义函数 _getProcAddress 得到 MessageBoxA 函数的地址 ;
最后,在 178 行通过 invoke 指令调用 _messageBox。通过这种方式调用所有引入的函数,从而实现了最终的 PE 文件中看不到导入表数据的效果。
可以看到,代码中依然使用了全局变量,所以该代码一定存在重定位问题。要想解决这个问题,需要使用重定位技术,下面看第二个源程序:
该部分代码摘自第 11 章动态加载技术的 11.3.3 小节中列出的代码清单。程序中所有用到的对全局变量的访问都变成无需重定位的寄存器访问方式(包括对函数的调用),相关代码如下:
下面我们对这两个程序进行测试:
由于两个程序都将要操作的数据移动到了代码段,而默认情况下代码段不允许写,所以在运行时会出现错误。要解决这个问题,必须对两个链接好的可执行程序的 PE 头进行手动修改,将“.text”节的属性从 06000020h 更改为 0E0000020h。
首先附上一些前提条件信息:
编译连接生成可执行文件先:
附前面积累的 IMAGE_SECTION_HEADER.Characteristics 数据位含义:(16进制32位)
开始修改字节码测试:
当然,你也可以在链接时通过 link 的参数将代码段追加可写属性。参数定义如下:
对上面两个程序的副本HelloWorld 1.exe 和HelloWorldl 1.exe稍做修改:
改动字段 IMAGE_OPTIONAL_HEADER32.ImageBase 的值,将基地址都改成 00800000,而不改动其他任何位置的字节码,再运行就会发现第一个没有重定位技术的运行不了,第二个无需重定位的寄存器访问的程序可以正常运行:
接下来使用小工具 PEInfo 分析 HelloWorld1_1.exe 的结构如下:
通过查看输出结果可以得知 : HelloWorld1_ 1.exe 只有一个代码段,没有导入表,也没有导出表,没有重定位的信息。为了免去导入表、数据段和重定位信息,源代码从最初的 468 字节变成了现在的 5229 字节。
PE 文件头中的重定位表:
在实际开发过程中,不可能所有的操作都由寄存器完成,这样既不直观,又难以阅读 ;为了方便开发人员编程,通常允许代码中存在重定位信息。重定位信息是在编译的时候,由编译器生成并被保留在可执行文件中。当程序执行前,操作系统会根据这些重定位信息对代码予以修正,复杂的操作由编译器和操作系统代替程序完成。开发人员在编写程序的时候就可以随意地使用那些涉及直接寻址的指令了。
根据前面学过的知识,程序被装和人内存时,其基址是由字段 IMAGE_OPTIONAL_HEADER32.ImageBase 决定的。但是,如果当装载时该位置已经被别的程序使用,那么操作系统就有权重新选择一个基地址。这时候就需要对所有的重定位信息进行修正,而修正的依据就是 PE 中的重定位表。
重定位表定位:
重定位表为数据目录中注册的数据类型之一,其描述信息处于数据目录的第 6个目录项中:
使用PEDump 小工具获取随书文件 chapter6\winResult.dll 的数据目录表内容如下:
加粗部分即为重定位表数据目录项信息。通过以上字节码得到如下信息:
口重定位表所在地址 RVA = 0x000004000h
口 重定位表数据大小 = 000000B4h
使用小工具 PEInfo 查看 PE 文件的节的相关信息,结果如下:
根据 RVA 与FOA 的换算关系,可以得到:
重定位表数据所在文件的偏移地址为 0x00000C00。
重定位表项 IMAGE_BASE_RELOCATION:
与导入表类似,重定位表指针指向的位置是一个数组,而不像导出表一样只有一个结构。这个数组的每一项都是如下结构;
IMAGE_BASE_RELOCATION.VirtualAddress:
+0000h,双字,重定位块RVA。由于直接寻址指令较多,所以在一些 PE 文件中,存在大量的需要修正的重定位地址。
按照常规计算,每个地址占 4 字节,如果有 n 个重定位项,那么需要总的空间为 4*n 字节。
重新审视直接寻址中的地址发现,在一页中的所有地址只需要 12 位(因为 Win32 页面大小为 1000h, 也就是 4096 字节,即2的12次方)。而这 12 位只需要用一个字(一个字两个byte共16位)就能表达出来。
如果有 n 个重定位项,则只需要 2*n 个地址 +4 字节的页面起始 RVA,+4 字节的本页的重定位项个数。将以上两种情况的表达式分别是:
Sum0=4*n
Sum1=2*n+4+4
很明显,当有大量的重定位地址时,Sum0 远大于 Sum1。事实上,为了节约存储空间,重定位表的存储方式选择第二种方式。字段 IMAGE_BASE_RELOCATION.VirtualAddress 就是表达式 Suml 中的第一个4,也就是页面的起始 RVA。
IMAGE_BASE_RELOCATION.SizeOfBlock:
+0004h,双字,重定位块的长度。该字段是表达式 Sum1 里的第二个4,描述的是该页面中所有的重定位表的项数。
数组和数组之间并不是相邻的。比如页面 1 的 IMAGE_BASE_RELOCATION 后,并不是页面 2 的IMAGE_BASE_
RELOCATION,而是页面 1 的所有重定位表项 ;
(所以单字的重定位表项后面遇到 IMAGE_BASE_RELOCATION.VirtualAddress为0就表明重定位块结束了)
重定位项大小为一个字(两个byte共16位),字的高四位被用来说明此重定位项的类型。字的低十二位是需要重定位的数据在页面中的地址。(注意内存和文件字节码中的小端顺序颠倒)
重定位项的高四位的含义见下表:
在实际的 PE 文件中,我们只能看到 0 和 3 这两种情况,也就是说这一项要么是对齐用的,要么是需要全部修正的。
重定位表的结构:
重定位表的结构如下图所示。该实例的重定位表结构中共存在两个重定位块,即块 1 和块 2。
块1有4个重定位表项,其中有 3 个表项是需要重定位的地址(16 位的高四位为3),最后一个为填充用(高四位为0)。
块 2 有 6 个重定位表项,它们全部是需要重定位的地址。
所有的重定位块最终以一个 VirtualAddress 字段为 0 的 IMAGE_BASE_RELOCATION 结构作为结束。
现在可以解释: 为什么我们看到的 PE 文件的可执行代码一般都是从 1000h 开始,而不是从 PE 的基地址开始了:
如果从 PE 的基地址开始,也就是页面的起始地址为 0000h,在定义重定位表块时第一个 VirtualAddress 的值就是 0 ,按照对重定位块的定义,到了这里就是所有重定位项的结束。这个解释和事实有出入,所以,在装载可执行代码时都是从 1000h 开始。
遍历重定位表:
遍历重定位表的编程是从本书 5.4.3 小节的 PEInfo.asm 程序开始的。
在函数 _openFile 中加入以下代码 (加黑部分):
代码略~~~~
用刚生成的PEInfo 打开 C:\windows\system32\kernel32.dll 文件,查看输出的重定位表相关信息,如下所示:
可以看到,每个重定位块中的重定位项的 RVA 值(低十六位)的高四位都是相同的,高四位的值由重定位块的基地址决定。如上面所列第二个重定位块的低十六位的高四位为“2”,则重定位表项所有的低十六位的高四位均为“2”。(注意这和重定位项的高4位类型无关)
重定位表实例分析:
首先回顾前面重定位表的定位:
现在对 chapter6\vwinResult.dll 的重定位表进行分析:
IMAGE_BASE_RELOCATION之间不是相邻的,页面 1 的 IMAGE_BASE_RELOCATION 后是页面1所有的重定位表项。页面2的IMAGE_BASE_RELOCATION在这些重定位表项之后。
从字节码可以获知:
口 重定位表第一项的代码起始页面 RVA=00001000
口 第一块的长度为 0b4h
块后紧邻的 00000000 是所有块的结束标志(所以说整个代码只有一个重定位表块)。第一块的第一个重定位项的值为 3036h(文件字节码是小端),其中高四位为3,转换为二进制码为 0011,表示该重定位值的高位和低位均需要修正。低十二位为修正地址,该地址加上基地址再加上代码页面的起始地址即为代码在内存的实际位置 VA 值。
公式如下:
实际 VA = 基地址 + 代码起始页面 + 低十二位虚拟地址
=00100000 + 001000+036
=00101036h
接下来调试调用了 winResult.dll 的程序 FirstWindow.exe。从内存中找到 001001036 处的字节码:
可以看到,0x10001036、0x10001040、0x10001050、0x10001057 处的地址均需要修正,这些地址刚好对应了在重定位表中看到的 3036、3040、3050、3057。
重定位表只是对全局变量等使用绝对地址的地方有记录而已,并未自行修改,这方便查看所有存在的如果移动代码时需要修改的绝对地址列表。
下面再深入看看该处的汇编指令代码:
对应到源代码如下(加粗部分):
所有加黑的操作数访问的全部是全局变量,因为使用的是绝对地址,所以必须修正。因此,在重定位表中会有该位置的记录信息,但也只是记录而已,程序并没有自行修改。
小结:
本章重点讨论了程序设计中的栈使用及 PE 中的重定位信息。PE 中的重定位表是为了方便程序设计人员在编码中使用全局变量,重定位表项的描述则是为了便于 PE 加载程序在合适时修正代码中使用的绝对地址,保证程序在不同地址空间上运行的兼容性。
本章还初步探讨了免导入表的编程技术。该技术是基于动态链接库的动态加载技术实现的,在本书第 11 章还会详细讲述。由于使用该技术编写的代码没有导入表,因此非常有利于将代码整体移动到某个目标 PE 文件的指定位置处。