目录

PE补丁技术

动态补丁:

进程间的通信机制:

读写进程内存:

目标进程枚举:

执行远程线程:

静态补丁:

整体替换 PE 文件:

整体替换 DLL 文件:

部分修改 PE 文件

嵌入补丁程序:

嵌入补丁程序框架:

嵌入补丁程序编写规则

嵌入补丁字节码实例分析:

万能补丁码

原理:(入口函数处放补丁代码)

字节码:

运行测试:

小结:


PE补丁技术

第 12 章介绍了 PE 变形技术,该技术研究的是程序的字节码; 本章来研究 PE 补丁技术, 该技术侧重于研究使用 Masm32 编写的补丁程序,而非程序字节码。PE 补丁技术被广泛应 用于 PE 病毒、PE 加密解密等领域,通过对目标程序戏入不同的补丁程序,可以实现不同的 目的。

PE 补丁分为动态补丁和静态补丁,其中静态补丁框架由两部分组成 : 补丁程序和将补丁 程序附加到目标 PE 的补丁工具。

动态补丁:

动态补丁是指目标 PE 处于活动状态时(即进程)为其实施的补丁。PE 文件被映像加载 器装载到内存后,就变成了进程,由 Windows 子系统调度 PE 映像里预先存放的指令代码完成指定的功能。前面讲过,每个进程其存取空间为 4GB,各进程的地址空间独立,相互之间并不影响,动态补丁技术即要求我们打破这种传统的认识,实现一个进程可以操作另外一个进程的地址空间。动态补丁常用于游戏修改器、动态调试、病毒生存等领域。

一个完整的动态补丁一般需要具备以下四个要素:

口 与其他进程通信的能力。

口 良好的读写其他进程地址空间的能力。

口 能正确识别要补丁的目标进程。

口 在其他进程地址空间执行代码的能力。

进程间的通信机制:

在实施补丁过程中,两个进程之间会相互交换数据,如补丁程序必须动态获取目标进程运行的状态,以确定在什么时候,什么地点实施补丁。这些信息的传递需要用到 Windows 系统中进程间的数据通信机制。

在 Windows 中,实现进程间通信的机制有很多方法,归纳一下分为两大类:

一种是通过两个进程实施的耦合性强的进程间通信。这种通信机制要求参与通信的两个进程必须密切配合,两个进程工作在 服务器/客户端 模式。这类通信机制主要包括匿名管道、命名管道、邮件槽、远程方法调用等。

另外一种是由第三方参与的耦合性相对较弱的进程间通信,比如通过剪贴板、共享内存、动态链接库、映射文件、注册表、一般文件、Socket、Windows 消息队列、信号量等。

以下是常见的进程通信机制:

1. 管道技术

管道 (pipe) 是一种具有两个端点的通信通道,两个端点分别连接两个进程。管道可以是一个方向的,也可以是两个方向的;连接管道的两个端点既可以从管道中读取数据,也可以将数据写进管道。

匿名管道 (Anonymous Pipe) 存在于父进程与子进程之间,由于它连接了两个具有继承关系的进程,所以该管道不需要名字,管道的创建由父进程完成。匿名管道是单机上实现子进程标准 I/O 重定向的有效方法,它无法在网络上使用,也不能用于两个不相关的进程。创建匿名管道的 API 函数是 CreatePipe。

命名管道 (Named Pipe) 是服务器进程和一个或多个客户进程之间通信的单向或双向管道。创建管道的服务器端在建立管道时会给管道指定一个名字,其他任何进程都可以通过这个名字打开管道的另一端,并根据给定的权限与创建管道的进程实施通信。创建命名管道的 API 函数是 CreateNamedPipe。

2. 邮件槽

单一的邮件槽(Mail Slots)提供了两个进程间的单向通信和能力。由一个进程建立邮件槽从而成为邮件槽服务器,而其他进程,则通过邮件槽的名字向服务器发送销息。该消息一直处在邮件槽中直到服务器读取它。这种机制与命名管道的机制类似,都是基于 SOCKET 技术通过端口实现的,但两者传递数据的协议不同。

如果要建立双向的通信,则客户端也可以建立相同的邮件槽,从而使得客户端同时有具备服务器和客户端两种角色。两个这样的进程连在一起就形成了一种双向的可读写的通信通道。由于邮件模使用了不可靠的数据报协议,所以其通常用于广播消息。创建邮件槽的 API 函数是 CreateMailslot。

3. 剪贴板

剪贴板(Clipped Board)是为应用程序之间进行数据共享而提供的一个第三方的数据存储区。两个需要传递数据的进程无需进行协商,由一方通过剪切(或复制)操作实施数据的转移,另一方则可以在任何时刻(保证剪贴板中数据没有被重新覆盖)从剪贴板中取回数据。进程间存取数据唯一的限制是两者必须使用同样格式的数据。

4. 共享内存

共享内存是文件映射机制的一个特例。内存映射文件 (Memory Mapped Files,就是分页机制原理) 将磁盘不连续存储的文件复制到连续内存空间中,在前面有所介绍。Win32 API 允许多个进程访问同一文件映射对象,各个进程在它自己的地址空间里接收指向内存线性文件的指针。通过使用这些指针,不同进程就可以读写文件的内容,从而实现对文件中数据的共享。(指针指向内存映射文件来读写)

