WINDOWS+PE权威指南读书笔记(21)

目录

在 PE 空闲空间中插人程序

什么是 PE 空闲空间:

PE 文件中的可用空间:

获取 PE 文件可用空间的代码:

获取 PE 文件可用空间的测试:

添加注册表局动项的补丁程序实例:

补丁程序的源代码:

补丁程序的字节码:

目标 PE 的字节码:

手工打造目标 PE 的步骤:

基本思路:

对代码段的处理:

对导入表的处理:

修改入口 AddressOfEntryPoint 地址来测试:

对数据段的处理:

修改前后 PE 文件对比:

开发补丁工具:

编程思路:

数据结构分析:

运行测试:

适应性测试实例分析:

小结:


在 PE 空闲空间中插人程序

本章主要通过一个实例介绍如何将自己编写的程序代码加入到任何一个可执行文件的空闲空间中。由于要插入的是一个程序,其目标代码大小不定,PE 文件头的可用空间比较分散,只适宜于针对特定指令代码的补丁,所以,这里的空闲空间只能利用节的剩余空间。这种补丁方式难度很大,成功率也比较低,其可行性取决于目标 PE 文件中节的具体情况。

什么是 PE 空闲空间:

PE 空闲空间是指不需要对 PE 进行变形(就是第 12 章不断调整结构,覆盖而挤出来的空间),且显式存在的可利用空间。在第 12 章 PE 变形技术中,对 PE 内部数据结构中所有可利用的空间进行了讨论。这些空闲空间包括 PE 文件头部结构中可连续利用的字段组成的空间,也包括文件头部、节区数据为了对齐而产生的空间。

PE 文件中的可用空间:

由文件头部可利用的连续字段组成的比较大的空间包含以下三个:

口 位于 IMAGE_DOS_HEADER 结构内的 54 (36h) 个字节

口 微软链接器生成的 DOS STUB 块程序的 104 (68h) 个字节

口 位于 IMAGE_DATA_DIRECTORY 结构内的 52 (34h) 个字节

这些空间对于编写一个小巧的补丁代码已经足够了,特别是 DOS STUB 位置的 104 个字节 ; 但是,对一些较大的补丁程序,哪怕是经过优化的补丁程序(如第 13 章中介绍的万能补丁码,其大小为 80h),这三个空间中的任何一个还是无法完全容纳它。

除了空间问题以外,还有一个问题,那就是当 PE 加载器加载任务完成后,文件头部所在页面将被设置为只读 ; 这样就限制了在以上可用空间中只能存放只读数据,其他可读写的数据、可执行的代码等都无法存放到这些空间中。比较麻烦的是,文件头部数据并没有对应的节来描述它,所以不可能在 PE 文件中定义文件头部所在页面的属性(除非采用其他非常规方法将该页面设置为可读、可写、可执行)。

既然文件头部结构中存在的连续空间无法实施大部分补丁程序,那么再来看一下 PE 文件中为了对齐而补足的空间。大部分的链接器在补足时将该空间的数值置为 0,这个特性非常重要,因为只有这样才能结合 PE 文件结构中的描述,从一个陌生的 PE 里判断出哪些是对齐用的数据,以及这些数据的大小。

获取 PE 文件可用空间的代码:

下面显示了 PE 文件头部和每个节因对齐,以及因补足而产生的空闲空间情况 :

(完整代码请见随书文件 chapter14\b\pe.asm )

获取 PE 文件可用空间的测试:

(1) 测试记事本程序

注意,.head 并不是一个节,而是文件头部,起这个名字是为了实现输出格式化。

(2) 测试 Explorer.exe 程序

从测试结果看,大部分的应用程序的节中还是有很多可以利用的空间的,至少能找到可以存放万能字节码(80h)的空间。

添加注册表局动项的补丁程序实例:

首先来看本章要使用的补丁程序,补丁的主要目标是在注册表中注册一个启动项。本补丁程序用到三个与注册表相关的 API 函数,注册表操作系列函数位于动态链接库 advapi32.dll 中,在编程时需要引入它们的 .inc 文件和 .lib 文件。

这三个函数分别是 RegCreateKey、RegSetValueEx 和 RegCloseKey。(在第 4 章导入表有讲)

补丁程序的源代码:

