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

目录

在PE间隙中插入程序

什么是 PE 间隙:

构造间隙一:

间隙一与参数:

插入 HellowWorld 的补丁程序实例:

补丁程序字节码:

目标 PE 结构:

开发补丁工具:

编程思路:

数据结构分析:

主要代码:

运行测试:

存在绑定导入数据的 PE 补丁程序实例:

改进补丁程序:

修正补丁工具:

为记事本程序打补丁:

小结:


在PE间隙中插入程序

在 PE 中插入程序的最大障碍,除了大部分 PE 文件中存在的空闲空间不足之外,还有一点就是对导入表的处理,对这部分数据的处理比较复杂。本章将使用第 13 章介绍的嵌入补丁程序框架,通过动态加载技术和免重定位技术的应用,打造一个免导入表的、更容易放到其他 PE 里的补丁程序。先来看看什么是 PE 间隙。

什么是 PE 间隙:

第 14 章介绍过,PE 相关数据结构中存在大量无用的字段和空闲空间,这些对于编写一些小的嵌入程序而言,空间是足够的,但无法容纳相对大一点的程序。间隙的存在,为程序代码提供了更大的生存空间。

空闲空间存在于 PE 文件格式相关的的数据结构中,而间隙则存在于数据结构之外:

PE 文件格式中存在三个主要的间隙,见图 15-1:

图 15-1 显示了 PE 结构外存在的三个间隙:

间隙一:主要是定义 DOS Stub 程序,而在 Windows 环境中,16 位程序已然废弃,所以把这个间隙当做可以利用的空间是可行的。该间隙的大小一般由不同的编译器和链接程序定义。选用不同的编译器,使用不同的链接参数,该大小是不一样的。通过调整字段 IMAGE_DOS_HEADER.e_lfanew 的值也可以改变间隙一的大小。

间隙二:一般由用户自己扩充定义。通过调整字段 IMAGE_FILE_HEADER.SizeOfOptionalHeader 的值即可改变间隙二的大小。

间隙三:大小也是由不同编译器和不同链接参数决定。通过调整字段 IMAGE_OPTIONAL_HEADER32. SizeOfHeaders 的值即可改变间隙三的大小。

开发者既可以将补丁程序部署到某一个间隙,也可以同时选择两个或三个间隙,根据自己的需要来定义。本章只演示将自己的代码部署到间隙一的方法。

构造间隙一:

一个有 4 个节表的标准 PE 头文件的文件头部分总大小为 258h,根据 Win32 的内存映射的机制,该文件头部分将被安排到基地址的开始处。也就是说,从 258h 开始,一直到 1000h 在内存中都将空闲(内存对齐以页为单位),这个空间就是间隙一的最大空间。

如图 15-2 所示:

如图所示,在真正构造间隙时,间隙并不在 PE 头部后面,而是在 DOS Stub 和 PE 头之间,通过扩充 PE 字段 IMAGE_DOS_HEADER.e_lfanew 的值来实现。即将该字段加上嵌入的代码长度 (允许嵌入代码的最大长度为 1000h-0258h=0DA8h),间隙扩充出来后,将补丁代码复制到间隙一起始位置即可。

间隙一与参数:

由于本实例的补丁程序没有导入表、没有数据段,并且代码里没有可以修正的重定位代码,所以对目标 PE 文件的相关参数的修正工作量很小。

间隙一的出现使得目标 PE 文件的大小发生了变化,所以,凡是在 PE 文件里涉及文件偏移的所有字段均需要修正。

这些字段和参数主要包括:

口 IMAGE_OPTIONAL_HEADER32.SizeOfHeaders (所有头 + 节表按照文件对齐粒度对齐后的大小)

口 节表中的 IMAGE_SECTION_HEADER.PointerToRawData 在文件中的偏移

口 代码中的 E9 FC FF FF FF 指令中操作数的修正(准备跳转到补丁代码处)

口 IMAGE_OPTIONAL_HEADER32.AddressOfEntryPoint (代码入口地址)

口 IMAGE_DOS_HEADER.e_lfanew (DOS Stub 程序偏移)

幸运的是,代码中的大部分地址是与内存有关的 RVA,而非文件偏移,所以要修正的字段全部都已经列出来了。

注意:

这里没有列出 IMAGE_OPTIONAL_HEADER32.SizeOfImage (内存中整个 PE 文件的映射尺寸)字段。在大部分情况下,对 PE 文件做过修改后都要修正这个值。因为本章假设补丁代码只用到了 PE 加载器分配给文件头部 (1000h) 空间的剩余空间,即使不用这些空间 PE 加载器也不会给文件头部分配更小的空间。所以,尽管文件大小发生了变化,但加载到内存后的 PE 映像尺寸并没有发生变化。

插入 HellowWorld 的补丁程序实例:

本实例选定第 6 章编写的 HelloWorld1.asm 程序,因为它符合无数据段、无导入表、无重定位信息条件。(用的是动态加载技术,数据段直接放在了代码段,所以只有一个紧密的 .text 节。AddressOfEntryPoint 把入口点定位在了.text段中真正代码段,所以不会碰到数据段,数据段的调用用的是重定位技术。)

复制该文件到 chapter15 目录,更名为 HelloWorld.asm,并在该文件中添加补丁流程转向特性,即在 HelloWorld.asm 的主程序的最后、ret 返回指令之前加入以下指令,然后重新编译链接生成 HelloWorld.exe。

注意:一定要将代码段的标志 06000020h (可读可执行) 更改为 0e0000020h (可读可写可执行),否则运行会出现异常。(因为数据在代码段,代码运行时要填入数据的值,也可以在编译时通过 -section:.text,ERW 来加入该属性)

运行生成的 PE 文件,和久违的 HelloWorld 对话框出现在屏幕上。显示完对话框后依然会出现异常。这个倒不用担心,这正说明代码中添加的跳转指令起作用了。

补丁程序字节码:

以下显示了 HelloWorld.exe 的字节码 ,红框部分为加入的跳转:(补丁码还没嵌入,跳转准备跳转回补丁码处)

所有可执行的代码长度为 01D8h,其中包含了导入函数、数据段、重定位信息的处理。可以说,这种结构的代码在移植性上具有更大的适应性。不过虽然提高了代码的适应性,但损失了代码的空间,从上面的字节码可以看出代码的长度明显增大(因为动态加载技术自行加载动态链接库函数的功能,所以使用动态加载技术的代码一般都比较长)。

如果想在其他 PE 中找到合适空闲的空间存放代码就成了一个问题。下面就来解决这个问题,通过合理地修改 PE 文件头,根据操作系统特性和 PE 文件的数据结构的定义来创造空间,为代码找到栖身之地。

目标 PE 结构:

本实例打完补丁以后的目标 PE 的大致结构如图 15-3 所示:

如图所示,补丁程序中的代码段字节码被嵌入到了目标文件头部,位于 DOS 头和 PE 头中间。目标文件有变动的其他部分包括 DOS 头中指向 “PE\0\0” 标志字的指针值、目标文件 PE 头部部分字段、节表的每个节的文件起始偏移,而目标文件的节内容并无变化。

开发补丁工具:

本小节将开发在 PE 间隙中插入补丁程序的补丁工具。重点学习本实例中用到的数据结构及变量,最后,通过对 Word 程序打补丁,对补丁工具进行简单测试。

编程思路:

考虑到补丁程序采用了重定位技术、动态加载技术,补丁字节码的可移植性较强。制作的补丁工具相对简单。

以下是在 PE 间隙中插入程序的补丁工具编程的基本思路:

步骤1:从补丁程序获取代码段字节码大小 dwPatchCodeSize。

由于补丁程序使用了嵌入补丁框架,所以数据、代码、导入表等数据均在补丁程序的代码段中。此例中的 PE 间隙位于文件头部,当 PE 被加载进内存后,系统会为文件头部分配一个页面 (1000h) 的大小。一个普通的 PE 头部大小为 258h,所以间隙一可利用的空间为:1000h - 258h = 0DA8h

当然,用户可以通过定义把间隙一的空间变得比 0DA8h 更大, 但一旦间隙一的空间大于该值,操作系统就会为 PE 头部分配多于一个页面,这将导致节表结构中描述节在内存的起始地址的字段发生变化,补丁工具必须针对这一变化修改 PE 中大量与之相关的数据。所以,为了简单起见,补丁工具会首先判断补丁代码段大小是否大于 0DA8h; 如果大于该值,则认为空间不足而拒绝打补丁。