Win32 API 中共享内存 (Shared Memory) 实际是文件映射的一种特殊情况。进程在创建文件映射对象时用 0xFFFFFFFF 来代替正常的文件句柄,表示对应的文件映射对象是从操作系统页面文件来访问内存(这里指操作系统的磁盘文件),其他进程只要打开该文件映射对象就可以访问该内存块,进而实现多进程共享同一段内存数据的目的。建立内存映射对象的 API 函数是 CreateFileMapping,其他进程访问内存映射文件的 API 函数是 OpenFileMapping。

5. 消息机制

消息机制是 Windows 应用程序的核心,在 Windows 中发生的大部分事件都可以用消息来表示。消息可以告诉操作系统发生了什么,所有的 Windows 应用程序都是消息驱动的。可以说,消息机制是 Windows 系统间、进程间传递数据的最好的方法。

窗口移动、鼠标点击、键盘按键等事件的发生,以及程序的启动或退出都会产生标准的 Windows 消息。这些消息告诉 Windows 操作系统(或接管了消息处理的程序)当前系统 (或进程) 的运行状态。当然,Windows 也提供了一些其他的非标准的消息,如异常消息,这些消息与系统的某些机制相关。

动态补丁程序使用消息机制,配合内存读写来实现进程间数据传递会相对容易些。一个进程在运行时会根据运行状态产生各种消息(比如,因异常事件触发的异常消息、程序中由中断指令引发的调试消息等),这些消息会通过进程的外露端口(如异常调试消息经由异常端口)传输出去。由于引发异常调试消息事件 EXCEPTION_DEBUG_EVENT 的指令最为精简(即 int 3 指令,一个字节,十六进制字节码为 0CCh),所以这种进程间传递数据的方式被普遍用在动态补丁技术中。

通过该消息传递数据的流程如图 13-1 所示:

如图所示,补丁工具通过进程内存读写技术将补丁代码写人目标进程指定位置 (补丁代码为 int 3,即 0CCh) , 同时,补丁工具记录该位置的字节值。当目标进程执行到该位置时将产生异常,操作系统将该异常产生时的相关信息 (各寄存器的值等) 包装到数据结构 EXCEPTION_RECORD 中,并查找此时调试目标进程的进程 (即动态补丁工具),操作系统将该结构通过消息传递给动态补丁工具, 然后,由动态补丁工具完成对目标进程信息的解读,将解读以后的相关信息再有选择地重新写回到目标进程地址空间,以改变目标进程的运行行为或当前的进程状态。

读写进程内存:

读写其他进程内存地址空间是动态补丁必须具备的功能,Windows API 中提供了读写其他进程地址空间的函数。首先介绍这些相关的函数,。

相关函数

Windows 的安全机制不允许一个进程直接读写其他进程空间的数据,除非使用了特定的 Windows API 函数:

(1) OpenProcess 函数

OpenProcess 函数用来打开一个已存在的进程对象,并和返回进程的句柄。

函数原型定义如下:

各参数解释如下:

1) dwDesiredAccess: 访问权限。它可以是表 13-1 所列的值。

2) blInheritHandle: 继承标志:如果设置为TRUE,表示继承打开的进程,否则表示不继承。

3) dwProcessId: 进程的 ID 号。

4) 返回值 : 如成功,返回值为指定进程的句柄 ; 如失败,返回值为空。可调用 GetLastError 获得错误代码。

(2) ReadProcessMemaory 函数

读进程内存韩束。

以下是函数原型:

各参数解释如下:

1) hProcess: 远程进程的句柄,远程进程即为要操作的进程。

2) pvAddressRemote: 要操作的进程的地址空间,该地址为 VA。

3) pyBufferLocal: 存放要操作的数据的本地缓冲区。

4) dwSize: 本地缓冲区大小。

5) pdwNumBytesRead: 输出参数,表示本次读取的实际字节数。

6) 返回值: 如成功,返回 TRUE,否则返回NULL。

(3) WriteProcessMemory 函数

写进程内存函数。

完整定义如下:

读进程内存和写进程内存的函数的参数定义是一样的,各参数的解释如下:

1) hProcess: 指定将要被读写的目标进程句柄。

2) pvAddressRemote: 目标进程中被读写的起始线性地址。

3) pvBufferLocal:用来接收读取数据的缓冲区(对于 ReadProcessMemory 函数)或者要写到目标进程的数据缓钟区(对于 WriteProcessMemory 函数)。

4) dwSize: 要读写的字节数。

5) pdwNumBytesRead : 指向一个双字变量,供函数返回实际读写的字节数 ,如果不关心这个结果,可以将其设置为NULL。

6) 返回值: 如果函数执行成功,那么返回值是非 0 值,执行失败的话返回 0。

读写进程内存实例分析:

如果大家经常使用 PEInfo 小工具,就会发现该工具在查看一些有大量数据的PE 时,经常会出现界面 “死住” 的情况,关于原因和解决方法已经在第 2 章里讲过了,是通过给显示输出函数单独设立一个线程解决,现在来看看通过动态补丁技术如何解决这一问题。