以下是补丁程序源代码,它完成了在注册表启动项中添加一条启动信息:

补丁程序的字节码:

1.PE 文件头

2. 代码段

3. 导入表

4. 数据段

目标 PE 的字节码:

为简单起见,这次操作选择 HelloWorld.exe 作为这次测试的目标 PE。以下所示是已经打造完成补丁嵌入的新的 PE 文件 (chapter14\HelloWorld_2.exe) 的字节码。

1.PE 文件头

与原始 HelloWorld.exe 相比,改造后的 PE 文件头部信息的如下部分发生了变化 (上面红框框起来的部分):

2. 代码段

合并以后的代码段明显分为两部分,红框框起部分为原始代码的内容,没有发生任何变化 ; 剩余部分字节码为补丁程序的代码段内容 ,所不同的是,某些指令的操作数已经做了修正。如补丁程序中的第一条压栈指令 “push addr @hKey”,在原始的补丁字节码中显示 68 5E 30 40 00,修正以后的 PE 中显示 68 69 30 40 00,因为压入全局变量后面跟的是 VA。

由于补丁程序的数据段定义的数据变量发生了位置上的迁移,所以指令操作数发生了变化,这在后面会讲到。

3. 导入表

从 ASCII 码显示的动态链接库及相关函数的名称可以看出,导入表对原始 PE 及补丁 PE 的导入采用了合并操作。当然,合并操作并不像看起来这么简单,合并不是数据的堆砌,而是根据结构进行有约束地重构造。后面还有大量修正操作。

4. 数据段

注意:

补丁后的 PE 文件数据段是补丁前 PE 数据段内容,及补丁程序数据段内容的叠加。这种叠加方式必须有一个前提,那就是要确保叠加补丁数据时的位置。补丁数据不能覆盖目标 PE 数据段的数据。因为原始 PE 文件的数据段中只定义了 “HelloWorld\0” 字符串,所以合并时将补丁程序的数据内容紧跟在该字符串之后。

手工打造目标 PE 的步骤:

(可以参考手工重组导入表目录中构造目标指令和 PE 头部变化部分)

为了熟悉程序流程,先通过手工方式打造这一补丁。目的是使用最原始的不经过加工 (如免重定位、动态加载技术) 的程序,在尽量不改变目标 PE 结构的前提下,实现将代码附加到 .text 节中、将数据附加到 .data 节中、将导入表附加到 .rdata 中,以实施补丁。

基本思路:

为了能将补丁程序插入到目标 PE 的空闲空间里,需要重点处理以下几种数据 : 数据段、代码段和常量段。

此次手工补丁的基本思路如下:

首先,从目标 PE 中获取如下参数:目标 PE 的节空闲长度。由于导入表需要单独的空间,所以目标 PE 至少要有两个节。这样可以把补丁代码和数据放到一个节中,而导入表等其他常量则放到另外一个节中。

其次,计算补丁程序的代码节和数据节的大小,并判断目标 PE 是否有单独的空间。

然后,计算补丁程序与目标 PE 总的导入表所需要的空间,并判断目标 PE 是否有单独的节空间。

最后,修正代码及相关参数。

打完补丁以后的目标程序大致结构见图 14-1:

如图所示,依据同类数据放到相同类别的数据所在节中的基本原则,补丁代码嵌入到 .text 节,导入表及相关结构嵌入到 .rdata 节,而数据则嵌入到 .data 节。下面分别来看对各类数据的手工处理过程。

对代码段的处理:

附加代码的最佳方法是将代码附加到某一位置,然后修改入口地址(AddressOfEntryPoint),使其指向该位置。当运行完附加代码后,还要回到原代码处执行(原代码有 ExitProcess,不会无限反复)。这时候需要在附加代码的最后添加一条跳转指令,该指令常用的有两种,一个是 EB,另外一个是 E9 ; 但 EB 属于近跳转,在某些大型的程序中,当附加代码和原代码距离超过一个字节时,该指令就无能为力了,所以,这里选用 E9 指令,后跟一个双字的有符号偏移量。(E9(段间跳转) JMP immed16 EB

JMP immed8)

总体来说,对代码段的处理包括以下两部分:(AddressOfEntryPoint 入口地址最后才改,不然没有导入表)

口 将代码附加到目标 PE 的某个位置

口 修正代码中的地址

