在 PE 新增节中插入程序
本章研究的是在目标 PE 中新增一个节,并将可执行代码附加到该节中的技术。本章设计了一个在本地建立子目录的补丁程序和补丁工具。补丁工具中对文件头部做的修改主要包括以下字段: SizeOfHeader、SizeOfImage、AddressOfEntryPoint 和 NumberOfSections 。
新增 PE 节的方法:
补丁程序创造空间最好的做法就是按照 PE 数据结构的规则新增加一个节,然后将这个节有机地融合到 PE 文件中。为 PE 文件新增加一个节的空间,只需要扩充文件尾部即可。
但要想将该节加入到 PE 文件中,就要对 PE 头部信息做工作:
口 在文件头部重新修改节的数量字段。
口 在文件头部节表后追加一个节的 IMAGE_SECTION_DESCRIPTOR 描述结构,并对这个结构中的每个字段赋值。
口 如果新建的 PE 节中包含可执行代码,必须设置好该节的属性,保证该节被装载到内存后对应的页面是可执行。
幸运的是,节表的定义位于文件头部,且在其他数据结构定义之后,其扩充操作可以在文件头部范围内完成,而无需移动其他节相关的数据。
图 16-1 为新增一个节后的 PE 结构示意图:
如图所示,节表中增加了一项用于描述新增加的节,然后将变动的文件头部分按照文件对齐粒度实施对齐,新增的节附加到文件末尾。
在本地建立子目录的补丁程序实例:
本实例中,补丁程序的主要目标是在本地 C 盘根目录下建立子目录。补丁程序的字节码将被添加到新的节中,以下为补丁程序源代码,该补丁程序实现以下两个功能,在C 盘上创建一个名为 BBBN 的目录,然后显示一个对话框。
补丁程序源代码:
为了降低编写补丁工具的难度,在补丁程序代码中使用了前面几章讲述的编程技术,如补丁代码中对涉及地址的地方使用了本书 6.1 节中的代码重定位技术,对导入函数的调用则使用了本书 11.4 节中介绍的 API 函数地址的动态获取技术。
完整源代码见代码清单 16-1:
目标 PE 结构:
本实例程序中用到的所有变量所在位置如图 16-2 所示:
如图所示,PE 文件被分隔成四部分:
口 目标文件 DOS 头。包含目标文件的 DOS MZ 头和 DOS Stub。
口 目标文件 PE 头 + 节表。节表部分除了目标文件的所有的节表项外,还有新增加的节表项,以及节表的最后一个零结构项。
口 目标文件所有节的内容。
口 新增节的内容。
补丁工具程序中用到的主要变量有两个:
口IpPatchPE (目标文件 PE 头起始地址)
口IpOthers (节表后的节内容的起始地址)
另外,还有很多长度变量,这些长度变量基本上分为两类,一类是原始长度,一类是按照文件对齐粒度对齐后的长度。
这些长度与变量之间的关系如下:
IpOthers = dwValidHeadSize+ dwPE_ SECTIONSize+ dwPE_SECTIONSize + sizeof IMRGE_SECTION_HERDER * 2 ;这里的乘以 2 是因为补丁节表占一项,自己额外(不是必须)添加的 0 节表占一项。
IpOthers = dwHeaderSize ; 按文件对齐粒度对齐以后的大小
IpPatchPE = dwValidHeadSize
dwHeaderSize = dwValidHeadSize+ dwPE_SECTIONSize+Sizeof IMAGE_SECTION _HERDER * 2
开发补丁工具:
有了对补丁程序和对 PE 中新增节的认识,下面来开发补丁工具。本节将开发一个能够将补丁程序嵌入到 PE 新增节中的补丁工具,首先来看编程思路。
编程思路:
按照对 “在 PE 新增节中插入程序” 的理解,补丁工具编写的思路应包括以下五步:
步骤1:新建一个节,将该节命名为 “PEBindQL ”。
注意:因为程序与数据是在一起的,所以该节的属性必须定义为: 可读、可写、可执行,即将该节的属性设置为 0C0000060H。
步骤2:将补丁程序的字节码附加到原 PE 文件的未尾。
步骤3:计算新节的相关数据 (如节的名称、节实际尺寸大小、节对齐尺寸、节的属性、节的起始 RVA、节在文件中的偏移等),在节表里将该节添加进去。
步骤4:修改全局变量,如 SizeOfImage、SizeOfHeaders 等。
步骤5:在文件头部修改入口地址,指向新节的可执行代码入口处,同时修正新节最后 E9 指令的操作数,使其返回到原始的文件入口处。
这种操作与在链表的中间部位插入一个元素非常类似,而链表就是机器指令流,插入的元素就是要执行的代码。
补丁工具的开发流程如图 16-3 所示:
如图 16-3 所示,补丁工具的编程思路是根据 对“在PE 新增节中插入程序” 的理解来写的。事实上,在进行程序设计时,考虑到代码功能的一致性原则和程序结构化设计的特点,其具体实现的思路略有不同。比如,编程思路的第一步是通过为变量赋值, 得出新增节所在节表的起始和新增节内容所在文件的偏移来实现的; 第二步附加补丁代码是通过构造新文件数据实现的,后三步则是通过修正字段参数来实现的。
下面将按照源代码中对程序设计的步又分三部分来描述补丁工具的编写,这三部分依次是:
口 为变量赋值
口 构造新文件数据
口 修正字段参数
为变量赋值:
首先来看第一部分,为变量赋值。补丁工具程序需要通过程序计算的变量包括以下项目:
新增加节的节表项所在文件起始偏移 可以由以上变量 IpPatchPE 和 dwPE_SECTIONSize 相加得到; 新增加的节的内容所在文件的起始偏移,则是由以上变量 IpOthers 和 dwSectionsAlignLeft 相加得到。
获得新文件大小以后,按照新文件大小 dwNewFileSize 申请内存空间。
构造新文件数据:
内存空间申请成功后,即可构造新文件的数据了,构造包含以下过程:
步骤1 将目标文件的有效数据部分复制到申请的空间中。
步骤2 复制 PE 头及目标节表。
步骤3 定位到 IpOthers,复制节的详细内容。
步骤4 将补丁代码附加到新的节中。
可以看到,在新文件数据构造的第 4 步,才实现了编程思路中的第 2 步附加代码补丁部分。
修正字段参数:
最后,需要修改文件头部的某些字段和一些参数,以便操作系统加载器可以正确加载并运行新文件。
这些待修正的字段主要包括:
口 新节的 IMAGE_SECTION_HEADER 结构中的所有字段
口 节的个数 (IMAGE_FILE_HEADER.NumberOfSections)
口 DOS 头中的 e_flanew 值 (PE 头相对于文件偏移的起始地址)
口 函数入口地址
口 补丁代码中的 E9 指令后的操作数
口 原节表中节表项目与文件偏移有关的几个字段
口 SizeOfHeaders
口 SizeOfImage
字段参数修正的第一步,就是对新增加的节的节表项字段进行赋值。字段修正还包括 PE 头部字段修正和跳转指令修正。所有工作完成以后,将修改完的新文件数据写入磁盘文件。
主要代码:
以下代码截选自随书文件 bind.asm 的函数 _openFile,函数按照 16.3.1 小节所示步骤编写。由于是按顺序编程,注释比较明晰,读者可以参照注释自己阅读分析这部分代码。
详见代码清单 16-2:
程序首先获取补丁程序代码段的大小,并将该大小按照文件对齐粒度对齐,计算出要新增加的节的大小。用补丁文件代码大小按目标文件的 FileAlignment 字段对齐,所以要打开两个文件。
以下代码计算出增加新节以后的 PE 文件头的大小。一个节增加后,在 PE 头部的节表处会增加一项。
以下代码计算出加入新节后文件的大小。公式为:新文件大小= 目标文件大小+ 多出来的部分 + 新节对齐以后的大小
以下码完成了从目标代码到新文件的数据复制。复制的内容包括 : 目标代码的文件头、新节描述、目标 PE 节数据、补丁代码数据即新节数据。
最后,程序修正了许多参数,这些参数包括 SizeOfImage(内存中整个 PE 文件的映射尺寸)、程序入口、新节数据结构中的各字段值、SizeOfHeaders(所有头+节表按照文件对齐粒度对齐后的大小 (即含补足的 0 ))、补丁代码中的跳转指令操作数等。
运行测试:
以下是通过 bind.asm 为记事本程序打补丁时的输出信息:
小结:
本章通过扩充文件尾部实现新增节,并在文件头部节表部分新增加一个表项。因为节表在文件头部的最后,所以,增加的节表占用了对齐补足的部分,不会影响到其他的数据。
为了降低编写补丁工具的难度,在补丁代码中还使用了前面几章讲述的编程技术,如补丁代码中对涉及地址的地方使用了本书 6.1 节中的代码重定位技术,对导入函数的调用则使用了本书 11.4 节中介绍的 API 函数地址的动态获取技术,等等。因此,大家要学会整体上使用供入补丁框架技术,局部上学习综合运用代码重定位技术和动态加载技术,以增强补丁程序的可移植性,降低补丁工具编写的难度。
在 PE 最后一节中插入程序
与前三章的补丁方法相比,这种方法是最简单也是最有效的一个,在后续章节的实例中几乎都是采用这种方法对目标 PE 实施补丁的。因为 PE 文件的最后一节的数据在文件的末尾,所以,将代码添加到最后一节时无需新增节表项,无需移动现有文件内容。
读者可能会问,如果最后一节是在装载时可以被抛弃的节该怎么办 (比如重定位节)
幸运的是,相对于可运行的 PE 文件来说,由于操作系统为每个进程分配的地址空间是独立的,所以其被装载时总是被放置到指定的位置,所以,在可运行的 PE 里,不存在重定位节。
意思应该是重定位节不会在可运行的程序中,因为一个可运行程序装载的基地址可以说是固定的(优先级最高),只有其它 dll 程序才可能存在基地址被占用而移位的情况。
网络文件下载器补丁程序实例:
本节要完成的补丁程序是一个网络文件下载器,即从网络上下载指定地址的 PE 文件并运行。从网络上下载文件时,会用到动态链接库 wininet.dll,其中包含了 Win32 下与网络有关的函数,利用这些函数实现基于 HTTP 协议和 FTP 协议的服务连接和文件传输等功能。
网络文件下载器 (简称 “下载器” ) 可以用于软件在线自动升级、软件更新、远程管理和控制等领域。下载器首先通过检测本地网络连接状态,确定是否执行下载操作 (本实例会循环检测网络连接状态,直到发现一个连接为止),下载时使用函数 InternetOpenURL 打开指定 URL 地址的连接 然后,使用 InternetReadFile 读取要下载的文件相关数据,并写入本地文件,完成对网络文件的下载。本例中最后还单独开启一个线程尝试执行已下载的文件。
用到的 API 函数:
基于 HTTP (HyperText Transfer Protocol,超文本传输协议) 的文件下载的相关函数在动态链接库 wininet.dll 中。
与本章补丁程序编写有关的图数见表 17-1:
如表 17-1 所示,大部分情况下,每个功能的函数都会有扩展函数,在原函数名末尾添加后缀 “Ex”。如果函数参数中有字符串,则通常还会存在两个版本的相同名称的函数。
例如读取文件的扩展函数 InternetReadFileExA,除此之外,还有一个名称是 InternetReadFileExW。
补丁功能的预演代码:
如果直接编写补丁程序,需要通过补丁工具将补丁程序人嵌入到目标 PE 文件中才能进行测试,这种方法不利于对程序的调试和纠错,因此,对于相对复杂的补丁程序的编写,应该先从它的功能代码开始。方法是编写一个简单的功能预演代码,该预演代码能够实现补丁程序应具备的所有功能;通过对预演代码的调试,能及时方便地发现代码中存在的错误并改正。当所有代码逻辑都没有问题以后,再着手编写补丁程序。
代码清单 17-1 是实现目标补丁的功能代码 (注意,这不是补丁程序):
本程序试图连接 www.jntjdx.com 网站,并从该网站下载文件 gz.doc。
注意:
在实际测试中,读者可以在互联网的某个网站上部署一个可执行文件,然后修改代码中指定要下载的 URL 地址。比如我们可以修改成 https://e00-elmundo.uecdn.es/assets/multimedia/imagenes/2018/11/07/15416159908295.jpg
实际调试中发现需要把第一个 .code 段改为 .data 段,并删除 jmp start 行才可运行:
补丁程序的源代码:
经过补丁功能代码的反复调试,如果没有错误,则可以进行补丁代码的编写。代码清单 17-2 是网络文件下载器的补丁代码,使用了本书 13.3 节介绍的嵌入补丁框架,这里只选取其中的一小部分。
完整的补丁程序请参考随书文件 chapter17\a\patch.asm:
补丁代码和功能代码在逻辑思路上是完全一致的,所不同的是代码的表现方法。在补丁代码中使用了重定位技术和动态加载技术,所以,对每个函数的调用就相对复杂一些。
如测试网络连通状态的代码在补丁功能代码中只有一行:
而在补丁代码中则需要很多行,如下所示:
目标 PE 结构:
在 PE 最后一节中插入程序后 PE 文件的结构如图 17-1 所示:
如图所示,附加代码即为补丁字节码,添加到目标 PE 文件末尾作为最后一节的一部分。
图中标识的各变量解释 (这些变量在补丁工具代码中被定义) 如下:
口 dwLastSectionAlignSize:最后一节对齐后的尺寸 (含嵌入的补丁字节码)。
口dwNewFileSize:补丁后的目标 PE 文件的总大小。
口 dwLastSectionStart:最后一节起始位置在文件中的偏移量。
口@dwFileSize1:目标 PE 最后一节未对齐时的大小 (此大小不包含补丁代码)。
口 dwNewFileAlignSize:附加代码在文件中的起始位置。
开发补丁工具:
和前几章的补丁工具类似,本节的补丁工具完成了对补丁代码的附加,以及对参数的修正操作。
编程思路:
在 PE 最后一节中插入程序的基本思路如下:
步骤1:在PE文件的最后一节将补丁代码附加进去。
步骤2:修改最后一个节表的内容,主要是 SizeOfRawData、PointerToRawData 和 Characteristics 三个字段的值。
步骤3:修正 PE 文件头部的相关字段,这些字段包括 : 映像尺寸 SizeOfImage、函数的入口地址 AddressEntryPoint。
步骤4:修正嵌入补丁框架中 E9 指令的操作数,即代码中的跳转指令地址。
根据以上分析的编程思路,可以知道编写补丁工具的大致流程如下:
1) 获得补丁代码段大小,因为假设补丁程序使用了嵌入框架,所以数据、代码均在代码段中,补丁程序没有数据段定义。
2) 将目标文件按照文件对齐粒度对齐。这主要是为了防止有一些文件结尾时,不考虑对齐粒度,从而导致在最后一节添加内容时造成不对齐。
3) 求最后一节在文件中的偏移。
4) 求最后一节的大小并按照文件对齐粒度对齐。
5) 计算出添加了补丁程序的新文件的大小 (原文件对齐后的值 +补丁大小)。
6) 按照计算出的新文件大小申请内存空间,并将原文件复制到申请的内存空间起始位置。
7) 复制补丁代码到 dwNewFileAlignSize 处。
8) 计算最后一节的 SizeOfRawData 和 Misc 的值,并更正 , 然后,设置该节的属性为可执行、可读、可写,即 0c0000060h。
9) 修正文件头部关键字段 SizeOfImage。
10) 修正函数入口地址和嵌入补丁框架最后的 E9 转移指令的操作数。
11) 将内存中的内容复制到磁盘文件中。
主要代码:
本节补丁工具的编写借用了第 2 章介绍的通用窗口程序框架 pe.asm,补丁工具的主要代码在函数 _openFile 中。
代码清单 17-3 列出了本节补丁工具的主要代码:
运行测试:
编译链接补丁工具代码,使用生成的 bind.exe 对记事本程序进行测试,相关文件在随书文件的目录 chapter17\a 中。
以下是使用补丁工具对 notepad.exe 打补丁的运行结果:
程序下载成功:
小结:
本章主要讨论了在 PE 最后一节附加代码的方法。相比前三章的方法,这种方法工作量最少、编码最简单、要修正的值也最少。所以,这种方法被广泛用于各种场合的静态补丁中。通过学习本章,读者还可以掌握编写复杂补丁程序的方法,即先编写补丁程序的功能预演代码,然后按照静态嵌入补丁框架的编写原则和逐句进行修改。