(1) 修改代码

为了简化问题的描述,配合大家理解进程内存读写,本实例采用了一些技巧,比如在需要打补丁的 PEInfo.asm 中,做了如下修改。

步骤1 在数据段中添加一个标志 dwFlag:

步骤2 在代码中增加检测标志位的代码:(原始代码在重定位目录处,已分析好)

在两层循环中均增加了对标志位的判断,如果该标志位值为1,则退出循环。因为 dwFlag 在定义时被设置为值 0xffffffff,并且 chapterl3\PEInfo.asm 中再也没有与 dwFlag 有关的赋值代码,所以,该程序中这个退出条件似乎永远也不会发生。

(2) 补丁工具源代码

动态补丁技术是通过修改进程内存空间数据,使被修改进程发生程序流向转移的技术。起到这种作用的程序一般称为补丁工具,代码清单 13-1 是补丁工具的部分源代码 (完整代码请参照随书文件 chapter13\DPatchPEInfo.asm )。

计算 STOP_FLAG_POSITION 在进程中的 VA:

STOP_FLAG_POSITION 是 PEInfo.asm 中定义的标志 dwFlag 所在进程地址空间中的 VA 值。

首先,用 PEInfo 查看该文件数据节的信息:

再在 Hex 中找到对应的预定义数据:(附上前后的变量值做参考)

根据 RVA 和 FOA 的换算关系可以得出,停止标志位的内存地址 (VA) 为 0x00404115。在 DPatchPEInfo.asm 的开始部分声明一个常量即可,STOP_FLAG_POSITION=00404115h

(3) 运行测试

运行测试的整体思路是 : 当 PEInfo 小工具运行时,如果用户发现要获取的信息已经输出,则可以通过另外一个程序终止 PEInfo 代码的运行(注意,不是终止进程的运行),看起来好像这个补丁程序也参与了 PEInfo 指令代码的流程控制过程。

现在来看该动态补丁的三要素:

1) 该补丁可以读写 PEInfo.exe 的进程内存。

2) 该补丁的调用时机由用户判断。当发现需要的信息已经输出,即选择菜单选项 “文件” | “停止遍历重定位表”,补丁会立刻生效,补丁的位置已经事先计算出来了。

3) 尽管补丁没有实现代码部分,为了简化任务,代码事先已经安排到原始程序中等待补丁对它的激活。以后我们看到的大部分的动态补丁则是将要追加的代码通过补丁工具直接写入补丁程序。

扩展阅读,关于热补:

通常情况下,补丁程序总是在被补丁的程序后期开发的,有时候,为了后期补丁方便,有些程序会事先给自己留一个“后门"。 比如,微软的大部分内核函数,检查 Windows 的 ntdll.dll 中大部分函数的源代码,其起始位置的代码看起来总是这样的:

“mov edi,edi” 指令为两个字节,可以存放短跳转指令,短跳转指令指向的位置可以存放一个指向长跳转指令的地址,从而在运行期实现热补 (hotfix ) 。

在 Visual C++ 的编译器选项中也有类似的参数 /hotpatch 可以创建可热修补的 PE 映像。

目标进程枚举:

在进行动态补丁时,有一步是必需的,即获取目标进程的句柄或者 ID 号。通过枚举系统进程即可获取这些信息。

1,枚举系统进程的方法,常见的有 4 种:

(1) 调用 PSAPI.DLL 提供的函数

该动态链接库是微软 Windows NT 开发小组开发的与进程有关的函数集。核心函数包括;

(2) 调用 ToolHelp API 提供的函数

ToolHelp32 函数是一组存储在 Kernel32.dll 中的 Windows API 函数,它能够通过 Snapshot (内存快照)获得驻留在系统内存中的进程表、线程表、模块表和堆表。核心函数包括:

(3) 调用 ntdll.dll 中未公开的 API 函数

在 ntdll.dll 中,有一组以 NtQuery 开头的函数集,利用其中的函数可以获取系统相关数据结构的信息,其中就包括进程信息。核心函数包括:

(4) 调用 PDH.DLL 中的 API 函数

PDH(Performance Data Helper,性能数据辅助数据库) 中包含了大量的信息,例如 CPU 使用率、内存使用率、系统进程信息等,该数据库既可以通过注册表函数来访问,也可以通过动态链接库 PDH.DLL 提供的系列函数来访问。核心函数包括:

2. 遍历系统进程示例分析

下面以第二种方法为例,即调用 ToolHelp API 提供的函数,实现遍历系统进程,以获取指定进程关联模块列表。

(1) 获取指定进程关联模块列表的源代码见代码清单 13-2:

主程序通过以下代码调用两个函数:

(2) 测试运行,运行界面见图 13-2:

执行远程线程:

大部分的动态补丁最后一步要实现代码的运行,即确保插入到目标进程内存的数据要具备运行权,并能最终运行起来。在一个进程中要运行插入的代码,最好的办法就是通过远程线程技术,即将一段代码挂接到目标进程中,并指定代码块作为该目标进程的一个线程来运行。

相关函数——Windows API 为远程线程执行提供了函数支持下面是相关步骤:

(OpenProcess 函数和 WriteProcessMemory 函数在前面已介绍过)