第一步操作很简单,将目标代码复制到原始代码后 (文件偏移 0x00000424 处) 即可。下面重点讲述第二部分内容,即修正代码中的地址。

要实现这一步操作需要解决以下三个问题:

1) 代码中哪些地址是需要修正的? 即要明晰字节码与指令之间的关系。

2) 在毫无意义的字节码序列中,如何确定哪些字节码是描述地址的。

3) 找到这些地址后,如何修正它们。

1.获取指令码与字节码的对应关系:

在本书的学习过程中,经常会碰到将指令翻译为字节码,将字节码翻译为指令这样的任务。那么,有没有一种办法能够查找到汇编语言中的指令与字节码之间的对应关系呢? 下面将使用一个简单的程序生成字节码,并通过 W32DASM 程序反编译后获取指令与字节码之间的对应关系。

如代码清单 14-3 所示:

该程序生成了一个用以描述字节码与汇编指令之间关系的文件 comset.bin; 通过反汇编程序反汇编该文件,即可得到字节码与汇编指令之间一对一的关系。

该程序的基本思路是构造从 00 ~ff 之间的所有指令码的指令 + 操作数,指令与指令之间操作数的长度为 6 个字节,最后两个字节是指令 nop 对应的 90h。

运行后生成 comset.bin 字节码集合,使用 W32dasm 反编译,会生成如下结构的指令码与字节码之间的对应关系。通过这些对应关系,我们很容易就知道了哪个字节码翻译成汇编指令以后会是什么。

通过以上方法可以得知,在 00~ff 字节码中,被翻译为跳转指令的一共有 3 个,它们分别是(框起来部分):

为了完成补丁代码结束以后向目标 PE 入口地址的跳转,这里将源代码稍做修改,增加了最后的 E9 跳转指令:

下面计算 HelloWorld.exe 中的跳转偏移。从 E9 指令到目标文件的最初起始指令大小为 63h,减 1 以后为 62h (即 0000 0000 0000 0000 0000 0000 0110 0010),取反结果为 1111 1111 1111 1111 1111 1111 1001 1101,写成十六进制为FF FF FF 9Dh。

2. 判断程序代码中的操作数是地址

这个不是特别容易,至少笔者是这么认为的。比如压栈指令 push,压双字的指令字节码为 68,但压入的是值还是 RVA 很难判断,因为这个压栈操作需要结合具体的函数进行分析才能得出结论。这里采用的办法是:根据后面操作数的具体值进行模糊推测。方法是取出指令的操作数,然后比较该值与数据所在段的范围 ,如果落在该范围内,则直接认为这是一个 RVA。

类似的代码如下:

3. 修正代码中的 RVA

代码中有许多对数据段变量进行操作的指令,由于在进行数据合并时更改了数据段中某些变量的位置,所以指令中这些涉及数据段变量的操作数必须得到修正。应该说,对程序而言这是一件很难的工作。但补丁程序是由开发者自己编写的,知道在编码时使用了哪些带地址的操作数的指令,相对来说再修正代码就容易多了。

本实例将只对以下指令后的操作数进行修正:

具体包括:

口 A3 指令 (传值指令 mov @hKey,eax )

口 B8 指令 (传值指令 mov eax,offset sz1)

口 03 05 指令 (加法指令 add eax,@hKey )

口 FF 05 指令 (加 1 指令 inc @hKey)

口 68 指令 (段内压栈指令 push dword ptr ds:[xxxx])

口 FF 25 指令 (跨段的跳转指令 jmp dword ptr ds:[xxxx])

口 FF 35 指令 (跨段的压栈指令 push dword ptr ds:[xxxx])

附上补丁内变量作为参考:

现在看一下字节码处的修改:

红框处是原 HelloWorld.exe 的代码,绿框处是上面讲到的涉及补丁变量指令的操作数部分,已经修改完毕。

下面谈谈如何进行地址修正,首先下图是数据段部分,补丁数据放在原 HelloWorld.exe 数据后面:

例如,第一个地址原始值为 0040305Eh,因为数据段的地址是在原数据的后面,即距离原数据的偏移为数据当前位置减去数据原来位置:

0000300Bh - 00003000h = 0Bh

地址进行修正的时候加上这个偏移量即可。如例子中的第一个地址:

0040305Eh + 0Bh = 00403069h

所以说,确定了数据在内存中的地址以后再对代码中的地址进行修正就很容易了。

对导入表的处理:

要想操作导入表,必须知道导入表所在节的相关信息,所以,首先要找到导入表所在的节。

导入表所在的节的确定方法如下:

步骤1 通过数据目录表获取导入表的 RVA。

步骤2 通过该 RVA 与每个节的起始、结束 RVA 对比。

步骤3 确定导入表落在哪个节内。

知道了导入表的在哪个节里,与这个节有关的其他信息,比如,节在文件中的的起始地址、节在内存中的起始地址、节的实际尺寸等参数,就可以从节表项的结构中获取到了。

相关代码如下:

为了将补丁导入表数据插入到目标 PE 中,首先要知道在补丁代码中一共调用了多少个动态链接库,以及每个动态链接库所引入的函数个数,这些信息可以从导入表中获取到。

现在,假设补丁程序中用到的所有函数在目标代码段的导入表中都没有。所以,只要确定了动态链接库的个数,确定了每个动态链接库中调用函数的个数,新导入表的大小也就确定了,剩下的工作就是确定新导入表的位置和相关数据结构中 RVA 地址的修正了。

(因为一个动态链接库对应一个 IMAGE_IMPORT_DESCRIPTOR 结构,一个函数对应一个 IAT 中的 VA (结尾双字的 0 也要考虑),.rdata 节中包括 IAT 和导入表描述符。)

特别注意:(虽然我们前面手工重组导入表目录中就破坏了原有的 IAT)

原有的 IAT 一定不能破坏,否则会导致原指令中许多语句 (那些涉及地址访问的语句) 需要修改,这可是个大工程,相信你不会愿意那么做的。

不破坏 IAT 结构的唯一办法就是把新增加的 IA 放到其他空闲的空间中。这样,才能保证原有调用的 RVA 不被破坏。那么,新增加的 IA 放到哪里去呢?

由于两个 PE 文件的导入表要放到一起,所以目标文件中的导入表必须移位 , 如果不移位置,新加入的补丁导入表会破坏后面的数据。基本想法是将目标文件中描述导入表的几个 IMAGE_IMPORT_DESCRIPTOR 数组原样移走,而代替原位置的将是补丁程序的 IAT 数据,以及由 originalFirstThunk (桥 1 ) 指向的 IMAGE_THUNK_DATA 结构数组数据。

(搬出原导入表结构——>空闲位留给补丁导入表的 IAT 和 IMAGE_THUNK_DATA——>这样由于原导入表的 IAT 和 IMAGE_THUNK_DATA 位置没变,所以原导入表的桥 1 桥 2 Name1 这些都不用变——>省了很多功夫)

目标文件导入表和补丁文件导入表将被移动到节的末尾(所以前面计算的节的末尾空闲位置派上了用场)。所以,两个导入表的 IMAGE_IMPORT_DESCRIPTOR 数组总和即为判断空间是否足够用的数值。

如果补丁程序调用的函数比较多,采用上述方法也会出现问题,主要是用来存放补丁程序导入表相关的两个结构无法在目标导入表的空间里容纳下,这时候可以采用第二种策略,即将两个相关结构分别放到其他空闲空间中。在示例程序中,采取第一种策略,大家可以自己通过程序实现第二种策略,以提高程序的兼容性。

大致步骤如下:

步骤1 求补丁程序的动态链接库个数 dwDll:

通过遍历导入表,直到发现最后一个全 0 结构即可获得动态链接库个数。相关代码见清单 14-4,其中返回值 eax 中存放了动态链接库的个数,而 ebx 为调用函数的个数。

步骤2 求补丁程序每个动态链接库对应的函数的个数:

导入表 IMAGE_IMPORT_DESCRIPTOR 的结构中有一个 FirstThunk 指针,这个指针指向的数组中有该动态链接库的个数 dwFunctions (这在后面会提到,按顺序记录每个 DLL 调用函数个数)。

程序设计时,可以构造一个 “个数,个数,个数,0” 的数组,用来记录每个 DLL 中调用的函数个数。

步骤3 计算新导入表增加的大小:

有了以上信息,新导入表比最初的导入表增加的大小就可以计算出来。