步骤2:将补丁代码大小对齐后,加上目标 PE 原始文件大小得出补丁后的新文件大小 dwNewFileSize。用这个变量申请内存空间,并将目标 PE 的 DOS 头和 DOS Stub 复制到新申请的内存中。

步骤3:通过目标 PE 计算出间隙一在目标文件的起始位置 lpPatchPE,并将补丁程序代码段的字节复制到新申请的内存该偏移处。

步骤4:将目标 PE 剩余部分字节码全部复制到补丁程序代码段后,从而完成对目标 PE 的补丁代码的插入。

步骤5:修正因在文件头部插入代码导致的每个节在文件中起始位置的变化。

步骤6:计算嵌入补丁框架中 E9 指令后的跳转偏移,使其跳转到原始的目标 PE 文件的入口处执行原始的目标 PE 文件。

步骤7:修正代码入口地址使其指向新加入的补丁代码的起始地址。

步骤8:将内存中已创建并修改完成的目标补丁程序写入文件。

打完补丁以后的目标程序大致结构可以参见图 15-3:

数据结构分析:

间隙一是用来存放补丁程序的。在程序编码阶段,补丁程序就被设计成一个整体,即与补丁程序有关的所有信息、数据、代码等均在其内,并且相对独立。在程序中,间隙一的大小和补丁程序的大小相等,实际上,间隙一的大小可能还要大。间隙一的实际代码大小在变量 dwPatchCodeSize 中,dwNewPatchCodeSize 是将实际大小按照 8 个字节对齐后的大小。

类似地,所有的头 + 节表大小也有两个表示,其中后者是按照字段 IMAGE_OPTIONAL_HEADER32.FileAlignment 对齐以后的大小。前者是实际大小,后者的大小放到了变量 lpOthers 里。(第一个表示是实际大小 (不知道是哪个),第二个表示是 IMAGE_OPTIONAL_HEADER32.SizeOfHeaders )

因为文件头从 0 开始,所以 DOS 头 + DOS Stub 的大小实际就是 IMAGE_DOS_HEADER.e_lfanew 的值,程序中用变量 lpPatchPE 表示。

各变量位置见图 15-4:

如图 15-4 所示,lpFirstSectionStart 是第一个节的数据位于文件中的偏移,大部分情况下,该值与 lpOthers 相等。也有例外,通过对诸如记事本、IExplorer.exe 等程序的分析可以看到,在这些程序的文件头里,节表后还有一些数据,它们是绑定导入数据(PE 文件头包括 DOS头、PE 头、节表项,节数据不在 PE 头范围中)。对这些可执行文件打补丁的方法在后面再进行讨论。

注意,当前的程序只适应于节表定义后再无数据的 PE 程序:

例如,打开 WinWord.exe (Word 字处理程序),其文件头节表定义部分如下:

节表后再无任何数据,补齐的部分全是用 0 填充

下面再看一个记事本程序,它的节表定义后的部分如下:

在导入表下的绑定导入表目录中有提及该字节码。

2f1a4818479b636e9f152d73bb42525a.png

可以看到,在节表后,文件偏移 0x00000250 开始的位置还有一些与绑定导入有关的数据,如果用本章开发的第一种补丁工具对这种程序打补丁,在 OD 中调试会提示找不到动态链接库。(绑定导入表是提前修正 IAT 地址,节省时间,声明绑定导入数据,第一个字段时间戳对应得上就没问题了)

对这种 PE 文件打补丁的基本思路是:

从目标 PE 的第一个节表的文件偏移往前找非 0 字节 (找到足够可以存放补丁代码大小的 0 即终止查找) ,然后,将 lpPatchPE 指向该位置,保留原始数据即可。与记事本程序的文件头类似的还有很多常见的程序,比如 IExplorer.exe、Explorer.exe 等。

主要代码:

本实例依旧使用第 2 章的 pe.asm 作为基本程序框架,在 _openFile 函数中增加代码清单 15-1 所示代码:

以下代码完成了对指定目标 PE 实施补丁的功能,补丁程序选择 15.2 节生成的 path.exe 程序,生成的打过补丁的目标 PE 文件为 C:\bindA.exe,完整的代码请参照随书文件 chapter15\bind.asm 。

运行测试:

编译链接 bind.asm,生成 bind.exe,执行该程序:

选择菜单 “文件” | “打开补丁文件”,选择 patch1.exe。

选择菜单 “文件” | “打开 PE 文件” 选择 Helloworld.exe。

选择菜单 “文件” | “附加到间隙一”,执行打补丁过程。运行时的输出如下:

运行 bindA.exe 可以看到,首先出现 HelloWorldPE 对话框,然后才运行原来的 HelloWorld 对话框。因为 AddressOfEntryPoint 指向补丁代码,然后补丁代码最后有个 E9 段间相对转移指令修正跳转到原代码处。

存在绑定导入数据的 PE 补丁程序实例:

本节针对上一节开发的补丁进行改进,使其可以适应具有绑定导入数据的 PE 文件。这些改进涉及补丁程序和补丁工具,以下将分别讨论。

改进补丁程序:

原有的 patch.exe 在打补丁后进行数据合并时更改了数据段中某些变量的位置,需要由补丁工具修正,但是我们上面的 bind.exe 没有像第 14 章的 bind.asm 一样有修正地址的代码,所以我们的 patch1.exe 加入了重定位因素,这样就不用修正了。

进行以下是改进后的补丁程序的部分代码:(这里的全局变量在后面都进行了重定位)

顺便附上原来的代码:(全局变量的都需要重定位)

我们将补丁程序用到的所有代码装入一个子程序 _goThere,将用到的可读、可写的数据全部设置为局部变量,让程序自动使用栈存取这些数据,这样可以避免代码操作数的重定位。

代码清单 15-2 是补丁代码中子程序的实现过程:

补丁代码的子程序几乎完成了全部的补丁功能。行 14 ~ 17 调用内部函数 _getKernelBase获取 kernel32.dll 的基地址; 行 19 ~ 38 得到 GetProcAddress 和 LoadLibraryA 两个了函数的地址; 行 40 ~ 45 动态加载 user32.dll 到进程虚拟内存空间; 行 47 一 52 得到函数 MessageBoxA 的地址,行 54 ~ 57 调用 MessageBoxA 显示弹出窗口。

(相当于把原来 patch.asm 的 start 主代码区的全部功能塞到一个 _goThere 函数里了,设置部分局部变量减少重定位个数)

修正补丁工具:

上一节开发的 bind.asm 无法对存在绑定导入数据的 PE 进行补丁,现在就来修正这个缺点。

补丁工具用到的变量所在位置如图 15-6 所示:

如图 15-6 所示,为了保持目标 PE 文件中绑定导入数据的相对位置不发生变化,补于工具首先求出了目标 PE 文件头中有效的数据长度 dwValidHeaderSize,这个长度包含了 PE 头和绑定导入数据; 然后,将这些数据复制到内存申请的新的空间中; 在这之后才附加了补丁代码。与图 15-4 相比,补丁代码部分多了一部分数据,这部分数据即是上图中 “一直到原 PE 头中有效数据结束为止 (再加两个 0) ” 所示的数据。

补丁工具的主要代码依然在 _openFile 中,见代码清单 15-3 :

行 22 调用函数 getValidHeadSize 获得目标 PE 文件头部的有效长度。这个有效长度包括绑定导入数据等分布在标准文件头部后的一些数据,新的文件头部的长度将包含这些有效数据的长度。如果不考虑这些数据,好多程序是无法打补丁的。

为记事本程序打补丁:

下面,为了测试对头部具有绑定导入数据的 PE 文件补丁效果,我们使用 bind1 为记事本程序打补丁,补丁程序选择 patch1.exe。

运行结果如下:

通过查看字节码发现PE后还有部分绑定导入数据残留,删除之后还是能照常运行:

小结:

本章主要介绍在扩充 DOS Stub 以后产生的 PE 间隙中插入程序的方法。PE 文件中存在三处间隙,想在间隙里插入程序,首先要通过合理地修改某些头部字段实现间隙, 然后,将补丁程序复制到这些间隙中,并对补丁程序和文件头部信息进行修正。

和第 14 章不同,这种方法改变了文件前半部分的大小,因此,后半部分涉及节中与文件偏移有关的信息必须得到修正。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

沐一 · 林

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

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

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

打赏作者

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

抵扣说明:

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

余额充值