步骤1 使用 OpenProcess 函数打开目标进程,获取进程操作句柄。

步骤2 使用 VirtualAllocEx 函数在目标进程中分配内存。

步骤3 使用 WriteProcessMemory 函数将远程代码写入。

步骤4 使用 CreateRemoteThread 函数在目标进程中创建远程线程并执行。

(1) VirtualAllocEx 函数

其中各参数解释如下:

1) flAllocationType:内存分配属性,可取下列值:

口MEM_COMMIT,为特定的页面区域分配内存中或磁盘的页面文件中的物理存储。

口MEM_PHYSICAL,分配物理内存(仅用于地址窗口扩展内存)。

口MEM_RESERVE,保留进程的虚拟地址空间,而不分配任何物理存储。保留页面可通过继续调用函数 VirtualAlloc 而被占用,直到最终提交。

口MEM_RESET,指明在内存中由参数 lpAddress 和 dwSize 指定的数据无效。

口MEM_TOP_DOWN,在尽可能高的地址上分配内存。

2) FlProtect:分配区的页面属性,可取下列值;

口PAGE_READONLY 区域为只读,如果应用程序试图访问区域中的页的时候,将会被 拒绝访问。

口PAGE_READWRITE 区域可被应用程序读写。

口PAGE_EXECUTE 区域包含可被系统执行的代码。试图读写该区域的操作将被拒绝。

口PAGE_EXECUTE_READ 区域包含可执行代码,应用程序可以读该区域。

口PAGE_EXECUTE_READWRITE 区域包含可执行代码,应用程序可以读写该区域。

口PAGE_GUARD 区域第一次被访问时进入一个 STATUS_GUARD_PAGE 异常,这个标志要和其他保护标志合并使用,表明区域被第一次访问的权限。

口PAGE NOACCESS,任何访问该区域的操作将被拒绝。

口PAGE_ NOCACHE RAM 中的页映射到该区域时将不会被微处理器缓存(cached)。

注意:

PAGE_GUARD 和 PAGE_NOCHACHE 标志可以和其他标志合并使用,以进一步指定页的特征。PAGE_GUARD 标志指定了一个防护页 (guard page),即当一个页被提交时会因第一次被访问而产生一个 one-shot 异常,接着取得指定的访问权限。PAGE_NOCACHE 防止当它映射到虚拟页的时候被徽处理器缓存,这个标志方便设备驱动使用直接内存访问方式(DMA)来共享内存块。

返回值:

如果执行成功就返回分配内存的首地址,不成功则返回 NULL。

(2) CreateRemoteThread 函数

各参数解释如下:

1 ) hProcess,目标进程句柄。

2) lpThreadAttributes,线程安全描述字,一个指向 SECURITY_AITRIBUTES 结构的指针。

3) dwStackSize,线程栈大小,以字节表示。

4) lpStartAddress,一个LPTHREAD_START_ROUTINE 类型的指针,指向在远程进程中执行的函数地址。

5) lpParameter,传入参数。

6) dwCreationFlags,创建线程的其他标志。

7) lpThreadId,(输出),返回线程号,如果为NULL,则不返回。

8) 返回值: 如果成功返回新线程句柄,失败返回NULL,并且可调用 GetLastEror 获得错误值。

向目标进程植入远程线程示例分析:(链接 remotethread.obj 时需要指定代码段属性为可读、可写和可执行)

本示例的目标是在进程 PEInfo.exe 中插入一个线程代码,运行并显示 HelloWorldPE 弹出对话框。

(2) 运行测试

从运行结果看,弹出的对话框继承了所属进程的部分特性,比如图标,如图所示:

静态补丁:

前一节节介绍的动态补丁是针对 PE 映像内存空间的,本节要介绍的静态补丁技术则是针对 PE 本身及相关联文件的。静态补丁常用于 PE 加密、汉化、软件升级等领域。

静态补丁可以通过对 PE 文件进行整体替换、对动态链接库进行替换、部分修改 PE 文件,即向 PE 文件附加代码并劫持映像入口等技术来实现,下面将分别讲述。

整体替换 PE 文件:

软件升级时,经常要做的事情就是对旧版的 PE 文件实施整体替换,下面以一个通过网络环境自动完成程序升级的例子来介绍这种静态补丁技术。

1. 设置网络升级环境

在网络上放置两个文件,一个是 PE 版本描述文件,一个是 PE 新版本文件。在本例中分别为:

PE 版本描述文件 version.ini 由客户端下载,并识别客户端当前部署的程序是否为新版本, PE 新版本文件则用于替换客户端旧版本文件。

version.ini 的内容如下:(节、节下索引和对应值)

version.ini 文件中记录了两个可以升级的文件,通过这种方式可以定义更多的供客户升级的文件。

每个文件均由以下三个项构成:

口 majorImageVersion (程序主版本号)

口 minorImageVersion (程序次版本号)

口 downloadfile (部署在服务器上的该版本对应的文件名称)

2. 用户执行升级程序

由用户运行升级程序 update.exe,该程序从网络下载 PE 版本描述文件,并与当前 PE 文件版本进行比对; 如果发现当前版本低,则下载对应的新版本文件,并保存。