(一个动态链接库中每个函数对应一个IMAGE_THUNK_DATA结构,末尾全 0 做结束,这里是把末尾的全 0 作为动态链接库个数了,大小是不变的)

1:(所有的函数个数 + 动态链接库个数) *4 = 新 IAT 项大小 (桥 2 指向)

2:(所有的函数个数 + 动态链接库个数) *4 = 新 originalFirstThunk 表项大小 (桥 1 指向)

3:(目标文件动态链接库个数 + 补丁文件动态链接库个数) *sizeof IMAGE_IMPORT_DESCRIPTOR = 新增加的导入表项大小 (这里是把原来的也算上了)

4:补丁函数名和动态链接库的字符串部分 (和前面手动重组导入表目录一样)

1) 将 1 和 2 两项的和与目标文件导入表数组的大小比较,如果前者小于后者,则满足条件,可以继续进行,否则提示空间不足。(因为补丁 IAT 和 IMAGE_THUNK_DATA 结构要放在原导入表结构空间中)

2) 将 3 和 4 两项的数值之和与找到的连续空闲空间相比较,如果前者小于后者,则满足条件,可以继续进行,否则提示空间不足。(因为这些导入表结构和字符串都移到到末尾处)

步骤4 找到目标导入表所处的节,算出该节的剩余空间:(就是用前面计算空闲空间的代码)

如果大于步骤 3 后中 3 和 4 算出的结果之和,则可以继续进行,否则提示导入表部分空间不足,退出。

步骤5 关于导入表及相关数据结构的位置:(.rdata 节的大小不用改,实验测试发现 .rdata 为任何值都不影响)

将目标文件偏移 0610 处 (即目标文件导入表) 开始的 3ch 字节复制到导入表所在节的空闲空间中(文件偏移 0692h,结尾的 0 要保留,所以在 692h 开始),然后修改目标文件中数据目录中导入表的 RVA 为 00002092h,经过这样修改的 PE 依旧可以运行,而无需改动其他位置的数据。

(这里的 3c 是 60 字节,是两个 IMAGE_IMPORT_DESCRIPTOR 的大小加全 0 的结束,没有移动函数和动态链接库名称字符串部分,所以桥 1 桥 2 不用变)

步骤6 在新的导入表后追加补丁代码的导入表数据:(桥 1,桥 2,和 Name1 字段会再修改)

步骤7 将导入表相关的函数名与动态链接库的名字附加到新导入表的后面:(补丁程序 IAT 还没搬过来)

步骤8 将导入表涉及的 IAT,以及由 originalFirstThunk 指向的数据结构 IMAGE_THUNK_DATA 数组分别存放到目标文件的原始导入表位置,即 610h 开始的位置:

口 IAT 存放在原始导入表的起始位置 0610h。

口由 originalFirstThunk 指向的数据结构 IMAGE_THUNK_DATA 数组存放在紧接下来的空间中 0620h。

步骤9 修正参数

1) 数据目录表中将导入表的 RVA 更改为 00002092h,大小设置为 50h。(原 3ch 加插入的 14h 共 50h)

2) 导入表所在的节 .rdata 大小设置为 0120h。(因为末尾对齐到了 710h 处)

3) 修正导入表的内容,以及 IAT 内容和 originalFirstThunk 指向的 IMAGE_THUNK_DATA 数据结构数组中相关的 RVA 值。

修改入口 AddressOfEntryPoint 地址来测试:

要拼接上面的代码段和数据的处理以及这里的导入表处理后才能测试:(不然代码段调用不了函数)

结果是能运行的,但是写不入注册表,因为 HKEY_LOCAL_MACHINE 没权限:

把 HKEY_LOCAL_MACHINE 改为 HKEY_CURRENT_USER 重复上述流程再试一下:

放入 PEinfo 中进行测试查看,正常显示:

对数据段的处理:

对数据段的处理包括目标数据段识别以及空间计算。因为补丁程序是开发者自己编写的,所以每个节的真实大小,都在节表项的 IMAGE_SECTION_HEADER.VirtualSize 字段中被记录下来,其他来源的 PE 文件则无法通过数据结构中的该字段获取真实大小 (因为即使用户更改了这一部分内容,装载器也不会发生错误! 大家可以自己来做这个实验)。