在对比时,可以使用 PE 文件头部的两个字段:

主进程在释放 update.exe 以后,将该信息(PE 文件头的主次版本号字段值)连同进程的 PID 号一起写人 update.exe 文件的指定位置(注意要运行才有进程),便于 update.exe 实施对比,下载文件与判断是否新版本的操作均在 update.exe 里完成。当然,大家也可以在主程序里维护这两个变量,而无需将版本信息写入 PE 头部,运行效果是一样的。

3. 完成新版本文件的替换

update.exe 提示用户有新程序可升级,从服务器上下载 version.ini 中记录的新版本文件,结束当前运行的旧版本的 PE 文件对应的进程,并通过重命名的方式实现新版本文件与旧版本文件的替换。

注意:

update.exe 升级程序是以资源的形式存储到 DPatchPEInfo.exe 文件的,也就是 DPatchPEInfo.exe 的一个菜单项,直到用户选择了升级菜单选项才被释放出来,并执行升级操作。

升级程序 update.asm 的源代码见代码清单 13-4:

如代码清单所示,升级程序 update.asm 的主要思路是:

行 53 一56: 调用函数 URLDownloadToFile 下载 version.ini,并写入.\tmp.ini中

行 57 一67: 调用函数 GetPrivateProfileInt 获取文件在服务器上的最新版本号(包含主版本和次版本)。

行 71 一74: 用新获取到的版本号与旧版本号对比,判断文件是否需要更新。在这里要特别注意,旧版本号在本程序中并没有任何一个语句或函数为其赋值。它的值来自另一个程序,这个程序就是释放 update.exe 的主程序 DPatchPEInfo.exe。

行 80 一123: 要升级的文件在服务器上有新版本,首先通过 version.ini 获得文件名,然后连接网络下载该文件,结束当前主进程,替换旧版本文件。

DPatchPEInfo 主程序要做的事情包括:

(1) 在资源文件中定义资源

在资源文件 DPatchPEInfo.rc 里定义资源 IDB_UPDATE,资源的内容为 update.exe 文件的字节码:

(2) 在主程序代码中维护版本

在主程序开始定义两个常量,用来标识主程序的主版本和次版本。该值在释放 update.asm 时将被传入 update.asm 的数据段定义的变量中。

(3) 编写调用升级补丁代码

在窗口回调函数中定义对菜单 “选项”|“检查网络升级” 的响应代码,如下所示;

4. 运行测试

步骤1 正常编译 update.asm 源代码,链接 update.obj 生成 update.exe 文件

步骤2 使用如下命令编译链接直至生成 DPatchPEInfo.exe 文件。(这里update.exe的字节码是编译在了rc文件中的)

步骤3 用 OD 打开 DPatchPEInfo.exe,查找需要修正的,即要在释放 update.exe 时要写入的三个变量位置:

红框框起部分为三个需要修正的变量位置。如上所示,记录起始地址为 0x0040528e。

步骤4 修正源代码 DPatchPEInfo.asm 中的部分指令操作数的值。

步骤5 确定要修改的变量的位置后,重新执行步骤 2 操作,生成新的 DPatchPEInfo.exe 文件。

步骤6 在服务器上部署 “升级文件” 与 “升级配置文件” ,在客户端运行旧版本 (通过修改两个常量可改变程序版本) 的 DPatchPEInfo 进行升级。

整体替换 DLL 文件:

Windows 操作系统在运行程序时大量使用动态链接库技术,所以静态补丁的目标除了 PE 文件本身外,还包含与 PE 文件相关联的动态链接库文件。通过对导入函数的持有者 DLL 进行替换 (专业术语为劫持),也可以实现针对 PE 的静态补丁。动态链接库劫持 (DLL HOOK)技术经常用于病毒与反病毒程序领域。

1. 静态 DLL 调用与动态 DLL 调用对比

DLL 是用于向主程序提供函数调用的,在大多数的 PE 中,DLL 是被 PE 加载器加载到进程地址空间的。PE 程序对 DLL 的调用分为静态调用和动态调用两种,相对这两种不同的调用方法,DLL 动持的方法也不一样。

首先来看针对静态 DLL 调用的劫持。如果有恶意用户在加载前对 DLL 进行替换,使用一个自己定义的 DLL 替换程序原有的 DLL,且保证替换后的 DLL 中存在原有 DLL 中所有函数的实现,那么,当进程运行调用到 DLL 的函数时,就等于运行了补丁后的DLL。

向静态 DLL 调用中的主程序实施补丁的流程见图 13-4:

如图 13-4 所示,静态调用 DLL 的主程序在被加载器加载到内存前,就已经更换了主程序调用的动态链接库文件。主程序本来应该直接调用原始 DLL 的,通过定义补丁DLL,使得主程序的调用发生转向,而对原始 DLL 的调用则转由补丁DLL 实现。

下面来看针对动态 DLL 调用的劫持。在有些情况下,如 PE 使用了动态加载技术或者使用了延迟加载导入技术,那就更简单了,在运行期替换 DLL 都可以。

向动态 DLL 调用中的主程序实施补丁的流程见图 13-5:

在使用了动态 DLL 调用 (如 PE 中存在延迟加载导入特性) 的程序中,补丁 DLL 可以在加载前实施,也可以在运行期实施。而静态 DLL 调用的程序则只能在加载前执行补丁程序。

有一些 DLL 文件在程序部署时会被复制到系统目录中,与应用程序不在同一个目录,而PE 加载器在加载 DLL 文件时会有一个搜索指定 DLL 文件的过程,这个过程会按照一个特定顺序的路径逐个查找,当前目录是这个搜索过程中第一个要查找的目录。只要将同名的 DLL 放置到与应用程序相同的目录下,使得加载程序认定该 DLL 即为要加载的 DLL 文件,这样 DLL 劫持就发生了。

注意:

这里提到的动态 DLL 调用中的 “动态” 与动态补丁中的 “动态” 是完全不同的两个概念,动态 DLL 调用描述的是 PE 调用 DLL 的方式,动态补丁则是针对进程的。动态 DLL调用与补丁是静态还是动态无关,读者不要被文字表象迷惑了。

2. 动态 DLL 劫持实例

下面通过一个例子,介绍 DLL 劫持。本例中的 PE 文件使用了动态 DLL 调用,所以发生的劫持是基于动态 DLL 调用的,替换原始 DLL 文件的操作可以在运行前进行,也可以在运行期 DLL 中的函数未调用前进行。相关文件在随书文件的 chapter13\b 目录中。以下是实例的详细步骤。

步骤1 编写新的 DLL:(新 DLL 要用原始 DLL 来掩护)

以 winResult.dll 为例,编写新的 DLL,通过动态加载原始 DLL 获取原始 DLL 中函数的地址,相关代码如下:

对原始 DLL 的加载在新的 DLL 的入口函数中实现,加载后通过函数 GetProcAddress 获取原始 DLL 中各函数的地址,以备后用。

步骤2 重写导出方法:

在新 DLL 中的相关方法中,先执行补丁代码 (这里显示一个对话框),然后跳转到正常的函数处执行。以 FadeInOpen 函数为例,相关代码如下:

实验测试:

ml -c -coff winResult.asm

link -dll -subsystem:windows -def:winResult.def winResult.obj

实验测试 2,把被劫持的 DLL 放入 C:\windows\ 目录下,就会有循环弹框:

因为劫持的 DLL中 是 invoke MessageBox,NULL,addr szHello,NULL,MB_OK 和 invoke _fadeInOpen,hWin,也就是说没有 _fadeInOpen实体,只有不断的循环调用自己,所以这是递归调用 MessageBox 弹框。

实验测试3:

要想使劫持发生,需要具备以下三个条件:

1) 原始的 DLL 被移动到系统目录,或者被更名。

2) 新的 DLL 必须包含所有原始 DLL 导出的函数。

3) 新的 DLL 在执行补丁程序后必须调用原始 DLL 函数相关代码,对 DLL 的替换及相关的测试请读者自行完成。

部分修改 PE 文件

与整体替换 PE 文件所不同的是,部分修改 PE 文件是向 PE 文件附加代码。这种静态补丁技术又被称为嵌入补丁技术,主要是利用 PE 变形技术将代码嵌入到原始 PE 文件里,然后修改程序流程转向以达到运行自己的目的。它也是 PE 进阶部分重点讨论的内容,本章随后还会用专门的一节 (13.3 节)来描述基于这种技术的汇编程序编写。

PE 被加载到内存以后,将执行入口地址指向的代码,该地址由文件头部的字段 IMAGE_OPTIONAL_HEADER32.AddressOfEntryPoint 来决定。

要想将成段代码嵌入到已发布的 PE 文件中,需要解决两个问题 :

一个是代码空间问题,另一个是流程转向问题。下面首先来看流程转向问题,有两种方法可以实现。关于空间的问题,在后续的章节中将依据不同的补丁方法单独描述。

第一种实现方法:

利用补丁工具将入口地址修改为附加补丁代码的起始地址,并保存该原始的入口地址。当进程的第一个线程开始工作的时候,补丁程序先工作,在附加的补丁代码最后有一个跳转指令,由该跳转指令跳转到原始的入口地址处执行。

流程转向如图 13-6 所示:

另一种方法是通过覆盖代码实现流程转向。这种方法不需要修改入口地址,而是通过修改入口地址处的代码来完成,流程转向如图 13-7 所示。

与第一种方法一样,补丁程序自己要记录原始入口地址位置的指令字节码。完成功能返回该处前,首先恢复原始代码处的指令字节码内容,再完成跳转。为了提高补丁代码的可移植性,补丁代码需要完全独立,即所有的代码尽量不与原代码发生关系 (除了跳转以外),只有这样才能适应各种目标程序。要想实现这个目标,需要在编写补丁代码时,

尽量使用局部(即栈)变量、重定位技术,以及动态 DLL 载人技术。

嵌入补丁程序:

因为后面的 4 章内容都是围绕嵌入补丁技术来写的,所以,有必要了解一下该技术的编程方法。对嵌入补丁的讨论将会涉及补丁程序本身和补丁工具(将补丁合理合法地和戏入 PE 文件内部,而不影响 PE 运行的一个程序) 。本节主要讲补丁程序本身,因为补丁工具会因嵌入补丁所在 PE 中的位置不同而有所不同,所以补丁工具的编写将分别在接下来的第 14 ~ 17 章中介绍。首先来看嵌入补丁程序框架。

嵌入补丁程序框架:

框架是一项工作开始的基础,代码清单 13-5 是基于汇编语言的嵌入补丁程序框架:

黑体部分为一个框架程序的大部分内容,包括引入函数声明、全局数据变量定义、局部变量定义、补丁功能码等。从框架中可以看出代码流程的转向过程:程序先调用函数 _start (行 297) 初始化一些变量; 然后在该函数最后调用了补丁代码程序 _patchFun (行 276 一 283 ); 执行完补丁代码以后,通过一个跳转指令 E9 返回到原始入口地址处执行。

注意:E9 指令的地址必须通过补丁工具进行修正。

嵌入补丁程序编写规则

在编写补丁程序程序时,必须考虑的因素有:对补丁代码中用到的全局变量的数据处理、通过 DLL 基地址和函数名获取其他函数地址的方法、补丁函数的调用方法等。

1. 全局变量及局部变量的数据处理

对全局变量的引用需要考虑重定位问题,如下所示:

在整个程序框架中,ebx 寄存器存放了用于修正全局变量地址的值,获取的全局变量与 ebx 相加后即可得到重定位后的正确地址。

局部变量即为栈里的变量,可以直接操作,如:

2. 获取 DLL 基地址方法

以下代码演示了如何获取某个特定 DLL 的基地址。该 DLL 以名称字符串标识,通过调用函数 kemmel32.dll!LoadLibraryA 将指定的动态链接库加载进内存并得到该动态链接库的基地址。

3. 获取其他函数地址

知道了函数所在动态链接库的基地址和函数的名字,通过调用函数 GetProcAddress 即可获取动态链接库中的导出函数的地址:

4. 补丁函数代码调用其他公有函数

如果在框架中添加了其他函数,那么,在调用这些函数的时候必须通过使用地址的方式,如下所示:

凡是全局变量 (包含公用函数地址) 均需要加 ebx 进行修正,局部变量 (在栈中,如函数传递的参数,函数内部定义的变量等) 则可以直接使用。

以上简单描述了使用嵌入补丁通用框架编程时需要注意的几个问题,下面来看通用补丁程序框架的字节码。

嵌入补丁字节码实例分析:

以下为 HelloWorldPE 补丁的字节码内容:

如下所示,字节码开始和结束都是跳转指令,前一个跳转指令跳转到该数据块的起始代码处运行,后一个跳转指令跳转到目标程序的入口地址处执行(但是这里并没有修改成起始地址)。

对最后的跳转到原始的入口处 E9 F0FFFFFF进行分析:

其段内跳转到 sub ebx offer @B 处,然后下一步又是 invoke _start 指令,我一开始好奇无限循环调用是怎么正常对出的,结果发现是异常调用,但是异常处理程序 _SEHHandler 并不起作用。

整个数据块具备良好的可移植性,测试目标定位为 PEInfo.exe:

将补丁字节码覆盖 PEInfo 入口地址 (文件偏移 0xF5F) 开始的指令字节码,然后运行 PEInfo.exe。你会发现 PEInfo 整个变成了 HelloWorldPE 程序。

万能补丁码

本节介绍一种基于供入补丁技术的万能补丁码,相关文件在随书文件 chapter13\d 目录中。

原理:(入口函数处放补丁代码)

当一个进程动态加载外部 DLL 文件时,除了将 DLL 内容上映射到内存外,还会执行 DLL 的入口函数 ,而动态加载 DLL 的 API 函数是 kernel32.dll 中的 LoadLibraryX 系列函数。所以,补丁代码需要做的工作就是找到内存中的 kernel32.dll 的基地址,然后查找其导出表获取 LoadLibraryA 的 VA,该地址在该系统下肯定是可用的。紧接着执行该 LoadLibraryA 函数,加载特定的 DLL 程序 (在万能补丁码的例子中为 pa.dll)。

由于这个 DLL 的入口函数中存放着补丁代码,所以一加载这个 DLL 就顺带执行了入口函数处的补丁代码。万能补丁码就是通过这种原理实现的一种补丁技术。因为其小巧,可移植性强,所以可用性比较高。

图 13-8 展示了万能补丁工作原理:

补丁代码 -1 负责嵌入目标 PE 文件内部,执行加载动态链接库 pa.dll 的任务,在加载时会首先调用 pa.dll 的入口函数。在 pa.dll 的入口函数中,嵌入真正要打的补丁是补丁代码 -2,通过这样一种传递方式,使得补丁程序可以脱离开目标 PE 文件而获得执行。这样生产的补丁代码具有更大的扩展空间,所以称为万能补丁码。

下面介绍万能补丁码的源代码:

字节码:

使用 FlexHex 打开最终生成的 getLoadLib.exe,将代码段的字节码复制出来如下所示。该代码在源代码中已有比较详尽的描述,经过特意调整动态链接库的名称为 “pa” ,即 “patch.dll” 的意思。调整后的大小为 80h,即 128 个字节,这个尺寸要比最小的弹出对话框的 PE(133 字节) 还要小,但它实现的功能却超出你的想象 !