构造以后的数据段增加了补丁的数据。 首先,从源中获取补丁数据段大小 dstDataSize,从目标数据段中获取剩余空间 (数据段文件对齐后的大小减去数据段真实的大小)。理论上讲,如果剩余空间大于补丁的数据段大小,那么对数据段的修改就被认为是可行的; 但通常获取的目标数据段真实大小是假的。

所以,数据段剩余空间的最好的判断方法是:

先定位可存放数据的节。方法是查找目标文件的节,其属性为 C0000040h,该节的属性一般为可读、可写,并包含初始化数据。只需要判断字段 IMAGE_SECTION_HEADER.Characteristics 标识字段的第 6、30、31 位均为 1(小端逆序,4 位分隔,所以第 6 位对应 4,第 30 和 31 位对应 C),即认定该节是存放数据的节。(因为数据节不一定叫 .data)

找到该节以后,查找该节在文件中的起始位置 startAddress,以及文件对齐后的长度 fLen。从本节的最后一个位置起往前查找连续的全 0 字符,并记录长度。如果长度能达到我们的要求,就可以认为数据段的剩余空间是足够存放补丁程序数据的。

这种判断方法会存在一些安全隐患,比如,目标 PE 文件的数据段中有一些连续的初始化为 0 的数据(如前面的 @hkey,所以我们从最末尾开始填充),通过这种方法找到的空间大小可能会比实际的大。从而在整合补丁数据的时候将数据覆盖到目标 PE 的正常数据区,导致目标 PE 文件运行出现问题。 在实际操作中将忽略这个安全因素,而直接记录下个比较重要的值,即在目标文件中存放补丁数据的起始地址 (文件中的) ,其他数据的插入方法类似。(原数据结尾的 0 要保留)

用思路编写程序,以第 2 章的 pe.asm 作为基础程序框架,在函数 _openFile 中加如下代码,来测试多个 PE 数据段:

从以上的代码中可以看出,退出补丁过程需要满足两个条件:

口 如果文件中不存在可存放数据的节,则退出,并提示未找到 .data 节。对应代码行1一7。

口 如果文件中连续的全 0 空间不够补丁数据大小,则退出,提示目标数据段空间不够。对应代码行 50 一 88。

完整的代码请参照随书文件 chapter14\bind01.asm:

因为对 0 的计数是从节的最后一个字节开始的,所以合并以后的数据段的情况是 : 如果空闲空间很大,那么老数据和新数据之间可能会存在很多 0。尽可能地把新数据放到离老数据相对较远的位置,这样也就最大限度地避免了某些以 0 初始化的老数据被覆盖的现象。

如下所示:

运行 bind01.exe 程序来测试:

修改前后 PE 文件对比:

需要修正和迁移的数据基本都完成了,使用PEComp 程序对比两个程序,运行界面如图 14-2 所示:

从对比图可以看出,手工补丁后的 HelloWorld 2.exe 和补丁前的 HelloWorld.exe 的区别有三处:

1) 文件头部分,程序人口地址做了修改,即字段 IMAGE_OPTIONAL_HEADER32.AddressOfEntryPoint 的值不一样。

2) 数据目录表中对导入表的描述不一样。因为导入表已经被搬移了原来的位置,而原来的位置放置了新文件的数据结构 (包括补丁 IAT 和补丁的 originalFirstThunk 指向的数据结构数组 )。

3) 三个节的长度不一样。前面已经讲过,数据节的长度不需要修正,程序可以照常运行。

开发补丁工具:

掌握以上的基础知识后,下面来开发补丁工具。该工具将要完成的操作与 14.3 节手工完成的类似。程序开发的流程会很复杂,和希望大家阅读时要有耐心。

编程思路:

补丁工具编写的思路如下:

步骤1:将补丁程序和目标程序均映射到内存中,并通过获取的内存操作句柄 @lpMemory 和 @lpMemory1 进行 PE 文件的读取操作。

步骤2:在内存中开辟一个与目标文件大小相同的空间,用来记录句柄 lpDstMemory,并将目标文件原样复制到该区域。

复制代码如下:

步骤3:按照规则将补丁程序的数据、导入表、代码复制到内存的指定位置,并修正 PE 文件的各参数。

步骤4:将内存数据 IpDstMemory 开始的新文件内容写入到新的文件中。

数据结构分析:

在程序设计时使用了一些特殊的数据结构 (如下所示),下面详细解释程序 bind.asm 用到的变量及相关数据结构:

这里程序中使用了两个相对特殊的数据结构:lpImportChange 和 dwFunctions

1.IpimportChange

该结构是双字数值对的数组,称为导入表修正值结构,其可能的形式如下:(这是内存中的)

举例:

00002056 —— 000021D0 是一对,前者是补丁导入表的 FirstThunk 指向数据结构的数组中的一项,而后者是移动到新文件修正后的值,这里并没有。

代码清单 14-6 是 bind.asm 中为导入表修正值结构赋值的相关代码:

2. dwFunctions

第二个结构是 dwFunctions。

这个数据结构以如下的格式记录了导入表调用函数的个数:

个数 1,个数2,个数3,个数4,0

每个数均为双字,个数 1 是第一个动态链接库引入的函数个数,个数 2 是第二个动态链接库引入的函数个数,依次类推。直到碰到一个双字零表示结束。

在 bind.asm 中为结构 dwFunctions 赋值的相关代码如代码清单 14-7 所示:

运行测试:

运行 bind.exe,输出结果可以作为理解程序设计思路的参考,如下所示:

运行补丁工具程序 (目录自修改过),在同目录下会生成打了补丁的新程序 bind2.exe,这相当于上面手工打造的 Helloworld.exe 和 Patch.exe 程序的合并。运行 bind2.exe 后会在注册表中写入对应内容。

适应性测试实例分析:

接下来,测试另外一个补丁程序,将第 4 章开发过的锁定任务栏的简单 PE 程序 LockTray.asm 作为补丁程序 (该文件可在随书文件 chapter14 目录中找到 )。

1. 编写补丁程序

根据规则,在该源代码的最后添加占位的跳转指令如下:(原来的是 invoke ExitProcess,NULL)

2. 运行补丁工具:

补丁工具不再需要单独开发,在已有的 bind.asm 中修改补丁文件的路径,使它指向新的补丁PE 文件 "锁定LockTray.exe",重新编译、链接和执行程序,执行结果如下:

3. 存在的问题及解决方案

当用该程序对其他 PE 程序进行绑定时,发现大部分不成功。比如用 bind.exe 为其自身打 path.exe 补丁,会提示导入表所在节的空间不够,程序无法正常进行,类似的情况还有很多。

造成这种情况的原因有两个:

一是因为在设置程序之初,就假设目标 PE 程序的代码、数据以及导入表所处的节的位置是不一样的。(相当于定死了不能互相借用空间)

二是如果某个节空间不够,程序就无法进行绑定 , 即使其他节的空间足够大,假设数据段有足够的容量存放补丁程序的所有数据,可是程序设计时只把补丁程序的数据存放到了该段中,其他数据存放到其他段中,而其他段却没有足够的空闲空间存储那些数据。

通过 PEInfo 查看大部分程序发现,许多 PE 程序导入表和代码经常使用同一个段,或者数据段的空闲比较大,而其他段的空闲比较小。这时候就需要重新审视前面的程序,使其能适应更复杂的场合。所以,需要改变程序设计思路,例如导入表段空间如果不够,可以使用数据段的空间 ; 代码段的空间不够,可以使用导入表所在段的空闲空间。这样用 bind1.exe 为 bind.exe 打 patch.exe 补丁就能成功。下面是两个程序对 bind.exe 打补丁时运行效果的对比。

1) 用bind 为 bind.exe 打补丁的运行结果 (因空间不足,输出打补丁失败提示信息):

2) 用bindl 为bind.exe 打补丁的运行结果 (因将导入表转移到数据段,补丁成功):

以下是两个程序对空闲空间的处理方法之间的比较。

1) bind.exe 如果判断出空间不够,即退出:

2) bindl.exe 如果判断出空间不够,还要看其他节的空间是否可以被有效利用:

小结:

PE 空闲空间可以由文件头部数据结构中连续的可覆盖的字段空间组成,也可以由对齐特性产生的全 0 空间组成。本章主要针对后者编写一个补丁程序,通过手工和开发补丁程序两种方式为目标 PE 实施补丁。该部分涉及对导入表的处理、数据的合并、指令操作数修正等操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沐一 · 林

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值