运行测试:

小结:

本章主要描述了对 PE 进程和 PE 文件进行补丁的不同方法,即动态补丁和静态补丁。重点对静态补丁中的基于 PE 文件的嵌入补丁技术进行了描述,内容包括补丁程序框架、补丁编写规则等。此外,还为读者描述了一种基于典入补丁技术的万能补丁码。后续四章将分别介绍对 PE 文件实施嵌入补丁的四种不同方法,因此,熟练掌握本章的内容是学习后续章节的基础。

WINDOWS+PE权威指南读书笔记(20)相关推荐

  1. WINDOWS+PE权威指南读书笔记(26)

    目录 EXE 捆绑器 基本思路: EXE 执行调度机制: 控制进程同步运行实例分析: 字节码转换工具 hex2db: hex2db 源代码: 运行测试: 执行调度程序 _host.exe: 主要代码: ...

  2. WINDOWS+PE权威指南读书笔记(27)

    目录 软件安装自动化 基本思路: 补丁程序 patch.exe: 执行线程函数: 简单测试: 消息发送器 _Message.exe: 窗口枚举回调函数: 调用窗口枚举函数: 向指定窗口发送消息: 消息 ...

  3. [读书][笔记]WINDOWS PE权威指南《零》PE基础

    参考: https://zhuanlan.zhihu.com/p/47075612 https://docs.microsoft.com/zh-cn/windows/win32/debug/pe-fo ...

  4. MongoDB权威指南读书笔记——CRUD

    插入并保存文档 插入是向MongoDB中添加数据的基本方法.可以使用Insert方法向目标集合插入一个文档:db.foo.insert({"bar" : "baz&quo ...

  5. HTTP权威指南读书笔记

    <<HTTP权威指南>>读书笔记 第一部分:Web的基础 第1章:HTTP概述 主要内容 1.什么是HTTP 2.HTTP的基本组件 HTTP HTTP:HTTP(Hypert ...

  6. mysql数据库权威指南_MySQL_MySQL权威指南读书笔记(三),第二章:MYSQL数据库里面的数 - phpStudy...

    MySQL权威指南读书笔记(三) 第二章:MYSQL数据库里面的数据 用想用好MYSQL,就必须透彻理解MYSQL是如何看待和处理数据的.本章主要讨论了两个问题:一是SQL所能处理的数据值的类型:二是 ...

  7. HTML5权威指南----读书笔记

    <!DOCTYPE html> <html> <head><meta name = 'keywords' content="HTML5权威指南--- ...

  8. [读书][笔记]WINDOWS PE权威指南《一》PE的原理和基础 之 第一章 环境搭建及简单破解

    文章目录 前言 前期准备 1.1 开发语言MASM32 1.1.1 设置开发环境 下载安装masm 环境变量配置 测试是否配置成功 1.1.2 开发第一个源程序HelloWorld.asm 配置 代码 ...

  9. 计算机网络和http权威指南 读书笔记

    计算机网络笔记 网络层 网络层向上提供无连接的,尽最大努力交付的数据报服务 网络层不提供数据质量承诺 物理层使用的中间设备叫转发器repeater 数据链路层叫网桥bridge 网络层叫路由器rout ...

最新文章

  1. 程序员每天工作摸鱼俩小时,月薪35K?
  2. 网站建设中的五大常见问题
  3. Python和Flask真强大:不能错过的15篇技术热文(转载)
  4. Java Serializable:明明就一个空的接口嘛
  5. SEO的有利因素跟不利因素
  6. 后端技术:Nginx从安装到高可用,看完本篇就够了!
  7. mysql 建表覆盖原先表_mysql表与表之间建关系
  8. 冒泡排序及其三种优化方案
  9. python迭代器是什么意思_理解Python的迭代器
  10. 线段树(区间合并) LA 3989 Ray, Pass me the dishes!
  11. bloom-generate 打包 ros 版本 noetic 的包及报 /usr/bin/ld: 找不到 -lpthreads与undefined reference pthread_create
  12. function与感叹号
  13. 用ASP.NET开发胖客户端应用程序
  14. 18.1---不用加号的加法(CC150)
  15. 软件开发模型、瀑布模型、V模型、原型模型、增量模型、螺旋模型、喷泉模型
  16. PSV遭到破解!reF00D让你在低版本执行新游戏
  17. JN5169 NXP ZigBee PRO 无线网络应用所需的常见操作(一)
  18. Android 应用市场大全 主流市场
  19. windows常见开机报错码以及解决方法
  20. 梦想照进现实|CSDN 实体奖牌 第五期

热门文章

  1. golang time.NewTimer ,time.After, time.NewTicker
  2. selenium 定位li_动漫之家selenium懒爬虫
  3. 宫崎骏的幻想世界6页 动漫主题 我愿称之为最美的网页设计作业
  4. 单片机基础之ADD与ADDC的区别详解
  5. SpringBoot实现Excel读取
  6. Oracle-----增、删、改数据
  7. 使用TensorFlow.js进行时间序列预测
  8. c语言x的n次方导数,a的x次方的导数
  9. java tcp多人聊天室
  10. 浅谈UI设计师的职业发展