精通恶意软件分析第二版-二-

214 阅读1小时+

精通恶意软件分析第二版(二)

原文:annas-archive.org/md5/a5e642fcde320e26768a38bb6eadf732

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章:解包、解密与去混淆

在本章中,我们将探讨恶意软件作者为绕过杀毒软件静态签名并欺骗经验不足的逆向工程师而引入的不同技术。主要内容包括打包、加密和混淆。我们将学习如何识别打包样本,如何解包它们,如何处理不同的加密算法——从简单的滑动密钥加密到更复杂的算法,如 3DES、AES 和 RSA——以及如何处理 API 加密、字符串加密和网络流量加密。

本章将帮助你应对使用打包和加密技术来规避检测并阻碍逆向工程的恶意软件。通过本章的信息,你将能够手动解包带有自定义打包工具的恶意软件样本,理解需要解密其代码、字符串、API 或网络流量的恶意软件加密算法,并提取其渗透数据。你还将了解如何使用 IDA Python 脚本自动化解密过程。

在本章中,我们将涵盖以下主题:

  • 探索打包工具

  • 识别打包样本

  • 自动解包打包样本

  • 手动解包技术

  • 转储解包后的样本并修复导入表

  • 识别简单的加密算法和函数

  • 高级对称和非对称加密算法

  • 现代恶意软件中的加密应用——Vawtrak 银行木马

  • 使用 IDA 进行解密和解包

探索打包工具

打包工具是一种将可执行文件的代码、数据,有时还包括资源打包在一起的工具,它包含了在运行时解包并执行程序的代码。我们将解决以下几个过程:

  • 高级对称和非对称加密算法

  • 现代恶意软件中的加密应用——Vawtrak 银行木马

  • 使用 IDA 进行解密和解包

这是此过程的高级流程图:

图 4.1 – 解包样本的过程

图 4.1 – 解包样本的过程

打包工具帮助恶意软件作者通过这些压缩和/或加密层隐藏其恶意代码。只有在恶意软件执行时(在运行时模式下),这些代码才会被解包并执行,从而帮助恶意软件作者绕过基于静态签名的检测,这些检测通常会针对打包样本进行应用。

探索打包和加密工具

多种工具可以对可执行文件进行打包/加密,但每种工具的用途不同。理解它们之间的区别非常重要,因为它们的加密技术是根据其用途量身定制的。我们来逐一了解它们:

  • 打包工具:这些程序主要是压缩可执行文件,从而减小它们的总体大小。由于它们的目的是压缩,因此并不是为了隐藏恶意特征,也本身不具有恶意。因此,它们不能作为已打包文件可能是恶意的指标。市面上有很多著名的打包工具,它们被良性软件和恶意软件家族同时使用,以下是几个例子:

    • UPX:这是一款开源打包工具,其命令行工具可以解压已打包的文件。

    • ASPack:这是一款常用的打包工具,提供免费版和高级版。提供 ASPack 的同一公司还提供如 ASProtect 这样的保护工具。

  • 合法保护工具:这些工具的主要目的是保护程序免受逆向工程的尝试——例如,保护共享软件产品的许可系统,或隐藏实现细节以防竞争对手窃取。它们通常集成了加密和各种反逆向工程技巧。虽然其中一些可能被滥用来保护恶意软件,但这并非它们的初衷。

  • 恶意加密工具:与合法保护工具类似,它们的目的也是使分析过程更为困难;不过这里的重点有所不同:为了避开病毒扫描,需要绕过沙箱并隐藏文件的恶意特征。它们的存在表明加密文件很可能是恶意的,因为它们不在合法市场上出售。

实际上,所有这些工具通常被称为打包工具,它们可能包括保护和压缩功能。

现在我们对打包工具有了更多了解,让我们来讨论如何识别它们。

识别已打包样本

有多种工具和方法可以识别样本是否被打包。在这一节中,我们将介绍不同的技术和标志,从最简单的方法到更复杂的技术。

技术 1 – 使用静态签名

识别恶意软件是否被打包的第一种方法是使用静态签名。每种打包工具都有独特的特征,能够帮助你识别它。某些 PE 工具,如 PEiDCFF Explorer,可以使用这些签名或特征扫描 PE 文件,识别用于压缩文件的打包工具(如果文件被打包);否则,它们将识别用于编译此可执行文件的编译器(如果文件未被打包)。以下是一个示例:

图 4.2 – PEiD 工具检测 UPX

图 4.2 – PEiD 工具检测 UPX

你需要做的就是在 PEiD 中打开这个文件,你会看到触发的签名(在前面的截图中,它被识别为 UPX)。然而,由于它们并不能总是识别使用的打包工具或编译器,你需要其他方法来识别文件是否被打包,以及如果被打包,使用了什么打包工具。

技术 2 – 评估 PE 区段名称

段名如果文件被打包,可以透露很多关于编译器或打包工具的信息。解压后的 PE 文件包含如.text.data.idata.rsrc.reloc等段,而打包文件包含特定的段名,如UPX0.aspack.stub等。以下是一个例子:

图 4.3 – PEiD 工具的段查看器

图 4.3 – PEiD 工具的段查看器

这些段的名称可以帮助你识别该文件是否被打包。通过在互联网上搜索这些段名,你可以帮助识别使用这些段名的打包工具,或者它们用于打包数据或 stub(解压代码)。你可以通过在 PEiD 中打开文件并点击EP Section旁边的**>**按钮,轻松找到段名。这样,你将看到该 PE 文件中所有段的列表以及它们的名称。

技术 3 – 使用 stub 执行标志

大多数打包工具会压缩 PE 文件的各个部分,包括代码段、数据段、导入表等,然后在文件末尾添加一个包含解压代码(stub)的新段。由于大多数解压后的 PE 文件是从第一个段开始执行的(通常是.text段),而打包后的 PE 文件会从最后一个段中的某个位置开始执行,这清晰地表明将会进行解密过程。以下是这一过程的迹象:

  • 入口点不指向第一个段(它通常指向倒数第二个段之一),并且该段的内存权限是EXECUTE(在段的特性中)。

  • 第一个段的内存权限通常是READ | WRITE

值得一提的是,许多感染可执行文件的病毒家族具有类似的特征。

技术 4 – 检测小的导入表

对于大多数应用程序,导入表中充满了来自系统库和第三方库的 API;然而,在大多数打包的 PE 文件中,导入表会非常小,并且只包含来自已知库的少数 API。这足以解压文件。每个库中的一个 API 将在解压后被使用。原因是大多数打包工具在解压 PE 文件后会手动加载导入表,如下图所示:

图 4.4 – 解压样本与使用 UPX 打包样本的导入表

图 4.4 – 解压样本与使用 UPX 打包样本的导入表

打包样本删除了ADVAPI32.dll中的所有 API,只留下一个,因此该库将由 Windows 加载器自动加载。解压后,解压器 stub 代码将再次使用GetProcAddress API 加载所有这些 API。

现在我们大致了解了如何识别一个打包的样本,接下来让我们深入探讨如何自动解压打包样本。

自动解压打包样本

在深入手动、耗时的解包过程之前,您需要首先尝试一些快速的自动化技术,以便快速获得干净的解包样本。在本节中,我们将解释最常见的几种快速解包技术,针对那些使用常见打包器打包的样本。

技巧 1 – 官方解包过程

一些打包器,如UPXWinRAR,是自解压包,它们包含了与工具一起提供的解包技术。如你所知,这些工具并非旨在隐藏任何恶意特征,因此其中一些工具提供了解包功能,既面向开发人员,也面向最终用户。

在某些情况下,恶意软件非法使用商业保护程序来保护自己,防止被逆向工程和检测。在这种情况下,您甚至可以直接联系保护提供商,以便为您的分析解保护该恶意软件。

在 UPX 的情况下,攻击者通常会修补打包样本,使其仍然可以执行,但标准工具无法再解包它。例如,在许多情况下,它涉及将其第一个节的UPX魔术值替换为其他内容:

图 4.5 – UPX 魔术值和节名称已更改,但样本仍然完全可用

图 4.5 – UPX 魔术值和节名称已更改,但样本仍然完全可用

恢复原始值可以使样本通过标准工具无法解包。

技巧 2 – 使用 OllyDbg 配合 OllyScript

有一个名为OllyScript的 OllyDbg 插件可以帮助自动化解包过程。它通过脚本化 OllyDbg 的操作来实现这一点,比如设置断点、继续执行、将 EIP 寄存器指向不同位置,或者修改一些字节。

如今,OllyScript 的使用已经不那么广泛,但它启发了下一个技巧。

技巧 3 – 使用通用解包工具

通用解包器是已经预先编写脚本的调试器,用于解包特定的打包器或自动化手动解包过程,我们将在下一节中详细描述其中的内容。以下是其中一个例子:

图 4.6 – QuickUnpack 工具详细介绍

图 4.6 – QuickUnpack 工具详细介绍

它们更加通用,能够与多个打包器兼容。然而,恶意软件可能会从这些工具中逃逸,从而导致恶意软件在用户的机器上执行。因此,您应该始终在隔离的虚拟机或安全环境中使用这些工具。

技巧 4 – 模拟

另一个值得提到的工具组是模拟器。模拟器是能够模拟执行环境的程序,包括处理器(用于执行指令、处理寄存器等)、内存、操作系统等。

这些工具具有更强大的能力来安全地运行恶意软件(因为所有操作都在模拟环境中),并且对执行过程有更多的控制。因此,它们能够设置更复杂的断点,并且可以轻松编写脚本(如libemuPokas x86 模拟器),如下面的代码所示:

from pySRDF import *
emu = Emulator(“upx.exe”)
x = emu.SetBp(“isdirty(eip)”) # which set bp on Execute on modified data
emu.Run() # OR emu.Run(“ins.log”) to log all running instructions
emu.Dump(“upx_unpacked.exe”, DUMP_FIXIMPORTTABLE) # DUMP_FIXIMPORTTABLE create new import table for new API
print(“File Unpacked Successfully\n\nThe Disassembled Code\n---------------”)

在这个例子中,我们使用了 Pokas x86 模拟器。通过它,设置更复杂的断点变得更加容易,例如修改数据时执行,当指令指针(EIP)指向解密/解包后的内存位置时,该断点会触发。

另一个基于仿真技术的出色工具是unipacker。它基于Unicorn引擎,并支持多种流行的合法打包工具,包括 ASPack、FSG、MEW、MPRESS 等。

技术 5 – 内存转储

我们将提到的最后一种快速技术是结合内存转储。由于其对大多数打包工具和保护器而言是最容易应用的技术之一(特别是当它们具有反调试技术时),因此这一技术被广泛使用。其背后的思路是执行恶意软件并获取其进程的内存快照。一些常见的沙箱工具提供进程的内存转储作为核心功能,或者作为其插件功能之一,例如Cuckoo沙箱。

这种技术对静态分析和静态签名扫描非常有益;然而,生成的内存转储与原始样本不同,无法直接执行。除了代码和数据的偏移位置与节表中指定的偏移位置不匹配外,导入表也需要修复,才能进行后续的动态分析。

由于这种技术无法提供干净的样本,并且因为我们之前描述的自动化技术存在局限性,了解如何手动解包恶意软件将帮助你应对那些你偶尔会遇到的特殊情况。通过手动解包,并理解反逆向工程技术(这些将在第六章中详细讲解,绕过反逆向工程技术),你将能够应对最先进的打包工具。

在下一节中,我们将探讨如何使用 OllyDbg 进行手动解包。

手动解包技术

尽管自动解包比手动解包更快、更易使用,但它并不适用于所有的打包工具、加密器或保护器。这是因为其中一些需要特定的定制解包方式。有些工具采用了反虚拟机技术或反逆向工程技术,而其他工具则使用了模拟器无法检测到的非常规 API 或汇编指令。在本节中,我们将探讨不同的手动解包恶意软件的技术。

之前的技术与手动解压的主要区别在于我们何时获取内存转储以及之后怎么处理它。如果我们仅执行原始样本,转储整个进程内存,并希望解压的模块在那里可用,我们将面临多个问题:

  • 可能解压后的样本已经按节区映射,且导入表已经填充,因此工程师需要更改每个节区的物理地址,使其与虚拟地址相等,恢复导入,甚至可能需要处理重定位,以使它们重新变得可执行。

  • 该样本的哈希值将与原始样本不同。

  • 原始加载器可能会将样本解压到分配的内存中,注入到其他地方,并释放内存,这样它就不会成为完整转储的一部分。

  • 很容易错过一些模块;例如,原始加载器可能只会为 32 位或 64 位平台解压一个样本。

更为简洁的方法是在样本刚被解压但尚未使用时停止解压。这样,它将只是一个原始文件。在某些情况下,甚至它的哈希值也会与尚未打包的原始样本匹配,因此可以用于威胁狩猎。

在本节中,我们将介绍几种常见的通用解压方法。

技术 1 – 执行时的内存断点

该技术适用于将解压样本放置在与已加载打包文件相同位置的内存中的打包器。正如我们所知,打包样本将包含原始文件的各个节区(包括代码节区),解压程序只会解压每个节区,然后将控制权转移到原始入口点OEP),以便应用程序正常运行。这样,我们可以假设 OEP 会在第一个节区中,这样我们就可以设置一个断点来捕获那里执行的任何指令。我们一步步地介绍这个过程。

步骤 1 – 设置断点

为了拦截第一个节区中的代码接管控制的时刻,我们不能使用执行时的硬件断点,因为它们最多只能设置为四个字节。这样,我们需要确切知道执行将从哪里开始。更有效的解决方案是设置执行时的内存断点。

在 OllyDbg 中隐式提供了在执行时使用内存断点的功能。可以通过进入视图 | 内存来访问,在这里我们可以将第一个节区的内存权限更改为读/写,如果它原本是完全访问的话。以下是一个示例:

图 4.7 – 在 OllyDbg 中更改内存权限

图 4.7 – 在 OllyDbg 中更改内存权限

在这种情况下,直到该段获得执行权限之前,我们无法在此段中执行代码。默认情况下,在多个 Windows 版本中,即使内存权限不包含 EXECUTE 权限,它仍然会对非关键进程保持可执行状态。因此,你需要强制执行所谓的 EXECUTE 权限,并且不允许任何不可执行的数据被执行。

该技术用于防止利用攻击,我们将在 第八章 中更详细地讨论 处理利用和 shellcode;不过,在我们想要轻松解包恶意软件样本时,它非常有用。

步骤 2 – 启用数据执行保护

要启用 DEP,你可以进入 高级系统设置,然后选择 数据执行保护。你需要为所有程序和服务启用它,如下图所示:

图 4.8 – 更改 Windows 上的 DEP 设置

图 4.8 – 更改 Windows 上的 DEP 设置

现在,应该强制这些类型的断点,并防止恶意软件在该段中执行,特别是在解密代码的开头(OEP)。

步骤 3 – 防止任何进一步尝试更改内存权限

不幸的是,仅仅强制执行 DEP 是不够的。解包存根可以通过使用 VirtualProtect API,再次将该段权限更改为完全访问,从而轻松绕过此断点。

该 API 使程序能够将任何内存块的内存权限更改为任何其他权限。你需要通过转到 VirtualProtect 设置一个断点,并在它指向的地址上设置另一个断点。

如果存根尝试调用 VirtualProtect 来更改内存权限,调试中的进程将会停止,你可以更改它尝试设置的第一个部分的权限。你可以将 NewProtect 参数的值更改为 READONLYREAD|WRITE,并从中移除 EXECUTE 位。调试器中显示的情况如下:

图 4.9 – 查找 VirtualProtect API 更改权限的地址

图 4.9 – 查找 VirtualProtect API 更改权限的地址

处理完这一部分后,是时候让断点触发了。

步骤 4 – 执行并获取 OEP

一旦点击 运行,调试中的进程最终会将控制权转交给 OEP,这将导致出现访问冲突错误,下面是截图所示:

图 4.10 – 在 OllyDbg 中停留在样本的 OEP

图 4.10 – 在 OllyDbg 中停留在样本的 OEP

这可能不会立即发生,因为一些加壳器会修改第一节的前几个字节,使用 retjmpcall 等指令,仅仅是为了使调试过程在此断点处中断;然而,经过几次迭代后,程序会中断。这发生在第一次加密/解压的过程完成后,它会执行程序的原始代码。

技巧 2 – 调用堆栈回溯

理解 调用堆栈 的概念对加速你的恶意软件分析过程非常有用。首先是解包过程。

看一下以下代码,并想象堆栈会是什么样子:

func01:
1: push ebp
2: mov ebp, esp ; now ebp = esp
...
3: call func02
...
func02:
4: push ebp     ; which was the previous esp before the call
5: mov ebp, esp ; now ebp = new esp
...
6: call func03
...
func03:
7: push ebp     ; which is equal to previous esp
8: mov ebp, esp ; ebp = another new esp
...

当我们查看在 call func03 保存返回地址之后的堆栈时,前一个 esp 的值通过 push ebp 被保存(它在第 5 行被复制到 ebp)。在这个之前的 esp 值上,存储了第一个 esp 值(这是因为 ebp 的指令 4 等于第一个 esp 值),接着是来自 call func02 的返回地址,以此类推。这里,存储的 esp 值后面跟着一个返回地址。这个 esp 值指向先前存储的 esp 值,后面跟着先前的返回地址,以此类推。这就是所谓的调用堆栈。下图展示了在 OllyDbg 中的实际情况:

图 4.11 – 在 OllyDbg 中存储的值后跟返回地址

图 4.11 – 在 OllyDbg 中存储的值后跟返回地址

如你所见,存储的 esp 值指向下一个堆栈帧(另一个存储的 esp 值和先前调用的返回地址),以此类推。

OllyDbg 包括一个可通过 视图 | 调用堆栈 访问的调用堆栈视图窗口。它看起来如下:

图 4.12 – OllyDbg 中的调用堆栈

图 4.12 – OllyDbg 中的调用堆栈

现在,你可能会问:调用堆栈如何帮助我们以快速高效的方式卸载恶意软件?

在这里,我们可以设置一个断点,确保它会使调试过程在解密代码执行的中途中断(解包阶段后的实际程序代码)。一旦执行停止,我们可以回溯调用堆栈,找到解密代码中的第一个调用。到达那里后,我们可以向上滑动,直到找到在解密代码中执行的第一个函数的起始位置,并将该地址声明为 OEP。我们将更详细地描述这个过程。

步骤 1 – 设置断点

为了应用这种方法,你需要在程序某个时刻会执行的 API 上设置断点。你可以依赖常见的 API(例如 GetModuleFileNameAGetCommandLineACreateFileAVirtualAllocHeapAllocmemset)、你的行为分析,或者沙盒报告,它会告诉你样本执行过程中使用了哪些 API。

首先,您必须在这些 API 上设置断点(使用您知道的所有 API,除了那些可能被解压缩存根使用的 API),然后执行程序直到执行被中断,如下图所示:

图 4.13 – 在 OllyDbg 的堆栈窗口中的返回地址

图 4.13 – 在 OllyDbg 的堆栈窗口中的返回地址

现在,您需要检查堆栈,因为接下来的大多数步骤都将在堆栈方面进行。通过这样做,您可以开始跟踪调用堆栈。

步骤 2 – 跟踪调用堆栈

跟踪堆栈中存储的 esp 值,然后跟踪下一个存储的 esp 值,直到您找到第一个返回地址,如下图所示:

图 4.14 – 在 OllyDbg 的堆栈窗口中的最后返回地址

图 4.14 – 在 OllyDbg 的堆栈窗口中的最后返回地址

现在,跟踪 CPU 窗口中反汇编部分的返回地址,如下所示:

图 4.15 – 在 OllyDbg 中跟踪最后一个返回地址

图 4.15 – 在 OllyDbg 中跟踪最后一个返回地址

一旦您到达解压缩区域中的第一个调用,剩下的唯一步骤就是到达 OEP。

步骤 3 – 到达 OEP

现在,您只需要向上滑动,直到找到 OEP。它可以通过标准的函数前言来识别,如下所示:

图 4.16 – 在 OllyDbg 中找到 OEP

图 4.16 – 在 OllyDbg 中找到 OEP

这是我们通过之前的方法能够到达的相同入口点。这是一个简单的技术,适用于许多复杂的打包程序和加密器。然而,这种技术很容易导致恶意软件的实际执行,或者至少是其部分代码的执行,因此需要小心使用。

技术 3 – 监视解压缩代码的内存分配空间

如果分析样本的时间有限,或者样本数量很多,这种方法非常有用,因为在这里我们不会深入讨论原始样本是如何存储的。

这里的想法是,原始恶意软件通常会分配一个大的内存块来存储解压缩/解密后的嵌入样本。稍后我们将讨论在这种情况不成立时会发生什么。

有多个 Windows API 可以用于在用户模式下分配内存。攻击者通常倾向于使用以下这些:

  • VirtualAlloc/VirtualAllocEx/VirtualAllocExNuma

  • LocalAlloc/GlobalAlloc/HeapAlloc

  • RtlAllocateHeap

在内核模式下,还有其他函数,例如 ZwAllocateVirtualMemoryExAllocatePoolWithTag 也可以以类似的方式使用。

如果样本是用 C 编写的,直接监视 malloc/calloc 函数是有意义的。对于 C++ 恶意软件,我们还可以监视 new 操作符。

一旦我们在样本的入口点(或 TLS 例程的开头,如果它存在的话)停下,就可以在这些函数的执行过程中设置断点。通常,可以在函数的第一条指令上设置断点,但如果担心恶意软件可能会挂钩它(即,用自定义代码替换前几条字节),在最后一条指令上设置断点效果会更好。

这样做的另一个好处是,只需要一个断点来同时监控 VirtualAllocExVirtualAlloc(后者是前者 API 的包装器)。在 IDA 调试器中,按 G 热键并在 API 名称前加上相应的 DLL 名称(不带文件扩展名,并用下划线分隔)可以直接跳转到该 API,例如,kernel32_VirtualAlloc,如下面的截图所示:

图 4.17 – 在 WinAPI 中设置内存分配断点

图 4.17 – 在 WinAPI 中设置内存分配断点

在此之后,我们继续执行并监控已分配内存块的大小。只要内存块足够大,我们可以在写入操作时设置断点,拦截加密的(或已即时解密的)有效载荷被写入的时刻。如果恶意软件调用这些函数的次数过多,可以考虑设置一个条件断点,仅监控分配大于特定大小的内存块。之后,如果内存块仍然是加密的,我们可以继续在写入时设置断点,直到解密例程开始处理它。最后,当最后一个字节解密时,我们可以将内存块转储到磁盘。

其他可以采用相同方法的 API 函数包括以下几个:

  • VirtualProtect:恶意软件作者可以利用这个函数将内存块存储解压后的样本可执行文件,或将头部或代码节区设置为不可写。

  • WriteProcessMemory:这通常用于将解压后的有效载荷注入到另一个进程或其自身。

一些打包工具,如 UPX,采用稍微不同的方法,在其节区表中添加一个节区,这个节区在 RAM 中占用大量空间,但在磁盘上不存在(物理大小为 0)。这样,Windows 加载程序会为解压程序准备这个空间,而无需动态分配内存。在这种情况下,在该节区开头设置写入断点的效果与之前描述的相同。

在大多数情况下,恶意软件会一次性解压整个样本,这样在转储后,我们可以获得正确的 MZ-PE 文件,可以独立分析。然而,也有其他选项,如以下所示:

  • 解密后的块是一个损坏的可执行文件,依赖于原始的打包工具来正确执行。

  • 打包工具按节区逐个解密样本并逐一加载它们。处理这种情况的方式有很多种,如下所示:

    • 转储各个部分,只要它们可用,并稍后将它们合并。

    • 修改解密例程,以一次处理整个样本。

    • 编写一个脚本来解密整个加密块。

如果恶意程序在任何阶段终止,这可能是一个迹象,表明它可能需要一些额外的东西(如命令行参数、外部文件,或者可能需要以特定方式加载),或者它可能需要绕过反逆向工程的技巧。你可以通过多种方式确认这一点——例如,拦截程序即将终止的时刻(例如,通过在ExitProcessTerminateProcess或更复杂的PostQuitMessage API 调用上放置断点),并追踪哪个部分的代码导致了终止。一些工程师更倾向于手动逐步调试主函数——直到某个子程序导致终止——然后重新启动过程并追踪该子程序的代码。接下来,如果需要,可以继续追踪该子程序内部的代码,直到确认终止逻辑。

技巧 4 – 原地解包

虽然不常见,但有可能在样本原始位置所在的同一个节中解密它(该节应该具有WRITE|EXECUTE权限),或者在原始文件的另一个节中解密。

在这种情况下,执行以下步骤是有意义的:

  1. 搜索一个大的加密块(通常,它具有高熵,并且在十六进制编辑器中肉眼可见)。

  2. 找到它将被读取的确切位置(块的前几个字节可能有其他用途——例如,它们可能存储各种类型的元数据,如大小或校验和/哈希值,用于验证解密)。

  3. 在那里放置一个读/写断点。

  4. 运行程序并等待断点被触发。

只要解密例程访问了这个块,就非常容易获得它的解密版本——无论是通过在解密函数的末尾放置断点,还是在写入加密块最后几个字节时放置断点,拦截它们被处理的时刻。

值得一提的是,这种方法可以与依赖恶意软件分配内存的方法一起使用。这个内容将在手动解包技巧一节中讨论。

技巧 5 – 搜索并将控制权转移到 OEP

理论上,任何控制流指令都可以在解包完成后将控制转移到 OEP。然而,实际上,许多解包器仅使用jmp指令,因为它们不需要任何条件,也不需要将控制返回(另一个不太常见的选项是使用push <OEP_addr>ret的组合)。由于 OEP 的地址通常在编译时未知,通常是通过寄存器或存储在特定偏移量的值传递给jmp,而不是实际的虚拟地址,因此很容易被发现。另一种可能是,OEP 地址在编译时已知,但由于解包尚未完成,因此那里还没有代码。在这两种情况下,搜索异常的控制转移指令可能是快速定位 OEP 的方法。对于jmp,可以通过运行全文搜索所有jmp指令(在 IDA 中,您可以使用 Alt + T 热键组合)并对其进行排序,以便发现异常条目。以下是这种控制转移的示例:

图 4.18 – 涉及寄存器的不常见控制转移

图 4.18 – 涉及寄存器的不常见控制转移

现在让我们进入技术 6。

技术 6 – 基于堆栈恢复

这种技术通常比前两种方法更快,但可靠性较差。这里的思路是,一些加壳工具会在解包完成后将控制转移到主函数结束时的解包代码。我们已经知道,在函数结束时,堆栈指针会恢复到函数开始时的相同地址。在这种情况下,可以在访问[esp-4]/[rsp-8]值时设置断点,并保持在样本的入口点,然后执行它,这样断点就有可能在转移控制到解包代码之前触发。

这可能永远不会发生,这取决于解包代码的实现,也可能会有其他情况会发生这种情况(例如,在开始实际解包过程之前,有多个垃圾调用)。因此,这种方法只能作为花费更多时间在其他方法之前的第一次快速检查。

当我们到达解包样本已加载到内存的阶段时,需要将其保存到磁盘。在下一节中,我们将描述如何将解包后的恶意软件从内存转储到磁盘并修复导入表。

转储解包后的样本并修复导入表

在这一节中,我们将学习如何将解包后的恶意软件从内存转储到磁盘并修复其导入表。除此之外,如果导入表已经由加载程序填充了 API 地址,我们还需要恢复原始值。在这种情况下,其他工具将能够读取它,我们也可以执行它进行动态分析。

转储进程

要转储进程,可以使用OllyDump。 OllyDump 是一个 OllyDbg 插件,可以将进程转储回可执行文件。它将 PE 文件从内存中卸载到必要的文件格式:

图 4.19 – OllyDump UI

图 4.19 – OllyDump UI

一旦从先前的手动解包过程中到达 OEP,您可以将 OEP 设置为新的入口点。 OllyDump 可以修复导入表(正如我们即将描述的那样)。 如果愿意使用其他工具,可以使用它或取消重建导入复选框。另一个选择是使用诸如PEToolsLord PE(用于 32 位)以及VSD(用于 32 和 64 位 Windows)之类的工具。除了所谓的Dump Full选项,主要是转储与样本相关的原始部分外,这些解决方案还可以转储特定的内存区域 – 例如,用于解密/解包样本的已分配内存,如下面的截图所示:

图 4.20 – PETools 的区域转储窗口

图 4.20 – PETools 的区域转储窗口

接下来,我们将看看如何修复恶意软件的导入表。

修复导入表

现在,你可能会问:需要修复的导入表会发生什么?答案是:当 PE 文件在进程内存中加载或者解包器存根加载导入表时,加载器会遍历导入表(你可以在第三章x86/x64 的基本静态和动态分析中找到更多信息),并使用可用于计算机上的 DLL 实际地址填充它们。这里是一个例子:

图 4.21 – PE 加载前后的导入表

图 4.21 – PE 加载前后的导入表

在此之后,这些 API 地址将用于在应用程序代码中访问这些 API,通常通过使用calljmp指令:

图 4.22 – 不同 API 调用的示例

图 4.22 – 不同 API 调用的示例

要恢复导入表,我们需要找到这些 API 地址列表,找到每个地址代表的 API(我们需要逐个库列表地址和相应的 API 名称进行查找),然后用指向 API 名称字符串的偏移量或序数值替换每个地址。如果在文件中找不到 API 名称,可能需要创建一个新的部分,将这些 API 名称添加到其中,并用它们来恢复导入表。

幸运的是,有些工具可以自动完成这些操作。 在本节中,我们将讨论Import REConstructorImpREC)。 这是它的外观:

图 4.23 – ImpREC 接口

图 4.23 – ImpREC 接口

要修复导入表,您需要按照以下步骤操作:

  1. 使用例如OllyDump(并取消选中Rebuild Import复选框)或其他首选工具,转储进程或任何库。

  2. 打开ImpREC并选择当前正在调试的进程。

  3. 现在,将 OEP 值设置为正确的值,然后点击IAT AutoSearch

  4. 之后,点击Get Imports,并删除Imported Functions Found部分中任何valid: NO的行。

  5. 点击Fix Dump按钮,然后选择之前转储的文件。现在,你将得到一个工作正常、未打包的 PE 文件。你可以将它加载到 PEiD 或任何其他 PE 浏览器应用程序中,检查它是否正常工作。

重要说明

对于 64 位 Windows 系统,可以使用 Scylla 或 CHimpREC 工具来代替。

在接下来的章节中,我们将讨论基本的加密算法和功能,以增强我们的知识基础,从而丰富我们的恶意软件分析能力。

识别简单的加密算法和功能

在本节中,我们将了解广泛使用的简单加密算法。我们将学习对称加密和非对称加密之间的区别,并将学习如何在恶意软件的反汇编代码中识别这些加密算法。

加密算法的类型

加密是修改数据或信息的过程,目的是使其在没有密钥的情况下不可读或无法使用,而该密钥仅提供给那些预期会读取消息的人。编码或压缩与加密的区别在于,编码和压缩不使用任何密钥,它们的主要目标与保护信息或限制访问信息无关,而加密则有此目的。

加密算法有两种基本类型:对称算法和非对称算法(也称为公钥算法)。让我们来探讨它们之间的区别:

  • 对称算法:这些算法使用相同的密钥进行加密和解密。它们使用一个由双方共享的单一密钥:

图 4.24 – 对称算法解释

图 4.24 – 对称算法解释

  • 非对称算法:在这种情况下,使用了两个密钥,一个用于加密,另一个用于解密。这两个密钥分别称为公钥私钥。一个密钥是公开的(公钥),而另一个密钥是保密的(私钥)。以下是一个高层次的示意图,描述了这一过程:

图 4.25 – 非对称算法解释

图 4.25 – 非对称算法解释

现在,让我们谈谈恶意软件中常用的简单自定义加密算法。

基本加密算法

大多数恶意软件使用的加密算法由基本的数学和逻辑指令组成——即xoraddsubrolror。这些指令是可逆的,在加密时使用它们不会丢失数据,而与shlshr等指令相比,后者可能会丢失左侧或右侧的一些位。这种情况也会发生在andor指令中,当使用or与 1 或and与 0 时,可能会导致数据丢失。

这些操作可以以多种方式使用,具体如下:

  • rol指令:

图 4.26 – rol指令示例

图 4.26 – rol指令示例

  • 运行密钥加密:在这里,恶意软件在加密过程中更改密钥。以下是一个示例:

    loop_start:
    mov edx, <secret_key>
    xor dword ptr [<data_to_encrypt> + eax], edx
    add edx, 0x05 ; add 5 to the key
    inc eax
    loop loop_start
    
  • 0x23)。

  • 其他加密算法:恶意软件作者在创造新的算法时总是有源源不断的创意,这些算法代表了这些算术和逻辑指令的组合。这就引出了下一个问题:我们如何识别加密函数?

在反汇编中识别加密函数

以下截图展示了从14编号的区域。这些区域对于理解和识别恶意软件中使用的加密算法至关重要:

图 4.27 – 识别加密算法时需要注意的事项

图 4.27 – 识别加密算法时需要注意的事项

要识别加密函数,您需要搜索四个要素,如下表所示:

这四个要素是任何加密循环的核心部分。在一个小的加密循环中它们很容易被发现,但在更复杂的加密循环(如 RC4 加密)中,可能更难发现,我们稍后会讨论 RC4 加密。

简单算法的字符串搜索检测技术

在本节中,我们将探讨一种名为X-RAYING的技术(由 Peter Ferrie 在 VB2004 的PRINCIPLES AND PRACTICE OF X-RAYING文章中首次提出)。这种技术被杀毒软件和其他静态签名工具用于检测具有签名的样本,即使它们是加密的。该技术可以深入加密层,揭示样本代码并检测它,而无需知道加密密钥,且无需使用像暴力破解等费时的技术。在这里,我们将描述该技术的理论和应用,以及一些可以帮助我们使用该技术的工具。我们可以使用此技术来检测嵌入的 PE 文件或解密恶意样本。

X-RAYING 的基础知识

对于我们之前描述的那些算法类型,如果你有加密数据、加密算法和秘密密钥,你可以轻松地解密数据(这也是所有加密算法的目的);然而,如果你只有加密数据(密文)和一部分已解密的数据,你仍然能够解密剩余的加密数据吗?

在 X 射线技术中,如果你拥有一部分已解密的数据(明文),即使你不知道该明文数据在整个加密数据块中的偏移位置,你也可以暴力破解算法及其秘密密钥。它适用于我们之前描述的几乎所有简单算法,即便是多层加密。对于大多数加密的 PE 文件,明文通常包含诸如This program cannot run in DOS modekernel32.dll的字符串,以及一系列的空字节。

首先,我们将选择第一个候选加密算法,例如,XOR。然后,我们将在密文中搜索一部分明文。为此,我们将使用一部分预期的明文与密文进行 XOR 运算,例如,一个 4 字节的字符串。XOR 运算的结果将为我们提供一个候选解密密钥(这是 XOR 算法的一个特性)。然后,我们将使用这个密钥来测试剩余的明文。如果这个密钥有效,它将揭示密文的剩余部分明文,这意味着我们已经找到了秘密密钥,并且可以解密剩余的数据。

现在,让我们来讨论一些可能帮助我们加速这个过程的工具。

X 射线工具用于恶意软件分析和检测

一些工具已经被编写出来,帮助恶意软件研究人员使用 X 射线技术进行扫描。以下是您可以使用的这些工具,可以通过命令行或脚本来使用:

  • rolror指令):

图 4.28 – XORSearch 用户界面

图 4.28 – XORSearch 用户界面

  • xor签名:

图 4.29 – 使用 YARA 签名的示例

图 4.29 – 使用 YARA 签名的示例

对于更高级的 X 射线技术,您可能需要编写一个小脚本手动扫描。

识别 RC4 加密算法

RC4 算法是恶意软件作者最常用的加密算法之一,主要因为它简单,同时又足够强大,不像其他简单的加密算法那样容易被破解。恶意软件作者通常手动实现它,而不是依赖于 WinAPI,这使得新手逆向工程师更难识别。在本节中,我们将看到该算法的具体样子以及如何识别它。

RC4 加密算法

RC4 算法是一个对称流加密算法,由两部分组成:密钥调度算法KSA)和伪随机生成算法PRGA)。让我们更详细地了解一下它们。

密钥调度算法

算法的秘钥调度部分从秘钥创建一个称为 S 数组的 256 字节数组。这个数组将用于初始化流密钥生成器。它由两部分组成:

  • 它按顺序创建一个从 0256S 数组:

    for i from 0 to 255
      S[i] := i
    endfor
    
  • 它使用密钥材料排列 S 数组:

    for i from 0 to 255
      j := (j + S[i] + key[i mod keylength]) mod 256
      swap values of S[i] and S[j]
    endfor
    

一旦密钥的初始化部分完成,解密算法就开始了。在大多数情况下,KSA 部分是写在一个单独的函数中的,这个函数只接受密钥作为参数,而不需要加密或解密的数据。

伪随机生成算法(PRNG)

算法的伪随机生成部分只是生成伪随机值(再次基于字节交换,就像我们为 S 数组所做的那样),但也执行与生成的值和数据中的一个字节的 XOR 操作:

i := 0
j := 0
while GeneratingOutput:
  i := (i + 1) mod 256
  j := (j + S[i]) mod 256
  swap values of S[i] and S[j]
  K := S[(S[i] + S[j]) mod 256]
  Data[i] = Data[i] xor K
endwhile

如你所见,实际的加密算法是 xor。然而,所有这些交换旨在每次生成一个不同的密钥值(类似于滑动秘钥算法)。

在恶意软件样本中识别 RC4 算法

要识别 RC4 算法,一些关键特征可以帮助你检测它:

  • RC4 算法如下:

图 4.30 – RC4 算法中的数组生成

图 4.30 – RC4 算法中的数组生成

  • 存在大量的交换:如果你能识别出交换函数或代码,你会发现它无处不在于 RC4 算法中。算法的 KSA 和 PRGA 部分是它是 RC4 算法的一个很好的标志:

图 4.31 – RC4 算法中的交换

图 4.31 – RC4 算法中的交换

  • 实际算法是 XOR:在循环结束时,你会注意到这个算法是一个 XOR 算法。所有的交换都是在密钥上进行的。唯一影响数据的变化是通过 XOR 进行的:

图 4.32 – RC4 算法中的异或操作

图 4.32 – RC4 算法中的异或操作

  • 加密和解密的相似性:你还会注意到加密和解密函数是相同的函数。XOR 逻辑门是可逆的。你可以用 XOR 和秘钥加密数据,然后用相同的秘钥和 XOR 解密这个加密数据(这与加/减算法等不同)。

现在是时候讨论更复杂的算法了。

高级对称和非对称加密算法

像对称 DES 和 AES 或非对称 RSA 这样的标准加密算法被恶意软件作者广泛使用。然而,包含这些算法的大多数样本从不实现这些算法自身或将它们的代码复制到它们的恶意软件中。它们通常使用 Windows API 来实现。

这些算法在数学上比简单的加密算法或 RC4 更加复杂。虽然你不一定需要理解它们的数学背景就能理解它们是如何实现的,但了解如何识别它们的使用方式、如何确定涉及的具体算法、加密/解密密钥以及数据是很重要的。

从 Windows 加密 API 中提取信息

一些常见的 API 用于提供对加密算法的访问,包括 DES、AES、RSA,甚至 RC4 加密。这些 API 包括CryptAcquireContextCryptCreateHashCryptHashDataCryptEncryptCryptDecryptCryptImportKeyCryptGenKeyCryptDestroyKeyCryptDestroyHashCryptReleaseContext(来自Advapi32.dll)。

在这里,我们将看看恶意软件如何通过这些算法加密或解密数据,并且如何识别所使用的具体算法以及密钥。

步骤 1 – 初始化并连接到加密服务提供者(CSP)

加密服务提供者是一个在 Microsoft Windows 中实现与加密相关的 API 的库。为了初始化并使用这些提供者,恶意软件样本执行CryptAcquireContext API,如下所示:

CryptAcquireContext(&hProv,NULL,MS_STRONG_PROV,PROV_RSA_FULL,0);

你可以在系统的注册表中的以下密钥找到所有支持的提供者:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\Defaults\Provider

步骤 2 – 准备密钥

准备加密密钥有两种方式。如你所知,这些算法的加密密钥通常是固定大小的。以下是恶意软件作者常用来准备密钥的步骤:

  1. 首先,作者使用他们的明文密钥并通过任何已知的哈希算法(如MD5SHA128SHA256等)对其进行哈希:

    CryptCreateHash(hProv,CALG_MD5,0,0,&hHash); CryptHashData(hHash,secretkey,secretkeylen,0);
    
  2. 然后,它们使用CryptDeriveKey从这个哈希值创建会话密钥,如下所示:

    CryptDeriveKey(hProv, CALG_3DES, hHash, 0, &hKey);
    

从这里,他们可以轻松识别从 API 传递的第二个参数值中的算法。最常见的算法/值如下:

CALG_DES = 0x00006601  // DES encryption algorithm.
CALG_3DES = 0x00006603 // Triple DES encryption algorithm.
CALG_AES = 0x00006611  // Advanced Encryption Standard (AES).
CALG_RC4 = 0x00006801   // RC4 stream encryption algorithm.
CALG_RSA_KEYX = 0x0000a400 // RSA public key exchange algorithm.
  1. 一些恶意软件作者使用KEYBLOB,其中包含他们的密钥,并与CryptImportKey一起使用。KEYBLOB是一个简单的结构,包含密钥类型、使用的算法以及加密的秘密密钥。KEYBLOB的结构如下:

    typedef struct KEYBLOB { BYTE bType;
    BYTE bVersion; WORD reserved; ALG_ID aiKeyAlg; DWORD KEYLEN;
    BYTE[] KEY;}
    

bType短语表示此密钥的类型。最常见的类型如下:

  • PLAINTEXTKEYBLOB (0x8):表示一个对称算法的明文密钥,如DES3DESAES

  • PRIVATEKEYBLOB (0x7):表示这是一个非对称算法的私钥

  • PUBLICKEYBLOB (0x6):表示这是一个非对称算法的公钥

aiKeyAlg短语包含CryptDeriveKey的第二个参数,即算法类型。以下是一些KEYBLOB的示例:

BYTE DesKeyBlob[] = { 0x08,0x02,0x00,0x00,0x01,0x66,0x00,0x00, // BLOB header 0x08,0x00,0x00,0x00, // key length, in bytes
0xf1,0x0e,0x25,0x7c,0x6b,0xce,0x0d,0x34 // DES key with parity
};

如你所见,第一个字节(bType)显示我们这是一个PLAINTEXTKEYBLOB,而算法(0x01,0x66)表示CALG_DES (0x6601)

另一个示例如下:

BYTE rsa_public_key[] = {
0x06, 0x02, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00,
0x52, 0x53, 0x41, 0x31, 0x00, 0x08, 0x00, 0x00,
...
}

这表示一个 PUBLICKEYBLOB (0x6),而算法表示 CALG_RSA_KEYX (0xa400)。之后,它们通过 CryptImportKey 被加载:

CryptImportKey(akey->prov, (BYTE *) &key_blob, sizeof(key_blob), 0, 0, &akey->ckey)

下面是它在汇编语言中的示例:

图 4.33 – 使用 CryptImportKey API 导入 RSA 密钥

](tos-cn-i-73owjymdk6/ae48fb90ad184661860e073449730e49)

图 4.33 – 使用 CryptImportKey API 导入 RSA 密钥

一旦密钥准备就绪,就可以用于加密和解密操作。

第 3 步 – 加密或解密数据

现在密钥已经准备好,恶意软件使用 CryptEncryptCryptDecrypt 来加密或解密数据。使用这些 API,你可以识别加密数据块(或待加密数据块)的起始位置。这些 API 的使用方式如下:

CryptEncrypt(hKey,NULL,1,0,cyphertext,ctlen,sz); CryptDecrypt(hKey,NULL,1,0,plaintext,&ctlen);

第 4 步 – 释放内存

这是最后一步,我们通过使用 CryptDestroyKey API 来释放内存和所有已使用的句柄。

下一代密码学 API(CNG)

还有其他方式可以实现这些加密算法。其中一种是使用 密码学 API: 下一代CNG),它是微软实现的一组新 API。尽管目前在恶意软件中尚未广泛使用,但它们更容易理解并从中提取信息。使用这些 API 的步骤如下:

  1. MSDN 列出了支持的算法:

    BCryptOpenAlgorithmProvider(&hAesAlg, BCRYPT_AES_ALGORITHM, NULL, 0)
    
  2. 准备密钥:这与对称和非对称算法中密钥的准备方式不同。此 API 可能使用导入的密钥或生成一个密钥。这可以帮助你提取用于加密的秘密密钥,方法如下:

    BCryptGenerateSymmetricKey(hAesAlg, &hKey, pbKeyObject, cbKeyObject, (PBYTE)SecretKey, sizeof(SecretKey), 0)
    
  3. 加密或解密数据:在此步骤中,你可以轻松识别出要加密(或解密)数据块的起始位置:

    BCryptEncrypt(hKey, pbPlainText, cbPlainText, NULL, pbIV, cbBlockLen, NULL, 0, &cbCipherText, BCRYPT_BLOCK_PADDING)
    
  4. 使用 BCryptCloseAlgorithmProviderBCryptDestroyKeyHeapFree 清理数据。

现在,让我们看看这些知识如何帮助我们理解恶意软件的功能。

现代恶意软件中加密的应用 —— Vawtrak 银行木马

在本章中,我们已经看到加密或打包如何用于保护整个恶意软件。这里,我们将查看这些加密算法在恶意软件代码中的其他实现,用于混淆和隐藏恶意密钥特征。这些密钥特征可以通过静态签名或甚至网络签名来识别恶意软件家族。

在这一部分中,我们将查看一个已知的银行木马——Vawtrak。我们将看到这个恶意软件家族如何加密它的字符串和 API 名称,并混淆其网络通信。

字符串和 API 名称加密

Vawtrak 实现了一个相当简单的加密算法。它基于滑动密钥算法的原理,并使用减法作为主要的加密技术。它的加密过程如下:

图 4.34 – Vawtrak 恶意软件中的加密循环

](tos-cn-i-73owjymdk6/6a47bc16b7aa4b48b8cc576980f85c1d)

图 4.34 – Vawtrak 恶意软件中的加密循环

加密算法由两个部分组成:

  • 生成下一个密钥:此操作生成一个 4 字节的数字(称为种子),并仅使用其中的 1 个字节作为密钥:

    seed = ((seed * 0x41C64E6D) + 0x3039 ) & 0xFFFFFFFF key = seed & 0xFF
    
  • 加密数据:这部分非常简单,它使用以下逻辑加密数据:

    data[i] = data[i] - eax
    

该加密算法用于加密 API 名称和 DLL 名称,以便解密后,恶意软件可以使用名为LoadLibrary的 API 动态加载 DLL,如果 DLL 尚未加载,则加载该库,或者如果已经加载,则仅获取其句柄。

在获取 DLL 地址后,恶意软件通过名为GetProcAddress的 API 获取要执行的 API 地址,该 API 通过库的句柄和 API 名称获取该函数地址。恶意软件实现如下:

图 4.35 – 在 Vawtrak 恶意软件中解析 API 名称

图 4.35 – 在 Vawtrak 恶意软件中解析 API 名称

相同的函数(DecryptString)在恶意软件内部被频繁使用,用于根据需要解密每个字符串(仅在使用时),如下所示:

图 4.36 – Vawtrak 恶意软件中的解密例程交叉引用

图 4.36 – Vawtrak 恶意软件中的解密例程交叉引用

要解密此内容,您需要遍历每个调用解密函数的调用,并传递被加密字符串的地址来解密它。这可能是费力的或耗时的,因此自动化(例如,使用 IDA Python 或可脚本化的调试器/模拟器)可能会有所帮助,正如我们将在下一节中看到的那样。

网络通信加密

Vawtrak 可以使用不同的加密算法对其网络通信进行加密。它实现了多种算法,包括RC4LZMA压缩、LCG加密算法(这是用于字符串的,如我们在前一节中提到的)等。在本节中,我们将查看其加密的不同部分。

在请求中,它实现了一些加密,以隐藏基本信息,包括CAMPAIGN_IDBOT_ID,如以下截图所示:

图 4.37 – Vawtrak 恶意软件的网络流量

图 4.37 – Vawtrak 恶意软件的网络流量

Cookie,即PHPSESSID,包含了一个加密密钥。使用的加密算法是 RC4 加密。解密后的消息如下:

图 4.38 – 从 Vawtrak 恶意软件的网络流量中提取的信息

图 4.38 – 从 Vawtrak 恶意软件的网络流量中提取的信息

解密后的PHPSESSID在前 4 个字节中包含 RC4 密钥。BOT_ID和下一个字节表示Campaign_Id(0x03),其余的字节表示其他重要信息。

接收到的数据具有以下结构,包括用于解密的第一个种子、总大小以及用于解密的多种算法:

图 4.39 – Vawtrak 恶意软件中用于解密的结构

图 4.39 – Vawtrak 恶意软件中用于解密的结构

不幸的是,网络通信没有简单的方法可以抓取所使用的算法或协议结构。你必须搜索网络通信函数,如 HttpAddRequestHeadersA(我们在解密过程中看到的那个)和其他网络 API,跟踪接收到的数据,以及跟踪将要发送的数据,直到你找到命令与控制通信背后的算法和结构。

现在,让我们探索 IDA 的各种功能,这些功能可能有助于我们理解并绕过涉及的加密和打包技术。

使用 IDA 进行解密和解包

IDA 是一个非常方便的工具,用于存储分析样本的标记。它的嵌入式调试器和多个远程调试器服务器应用程序允许你在一个地方进行静态和动态分析,支持多个平台——即使是那些 IDA 无法独立执行的平台。它还具有多个插件,可以进一步扩展其功能,并且内嵌的脚本语言可以自动化各种繁琐的任务。

IDA 小贴士和技巧

虽然 OllyDbg 在调试方面提供了相当不错的功能,但总体而言,IDA 在维护标记方面有更多选择。这就是为什么许多逆向工程师倾向于在 IDA 中同时进行静态和动态分析,特别是在解包方面非常有用。以下是一些小贴士和技巧,能够让这个过程更加愉快。

静态分析

首先,让我们看一下主要适用于静态分析的一些建议:

  • 当使用内存转储而不是原始样本时,可能会出现导入表已经填充了 API 地址的情况。

获取实际 API 名称的简单方法是使用 pe_dlls.idc 脚本,该脚本分发在 pe_scripts.zip 包中。该包可以在官方 IDA 网站上免费下载。从那里,你需要加载从生成转储的机器上获取的 DLL。指定 DLL 名称时,别忘了去掉文件名扩展名,因为在 IDA 中,文件名中不能使用点号符号。此外,该脚本不允许你选择 DLL 的基址。为了解决这个问题,在 pe_sections.idc 脚本的第 692 行添加以下代码:

imageBase = long(ask_addr(imageBase, “Enter base address”));
  • 通常来说,在 IDA 的 Structures 标签中重建恶意软件使用的结构比在反汇编代码中添加注释要更有意义,后者通常位于访问其字段的指令旁边。跟踪结构是一种错误更少的方式,并且意味着我们可以将其重复使用在类似的样本中,以及比较恶意软件的不同版本。

之后,你可以简单地右键点击该值并选择结构体偏移量选项(T热键)。可以通过在结构体子视图中按Ins热键快速添加结构体并指定其名称。然后,通过将光标放置在结构体末尾并按D热键一次、两次或三次,具体取决于所需的大小,添加单个字段。最后,要添加其余大小相同的字段,选择所需的字段,右键点击并选择数组...选项,指定具有相同大小的元素数量,并取消选中使用“dup”构造创建为数组选项的复选框。

  • 对于恶意软件访问存储在堆栈中的结构体字段的情况,可以通过右键点击并选择**手动...**选项(Alt + F1 热键)来获取实际偏移量,替换变量名为结构体开头的指针名称和剩余的偏移量,然后将偏移量替换为所需的结构体字段,如下图所示:

图 4.40 – 将本地变量映射到相应的结构体字段

图 4.40 – 将本地变量映射到相应的结构体字段

确保在重命名操作数时启用检查操作数选项,以验证值的总和是否保持准确。

另一种选择是选择变量的文本(而不仅仅是左键点击它),右键点击结构体偏移量选项(同样是T热键),指定偏移量差值,该值应等于结构体开头的指针偏移量,最后选择建议的结构体字段。

该方法更快捷,但不会保留指针的名称,如下图所示:

图 4.41 – 将本地变量映射到结构体字段的另一种方法

图 4.41 – 将本地变量映射到结构体字段的另一种方法

  • 许多自定义加密算法使用了xor操作,因此找到它们的简单方法是按照以下步骤操作:

    1. 打开包含两个不同寄存器或一个未通过帧指针寄存器(ebp)访问的内存值的xor指令。
  • 不要犹豫使用免费插件,如–v命令行参数来获取已识别函数的虚拟地址。

  • 如果需要导入包含枚举定义列表的 C 文件,建议使用h2enum.idc脚本(不要忘记在第二个对话框中提供正确的掩码)。导入包含结构体的 C 文件时,通常应该在其前面加上#pragma pack(1)语句,以保持偏移量正确。文件 | 加载文件 | 解析 C 头文件...选项和Tilib工具在大多数情况下可以互换使用。

  • 如果需要重命名多个指向实际 API 的连续值,选择所有这些值并执行renimp.idc脚本,该脚本可以在 IDA 的idc目录中找到。

  • 如果你需要在一台 Windows 机器上同时使用IDA <= 6.95IDA 7.0+,请按照以下步骤操作:

    1. 安装 x86 和 x64 的 Python 到不同的位置——例如,C:\Python27C:\Python27x64

    2. 确保以下环境变量指向IDA <= 6.95的设置:

    set PYTHONPATH=C:\Python27;C:\Python27\Lib;C:\Python27\DLLs;C:\Python27\Lib\lib-tk;
    set NLSPATH=C:\IDA6.95\
    

通过这种方式,IDA <= 6.95可以像平常一样通过点击其图标使用。要执行 IDA 7.0+,请创建一个特殊的LNK文件,该文件将在执行 IDA 之前重新定义这些环境变量:

C:\Windows\System32\cmd.exe /c “SET PYTHONPATH=C:\Python27x64;C:\Python27x64\Lib;C:\Python27x64\DLLs;C:\Python27x64\Lib\lib-tk; && SET NLSPATH=C:\IDA7.0 && START /D ^”C:\IDA7.0^” ida.exe”
  • 如果你的 IDA 版本未包含 Delphi 编程语言的 FLIRT 签名,仍然可以使用IDR工具生成的 IDC 脚本进行标记。建议仅应用它生成的脚本中的名称。

  • IDA 的最新版本提供了对 Go 语言编写的程序的良好支持。对于旧版本的 IDA,应该使用像golang_loader_assistIDAGolangHelper这样的插件。

  • 为了处理变量扩展混淆,如果可以使用 IDA Hex-Rays 反编译器,请使用基于Z3项目的D-810插件。其界面如下所示:

图 4.42 – D-810 插件支持的去混淆规则

图 4.42 – D-810 插件支持的去混淆规则

  • 通常,恶意软件样本会附带像 OpenSSL 这样的开源库,这些库被静态链接以利用正确实现的加密算法。分析这样的代码可能相当棘手,因为可能不容易看出代码的哪一部分属于恶意软件,哪一部分属于合法的库。此外,弄清楚库中每个函数的目的可能需要相当长的时间。像.lib/.a文件这样的开源项目用于所需平台的 OpenSSL(在我们的例子中是 Windows)。编译器应该尽量接近恶意软件所使用的编译器。

  • 从官方网站获取适用于你的 IDA 的Flair工具包。该包包含一套用于从各种对象和库格式(OMF、COFF 等)生成统一 PAT 文件的工具,以及sigmake工具。

  • 生成 PAT 文件,例如,可以使用pcf工具:

pcf libcrypto.a libcrypto.pat
  1. 使用.sig文件:
sigmake libcrypto.pat libcrypto.sig

如有必要,通过编辑创建的.exc文件并重新运行sigmake来解决冲突。

  1. 将生成的.sig文件放入 IDA 根目录下的sig文件夹中。

  2. 按照以下步骤学习如何使用它:

    1. 转到视图 | 打开 子视图 | 签名Shift + F5快捷键)。

    2. 右键点击应用新签名Ins快捷键)。

    3. 找到你指定名称的签名,并通过按确定或双击它来确认。

    4. 另一种方法是使用文件 | 加载文件 | **FLIRT 签名文件...**选项。

另一个创建自定义 FLIRT 签名的流行选项是idb2pat工具。

现在,让我们讨论 IDA 在动态分析方面的功能。

动态分析

现在,除了经典的反汇编功能外,IDA 还具备多种调试选项。以下是一些旨在简化 IDA 动态分析的技巧:

  • 要在 IDA 中调试样本,请确保样本具有可执行文件扩展名(例如 .exe);否则,旧版本的 IDA 可能会拒绝执行它,提示文件不存在。

  • 旧版本的 IDA 没有位于 IDA dbgsrv 文件夹中的win64_remotex64.exe服务器应用程序。如果需要,可以在同一台机器上运行它,并通过调试器 | **进程选项...**选项使它们通过本地主机进行交互。

图形视图仅显示已识别或创建的函数的图形。可以使用空格键快速切换文本视图和图形视图。调试开始时,图形视图中的图形概览窗口可能会消失,但可以通过选择视图 | 图形概览选项来恢复。

  • 默认情况下,IDA 在打开文件时会自动执行分析,这意味着后续解压的代码将不会被分析。要动态修复此问题,请按照以下步骤操作:

    1. 如有必要,通过按下C热键使 IDA 识别解压块的入口点为代码。通常,将其作为函数处理也是有意义的,可以使用P热键来实现。

    2. 将存储解压代码的内存段标记为加载器段。按照以下步骤进行操作:

      1. 进入视图 | 打开子视图 | Shift + F7 快捷键组合)。

      2. 找到存储感兴趣代码的段。

      3. 可以右键点击它并选择**编辑段...**选项,或者使用Ctrl + E 快捷键组合。

      4. 加载器段复选框中打勾。

    3. 重新运行分析,方法是进入选项 | 常规... | 分析,然后按下重新分析程序按钮,或者右键点击主 IDA 窗口的左下角,在那里选择重新分析程序选项。

  • 如果需要解压 DLL,请按照以下步骤进行操作:

    1. 像加载任何其他可执行文件一样将其加载到 IDA 中。

    2. 选择你偏好的调试器:

      • 本地 Win32 调试器,适用于 32 位 Windows

      • 使用win64_remote64.exe应用程序进行远程 Windows 调试,适用于 64 位 Windows。

    3. 进入rundll32.exe(或regsvr32.exe,用于 COM DLL,可通过DllRegisterServer/DllUnregisterServer或存在的DllInstall导出进行识别)到应用程序字段。

    4. 参数字段中设置 DLL 的完整路径。附加参数会根据 DLL 的类型有所不同:

a. 对于使用rundll32.exe加载的典型 DLL,追加要调试的导出函数的名称或序数(例如,#1),并用逗号与路径分开。即使只想执行主入口点逻辑,也必须提供参数。

b. 对于CPlApplet导出,可以在分析的 DLL 路径之前指定shell32.dll,Control_RunDLL参数。

c. 对于通常使用regsvr32.exe加载的 COM DLL,如果需要调试DllUnregisterServer导出,则应在完整路径前添加/u参数。对于DllInstall导出,应改为使用/n/i[:cmdline]参数的组合。

d. 如果 DLL 是一个服务 DLL(通常可以通过ServiceMain导出函数和与服务相关的导入来识别),并且您需要正确调试ServiceMain,请参阅第三章x86/x64 的基本静态和动态分析,获取有关如何调试服务的详细信息。

  • 在用于动态分析的其他脚本中,funcap工具似乎非常方便,因为它允许您记录在执行过程中传递给函数的参数,并在完成后将它们保留在注释中。

  • 如果在解密后,恶意软件不断使用另一个内存段的代码和数据(Trickbot 是一个很好的例子),可以将这些段转储到 IDB 中,然后使用0分别添加它们,并在0中指定实际虚拟地址,可以通过转到View | Open subviews | Selectors并将相关选择器的值更改为零来修复它。

IDA 脚本的经典和新语法

谈到脚本编写,最初编写 IDA 脚本的方式是使用专有的 IDC 语言。它提供了多个高级 API,可以在静态和动态分析中使用。后来,IDA 开始支持 Python,并通过idc模块提供与同名 IDC 函数的访问。另一个功能(通常更低级)可以在idaapiidautils模块中找到,但对于自动化大多数通用操作而言,idc模块已经足够好用。

随着 API 列表随着时间的推移不断扩展,累积了越来越多的命名不一致性。最终,在某个阶段,开始需要进行修订,这是不可能同时保持向后兼容性的。因此,从 IDA 版本 7.0 开始(在 6.95 之后的下一个版本),引入了一个新的 API 列表,影响了依赖 SDK 和 IDC 函数的插件。其中一些仅仅从CamelCase改为underscore_case,而其他一些则被替换为新的函数。

这里有一些示例,展示了它们的原始和新语法:

  • Functions/NextFunctionget_next_func允许您迭代函数。

  • Heads/NextHeadnext_head 允许你遍历指令。

  • ScreenEAget_screen_ea 获取当前光标所在位置的样本虚拟地址。

  • Byte/Word/Dwordbyte/word/dword 读取特定大小的值。* PatchByte/PatchWord/PatchDwordpatch_byte/patch_word/patch_dword 写入特定大小的块。* OpEnumExop_enum 将操作数转换为 enum 值。

辅助数据存储

  • AddEnumadd_enum 添加一个新的 enum

  • AddStrucExadd_struc 添加一个新的结构。

这是一个实现自定义 XOR 解密算法的 IDA Python 脚本示例,适用于短块:

图 4.43 – 32 位 Windows 的原始 IDA Python API 语法

图 4.43 – 32 位 Windows 的原始 IDA Python API 语法

这里有一个实现相同自定义 XOR 解密算法的脚本,适用于使用新语法的 64 位架构:

图 4.44 – 64 位 Windows 的新 IDA Python API 语法

图 4.44 – 64 位 Windows 的新 IDA Python API 语法

一些情况可能需要大量时间来分析一个相对较大的样本(或多个样本),如果工程师没有使用 IDA 脚本,并且恶意软件使用动态字符串解密和动态 WinAPI 解析。

动态字符串解密

在这种情况下,块状的加密字符串不会一次性解密。相反,每个字符串在使用前会立即被解密,因此它们从未被一次性解密。为了解决这个问题,请按以下步骤操作:

  1. 查找负责解密所有字符串的函数。

  2. 在脚本中复制解密器的行为。

  3. 让脚本通过跟踪交叉引用查找代码中所有调用此函数的地方,并读取将作为其参数传递的加密字符串。

  4. 解密它并将其写回加密内容上方,以确保所有引用保持有效。

动态 WinAPI 解析

使用动态 WinAPI 解析时,只有一个具有不同参数的函数用于访问所有 WinAPI。它动态地搜索请求的 API(通常是相应的 DLL),通常使用提供的名称的某种校验和作为参数。使其可读的两种常见方法是:

  • enum 值。

  • 查找所有使用解析函数的地方,获取其校验和参数,并将其转换为相应的 enum 名称。

  • 使用注释

    1. 查找所有校验和、API 和 DLL 的匹配项。

    2. 将关联存储在内存中。

    3. 查找所有使用解析函数的地方,获取其校验和参数,并在旁边添加带有相应 API 名称的注释。

IDA 脚本实际上是区别初学者与专业分析师之间的关键,它能够帮助分析师高效地解决任何逆向工程问题。一旦你写了几个脚本并采用了这种方法,你就会发现更新或扩展这些脚本,增加新任务的额外功能变得相当简单。

总结

在本章中,我们介绍了各种类型的加壳工具并解释了它们之间的差异。我们还给出了如何识别所使用的加壳工具的建议。接着,我们介绍了几种如何自动和手动解包样本的技术,并提供了实际的例子,展示在不同情况下如何以最有效的方式进行解包。之后,我们介绍了更高级的手动解包方法,这些方法通常需要更多的时间来执行,但能让你在合理的时间内解包几乎任何样本。

此外,我们还介绍了不同的加密算法,并提供了如何识别和处理它们的指南。接着,我们通过一个现代恶意软件的例子,结合这些指南,帮助你了解如何将所有这些理论应用到实践中。最后,我们讲解了 IDA 脚本语言——这是一种大大加速分析过程的强大工具。

第五章检查进程注入与 API 钩子 中,我们将扩展对恶意软件作者用于实现其目标的各种技术的理解,并提供一些应对这些技术的小贴士。

第五章:检查进程注入和 API 挂钩

在本章中,我们将探索恶意软件作者为多种目的使用的更高级技术,包括绕过防火墙、欺骗逆向工程师、以及监视和收集用户信息以窃取信用卡数据等。

我们将深入探讨各种进程注入技术,包括 DLL 注入和进程空洞(由 Stuxnet 引入的高级技术),并解释如何处理这些技术。随后,我们将了解 API 挂钩、IAT 挂钩以及恶意软件作者使用的其他挂钩技术,并讲解如何应对它们。

到本章结束时,你将拓展你对 Windows 平台的知识,并能够分析更复杂的恶意软件。你将学习如何分析其他进程中的注入代码,通过内存取证检测它,检测不同类型的 API 挂钩技术,并分析它们以检测浏览器中人攻击MiTB)。

为了使学习过程更加顺利,本章分为以下几个主要部分:

  • 理解进程注入

  • DLL 注入

  • 更深入地了解进程注入

  • 代码注入的动态分析

  • 进程注入的内存取证技术

  • 理解 API 挂钩

  • 探索 IAT 挂钩

理解进程注入

进程注入是恶意软件作者用来绕过防火墙、执行内存取证技术、以及通过将恶意功能添加到合法进程中并以此方式隐藏来拖慢经验不足的逆向工程师的其中一种最著名的技术。在本节中,我们将介绍进程注入背后的原理,以及为什么它在如今的高级持续性威胁APT)攻击中被广泛使用。

什么是进程注入?

在 Windows 操作系统中,进程被允许在另一个进程的虚拟地址空间中分配内存、读取和写入数据,还可以创建新线程、挂起线程并更改这些线程的寄存器,包括explorer.exe或其他用户的进程。然而,将代码注入到当前用户的浏览器和其他进程中仍然是可以的。

该技术被多个终端安全产品合法使用,用于监控应用程序和沙盒目的(正如我们将在理解 API 挂钩一节中看到的那样),但也常常被恶意软件作者滥用。

为什么要使用进程注入?

对于恶意软件作者而言,进程注入有助于他们实现以下目标:

  • 绕过简单的防火墙,这些防火墙只允许浏览器或其他允许的应用程序连接互联网。通过将代码注入这些应用程序之一,恶意软件可以在没有任何警告或被防火墙阻止的情况下与指挥与控制C&C)服务器进行通信。

  • 通过在另一个未监控且未调试的进程中运行恶意代码,避开调试器和其他动态分析或监控工具。

  • 在恶意软件将代码注入的合法进程中钩取 API,这可以对受害者进程的行为进行独特控制。

  • 为无文件恶意软件维持持久性。通过将代码注入到后台进程中,恶意软件可以在几乎不重启的服务器上保持持久性,而不在硬盘上留下可执行文件。

现在,我们将深入探讨各种进程注入技术,了解它们的工作原理以及如何应对这些技术。我们将从最简单、最直接的技术开始:DLL 注入。

DLL 注入

Windows 操作系统允许进程将 DLL 加载到其他进程中,出于安全原因、沙箱隔离或甚至图形处理。在本节中,我们将探讨合法的、直接的将 DLL 注入进程的方法,以及其他允许攻击者使用 Windows API 将代码注入进程的技术。

Windows 支持的 DLL 注入

Windows 为符合特定条件的每个进程提供了特殊的注册表项,以便加载 DLL。许多注册表项允许恶意软件 DLL 同时注入到多个进程中,包括浏览器和其他合法进程。这些注册表项有很多,我们将在这里探讨最常见的几个:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs

这是恶意软件最常误用的注册表项之一,用来将 DLL 代码注入到其他进程中并维持持久性。此处指定的库与每个加载 user32.dll(主要用于 UI 的系统库)的进程一起加载。

在 Windows 7 中,DLL 必须签名,默认情况下此逻辑在 Windows 8 及更高版本中被禁用。然而,攻击者仍然可以通过将 RequireSignedAppInit_DLLs 设置为 False,并将 LoadAppInit_DLLs 设置为 True 来滥用这一点(请参见下面的截图)。攻击者需要管理员权限才能设置这些条目,可以通过社交工程等手段解决这一问题:

图 5.1 – 使用 AppInit_DLLs 注册表项将恶意软件库注入不同的浏览器

图 5.1 – 使用 AppInit_DLLs 注册表项将恶意软件库注入不同的浏览器

现在, let’s move to the next commonly misused registry key:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\AppCertDlls

该注册表项中列出的库会被加载到使用以下任一函数的每个进程中:

  • CreateProcess

  • CreateProcessAsUser

  • CreateProcessWithLogonW

  • CreateProcessWithTokenW

  • WinExec

这使得恶意软件可以注入到大多数浏览器中(因为许多浏览器会创建子进程来管理不同的标签页)以及其他应用程序。它仍然需要管理员权限,因为 HKEY_LOCAL_MACHINE 对普通用户在 Windows 系统上是不可写的(Vista 及更高版本):

HKEY_CURRENT_USER\Software\Classes\<AppName>\shellex\ContextMenuHandlers

该路径加载一个 shell 扩展(一个 DLL 文件),以便为主 Windows shell(explorer.exe)添加附加功能。基本上,它可以被滥用来将恶意库加载为 explorer.exe 的扩展。此路径可以轻松创建和修改,而无需任何管理员权限。

还有其他注册表项可以将恶意库注入到其他进程中,以及多个软件解决方案,例如 Sysinternals 的 Autoruns,可以让你查看是否有任何这些注册表项被用于当前系统的恶意用途:

图 5.2 – Sysinternals 套件中的 Autoruns 应用程序

图 5.2 – Sysinternals 套件中的 Autoruns 应用程序

这些是恶意软件最常用的合法方式,用来将 DLL 注入到不同的进程中。

重要说明

值得一提的是,许多资源称这种技术为 DLL 劫持,并将其与经典的进程注入分开追踪,因为在这种情况下,攻击者依赖操作系统来执行实际的注入,而不是自己进行注入。

现在,我们将探索更高级的技术,这些技术需要使用不同的 Windows API 来分配、写入并执行恶意代码在其他进程中。

一种简单的 DLL 注入技术

该技术使用 LoadLibraryA API(或其其他变体)作为通过 Windows PE 加载器加载恶意库并执行其入口点的方式。主要目标是将恶意 DLL 的路径注入到进程中,然后将控制权转交给该进程,启动地址为 LoadLibraryA API 的地址。当将 DLL 路径作为参数传递给该线程(该参数传递给 LoadLibraryA API)时,Windows PE 加载器会将 DLL 加载到进程中并无误地执行其代码。以下是结果内存的样子:

图 5.3 – 一个简单的 DLL 注入机制

图 5.3 – 一个简单的 DLL 注入机制

恶意软件通常遵循的确切步骤如下:

  1. 在其他进程中找到目标进程(更多细节见下节)。

  2. 使用 OpenProcess API 获取该进程的句柄,作为标识符传递给其他 API。

  3. 使用 VirtualAllocExVirtualAllocExNumaNtAllocateVirtualMemory 或类似的 API,在该进程的虚拟内存中分配一个空间。这个空间将用于写入恶意 DLL 文件的完整路径。另一种选择是使用 CreateFileMapping -> MapViewOfFileCreateSectionEx -> NtCreateSection API 来准备该空间。

  4. 使用 WriteProcessMemoryNtWriteVirtualMemoryNtWow64WriteVirtualMemory64 等 API,或者借助 NtMapViewOfSection,将恶意 DLL 的路径写入进程中。

  5. 使用诸如CreateRemoteThread / NtCreateThreadExSuspendThread -> SetThreadContext -> ResumeThreadQueueUserAPC / NtQueueApcThread,甚至SetWindowHookEx等 API 加载并执行此 DLL,提供LoadLibraryA地址作为起始地址,DLL 路径的地址作为参数。

也可以使用具有类似功能的替代 API,例如,使用未记录的RtlCreateUserThread API 替代CreateRemoteThread

与我们将在接下来的章节中介绍的技术相比,这种技术相对简单。然而,该技术会在进程信息中留下恶意 DLL 的痕迹。任何简单的工具,例如LoadLibraryA,都可以检测到这一点。

在下一节中,我们将深入探讨并介绍更多高级技术。它们仍然依赖于我们之前描述的 API,但包括更多步骤,以确保进程注入的成功。

深入研究进程注入

在本节中,我们将介绍进程注入的中级到高级技术。这些技术不会在磁盘上留下痕迹,可以使无文件恶意软件保持持久性。在介绍这些技术之前,我们先讨论恶意软件如何找到它想要注入的进程——特别是,它是如何获取正在运行的进程列表,包括它们的名称和进程 IDPID)。

寻找目标进程

为了让恶意软件获取正在运行的进程列表,通常会执行以下步骤:

  1. 创建当前所有正在运行的进程快照。该快照包含关于所有运行进程的信息,包括它们的名称、PID 和其他重要信息。可以通过CreateToolhelp32Snapshot API 获取此快照。通常,当TH32CS_SNAPPROCESS作为参数传递时(用于获取正在运行的进程的快照,而不是线程或已加载的库)。

  2. 使用Process32First API 获取列表中的第一个进程。此 API 获取快照中的第一个进程,并开始对进程列表进行迭代。

  3. 循环调用Process32Next API,依次获取列表中的每个进程,包括其名称和 PID,如下图所示:

图 5.4 – 使用 CreateToolhelp32Snapshot 进行进程搜索

图 5.4 – 使用 CreateToolhelp32Snapshot 进行进程搜索

一旦找到目标进程,恶意软件就进入下一阶段,通过执行OpenProcess API,并传入进程的 PID,就像我们在上一节中学到的那样。

代码块注入

这项技术与 DLL 注入非常相似。这里的区别实际上在于目标进程内部执行的代码。在这种技术中,恶意软件注入一段汇编代码(作为字节数组),并直接将控制权转交给它。这段代码是位置无关的。它具有加载自己的导入表、访问自己的数据,并在目标进程内执行所有恶意活动的能力。

恶意软件执行这些代码注入技术的步骤与前面的步骤几乎相同:

  1. 搜索目标进程(在图 5.4中,恶意软件通过 PID 跳过其他进程)。

  2. 获取该进程的句柄或其他标识符。

  3. 为这个进程的内存准备好足够的空间,以容纳将要注入的整个恶意代码(请参见图 5.5中的VirtualAllocEx调用)。

  4. 将这段代码复制到目标进程中(请参见图 5.5中的WriteIntoProcessMemory函数)。

  5. 将控制权转移到受害进程地址空间中的这段代码(请参见图 5.5中的CreateRemoteThreadFunc例程)。

一些恶意软件会将恶意软件进程的名称或 PID 传递给这段注入的代码,以便它能终止恶意软件(并可能删除其文件及所有痕迹),以确保没有恶意软件存在的明确证据。

在以下截图中,我们可以看到典型的代码注入示例:

图 5.5 – 代码注入示例

图 5.5 – 代码注入示例

与 DLL 注入在进程注入步骤上非常相似,但大部分繁重的工作都在这一段汇编代码中。我们将在第八章 处理漏洞和 Shellcode 中深入探讨这种位置独立、PE 独立的代码(即 Shellcode)。我们将解释它如何找到自己在内存中的位置,如何访问 API,以及如何执行恶意任务。

反射式 DLL 注入

在这种情况下,恶意软件不是注入代码块,而是将整个 DLL 注入到目标进程的内存中,但这次是直接从内存中读取,而不是从磁盘中读取。在这种情况下,加载程序将负责加载此负载,手动完成 Windows 加载程序的工作。

首先,恶意软件准备与 ImageBase 大小相同的内存,并按照 PE 加载步骤执行,包括导入表加载和修复重定位条目(在重定位表中,如我们在第三章 x86/x64 基本静态与动态分析 中所学到的),如以下截图所示:

图 5.6 – Shellcode 中的 PE 加载过程

图 5.6 – Shellcode 中的 PE 加载过程

正如我们在这里看到的,利用memcpy函数的帮助,每个部分在LoopOnSections循环中被单独复制。这个技术在结果上与 DLL 注入类似,但它不需要恶意 DLL 存储在硬盘上,也不会在进程环境块PEB)中留下 DLL 的常见痕迹。因此,只依赖 PEB 来检测 DLL 的内存取证应用程序将无法检测到加载在内存中的这个 DLL。更多细节可以在后面内存取证技术与进程注入部分中找到。

Stuxnet 秘密技术 – 进程空洞化

空心进程注入process hollowing)是一种高级技术,它在 Stuxnet 恶意软件中首次出现,然后在 APT 攻击领域广泛传播。空心进程注入的基本原理是将目标进程的 PE 内存镜像从其虚拟内存中移除,并用恶意软件的可执行文件替换它。

例如,恶意软件创建了一个新的进程,比如svchost.exe。在进程创建并加载了svchost的 PE 文件之后,恶意软件从内存中移除已加载的svchost PE 文件,然后在相同的位置加载恶意软件可执行文件的 PE 文件并继续执行。更多信息请参见以下代码示例。

该机制完全将恶意软件可执行文件伪装成一个合法的外衣,因为 PEB 和等同的EPROCESS对象仍然保存有关合法进程的信息。这有助于恶意软件绕过防火墙和内存取证工具。

这种形式的代码注入过程与之前的有所不同。以下是恶意软件为了实现这一点所需执行的步骤:

  1. 在挂起模式下创建一个合法进程,该进程创建进程及其第一个线程,但不会启动它:

图 5.7 – 在挂起模式下创建进程

图 5.7 – 在挂起模式下创建进程

使用VirtualFreeEx卸载合法应用程序的内存镜像(实现进程空心化)。

  1. 在内存中分配与卸载的 PE 镜像相同的空间(例如,使用VirtualAllocEx等 API 允许恶意软件选择一个空闲的首选地址进行分配)。

  2. 通过加载 PE 文件并修复其导入表(如果需要,解决其重定位表),将恶意软件可执行文件注入该空间。

  3. 使用SetThreadContext API 将线程的起始点更改为恶意软件的入口点。GetThreadContext API 允许恶意软件获取所有寄存器的值、线程状态以及恢复线程所需的所有信息,而SetThreadContext API 允许恶意软件更改这些值,包括 EIP/RIP 寄存器(指令指针),使其指向新的入口点。最后一步是恢复该挂起线程,从该点执行恶意软件:

图 5.8 – SetThreadContext 和 ResumeThread

图 5.8 – SetThreadContextResumeThread

这是最著名的空心进程注入技术。还有类似的技术,它们不卸载实际进程,而是将恶意软件和合法应用程序的可执行文件一起包括在内。

现在,我们将看看如何在我们的动态分析过程或内存取证过程中提取注入的代码并进行分析。

代码注入的动态分析

进程注入的动态分析相当棘手。恶意软件会从调试的进程中逃逸,转而在另一个进程中运行 shellcode 或加载 DLL。以下是一些可能帮助你调试注入代码的技巧。

技巧 1 – 在当前位置调试

第一个技巧,许多工程师首选的技巧,是不允许恶意软件注入 shellcode,而是将 shellcode 在恶意软件的内存中调试,仿佛它已经被注入。通常,恶意软件会将其 shellcode 注入另一个进程并从该 shellcode 的特定位置执行。我们可以在恶意软件的二进制文件中(或者如果被解密,可以在内存中)找到该 shellcode,并将 EIP/RIP 寄存器 (OllyDbg 中的 New origin here) 设置为该 shellcode 的入口点,然后从那里继续执行。这使得我们能够在调试的进程中执行 shellcode,甚至绕过一些检查,这些检查是用于检查该 shellcode 应该在哪个进程中运行的。

执行此技术的步骤如下:

  1. 一旦恶意软件调用诸如 VirtualAllocEx 等 API 为目标进程的内存准备 shellcode 空间,保存该分配空间的返回地址(假设返回地址为 0x300000)。

  2. 在内存写入 API 上设置断点,如 WriteProcessMemory,一旦触发,保存源地址和目标地址。源地址是恶意进程内 shellcode 在内存中的地址(假设是 0x450000),目标地址可能是 VirtualAllocEx 返回的地址。

  3. 现在,在控制转移 API 上设置一个断点,比如 CreateRemoteThread,并获取目标进程中该 shellcode 的入口点(如果有参数,也要获取参数)(假设入口点是 0x30012F)。

  4. 现在,计算恶意进程内 shellcode 入口点的地址,假设在此案例中为 0x30012F - 0x300000 + 0x450000 = 0x45012F

  5. 如果使用虚拟机进行调试(强烈推荐),首先保存一个快照,然后将 EIP 值设置为 shellcode 的入口点(0x45012F),设置任何必要的参数,并从那里继续调试。

这个技巧非常简单,调试和处理起来也很容易。然而,它仅适用于简单的 shellcode,且不适用于多重注入(多次调用 WriteProcessMemory)、进程空洞技术或复杂参数。之后需要小心调试,以避免由于 shellcode 在与预期不同的进程中运行而导致错误或漏洞。

技巧 2 – 附加到目标进程

另一个简单的解决方案是在恶意软件执行 CreateRemoteThread 之前附加到目标进程,或者修改 CreateRemoteThread 的创建标志为 CREATE_SUSPENDED,如以下所示:

CreateRemoteThread(Process, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibrary, (LPVOID)Memory, CREATE_SUSPENDED, NULL);

为了能够做到这一点,我们需要知道恶意软件将注入的目标进程。这意味着我们需要在Process32FirstProcess32Next这两个 API 上设置断点,并分析搜索 API 的代码,如strcmp或等效代码,以找到要注入的目标进程。并非所有的调用都是为了进程注入;例如,它们也可以作为反逆向工程的技巧,正如我们将在第六章中看到的那样,绕过反逆向工程技术

技术 3 – 处理进程空洞化

不幸的是,前两种技术在进程空洞化(process hollowing)中不起作用。在进程空洞化中,恶意软件创建了一个处于挂起状态的新进程,这使得 OllyDbg 和类似的调试器无法检测到它。因此,在恶意软件恢复进程并执行恶意代码之前,很难附加到它们,因为此时恶意代码已经未调试且未监控地执行了。

正如我们之前提到的,在进程空洞化中,恶意软件将合法的应用程序 PE 镜像空洞化,并将恶意 PE 镜像加载到目标进程内存中。处理此问题的最简单方法是设置在内存写入 API(如WriteProcessMemory)上的断点,在 PE 文件加载到目标进程内存之前将其转储。一旦断点触发,跟踪WriteProcessMemory的源参数,并向上滚动直到找到 PE 文件的起始位置(通常可以通过MZ签名和常见的This program cannot run in DOS mode文本识别,如以下屏幕截图所示):

图 5.9 – 在 OllyDbg 中的 PE 文件十六进制转储

图 5.9 – 在 OllyDbg 中的 PE 文件十六进制转储

一些恶意软件家族使用CreateSectionMapViewOfSection代替WriteProcessMemory。正如我们之前描述的,这两个 API 创建了一个内存对象,恶意可执行文件可以写入其中。这个内存对象也可以映射到另一个进程中。所以,在恶意软件将恶意 PE 镜像写入内存对象后,它将其映射到目标进程中,然后使用如CreateRemoteThread等 API 从其入口点开始执行。在这种情况下,我们可以在MapViewOfSection上设置断点,以获取映射内存对象的返回地址(在恶意软件写入任何数据之前)。

现在,可以设置一个写入断点,监视写入到此返回地址的任何操作(写入此内存对象等同于WriteProcessMemory)。

一旦您的断点触发,我们就能找出写入到该内存对象的数据(在进程空洞化的情况下,这很可能是一个 PE 文件)以及数据的来源,它包含了所有已卸载的 PE 文件,这样我们就可以轻松地将其转储到磁盘,并将其加载到调试器中,就像它被注入到另一个进程一样。

简而言之,这项技术就是在文件加载之前找到 PE 文件并将其作为普通可执行文件转储。一旦获取到文件,我们就得到了第二阶段的有效载荷。现在,我们只需要在调试器中调试它或对其进行静态分析。

现在,我们将看看如何使用一款名为 Volatility 的内存取证工具,从内存转储中检测和转储注入的代码(或注入的 PE 文件)。这可能比使用动态分析处理进程注入更加复杂。

进程注入的内存取证技术

由于使用进程注入的主要原因之一是为了隐藏恶意软件在内存取证工具中的存在,使用这些工具进行检测变得相当棘手。在本节中,我们将看看可以使用哪些不同的技术来检测不同类型的进程注入。

在这里,我们将使用一个名为 Volatility 的工具。这个工具是一个免费的开源内存取证程序,能够分析受感染机器的内存转储。那么,让我们开始吧。

技巧 1 – 检测代码注入和反射式 DLL 注入

检测进程中注入代码的主要红旗是,包含 shellcode 或加载的 DLL 的分配内存总是具有 EXECUTE 权限,并且不代表映射文件。当一个模块(可执行文件)通过 Windows PE 加载器加载时,它会被加载并带有 IMAGE 标志,以表示它是一个可执行文件的内存映射。但是,当这个内存页面正常通过 VirtualAlloc 分配时,它会被分配为 PRIVATE 标志,以表示它是为数据分配的:

图 5.10 – 一个 OllyDbg 内存映射窗口(加载的映像内存块和私有内存块)

图 5.10 – 一个 OllyDbg 内存映射窗口(加载的映像内存块和私有内存块)

私有分配内存具有 EXECUTE 权限并不常见,通常也不常见(如大多数 shellcode 注入所做的那样)拥有 WRITE 权限和 EXECUTE 权限(READ_WRITE_EXECUTE)。

在 Volatility 中,有一个名为 malfind 的命令。该命令可以在进程(或整个系统)中查找隐藏的和注入的代码。执行该命令时(给定镜像名称和操作系统版本),如果需要扫描特定进程,可以使用 PID 作为参数;如果不指定 PID,则会扫描整个系统,如下图所示:

图 5.11 – Volatility 中的 malfind 命令检测到一个 PE 文件(通过 MZ 头部)

图 5.11 – Volatility 中的 malfind 命令检测到一个 PE 文件(通过 MZ 头部)

如我们所见,malfind 命令在 Adobe Reader 进程中通过 MZ 头部检测到了一个注入的 PE 文件,地址为 0x003d0000

现在,我们可以使用 vaddump 命令转储此进程中的所有内存镜像。该命令会转储进程内部的所有内存区域,遵循该进程的 EPROCESS 内核对象及其虚拟内存映射(以及等效的物理内存页)。vaddump 将把所有内存区域转储到一个单独的文件中,如下图所示:

图 5.12 – 使用 Volatility 中的 vaddump 命令转储 0x003d000 地址

图 5.12 – 使用 Volatility 中的 vaddump 命令转储 0x003d000 地址

对于注入的 PE 文件,我们可以使用 dlldump 而不是 vaddump 将其转储到磁盘(并重建其头部和节,但不重建导入表),如下图所示:

图 5.13 – 使用 dlldump 给定 PID 和 DLL 的 ImageBase 作为 --base

图 5.13 – 使用 dlldump 给定 PID 和 DLL 的 ImageBase 作为 --base

之后,我们将获得恶意软件 PE 文件(或 shellcode)的内存转储,用于扫描和分析。这个转储不是完美的,但我们可以使用 strings 工具扫描它,或对其进行静态分析。我们可能需要通过在调试器中修复导入表的地址,并重新转储,或直接调试它来手动修复这些地址。

技巧 2 – 检测进程空洞化

当恶意软件从其进程中将应用程序 PE 镜像挖空时,Windows 会删除该内存空间与应用程序 PE 文件之间的任何连接。因此,在该地址上的任何分配都变成私有的,并且不代表任何已加载的镜像(或 PE 文件)。

然而,这种脱离仅发生在 EPROCESS 内核对象中,而不会发生在进程内存中可以访问的 PEB 信息中。在 Volatility 中,有两个命令可以列出进程中所有加载的模块。一个命令列出来自 PEB 信息(用户模式)的加载模块,即 dlllist,另一个列出来自 EPROCESS 内核对象信息(内核模式)的所有加载模块,即 ldrmodules。这两个命令的结果之间的任何不匹配都可能表示进程注入空洞化,如下图所示:

图 5.14 – 0x01000000 地址上的 lsass.exe 在 ldrmodules 中未链接到其 PE 文件

图 5.14 – 0x01000000 地址上的 lsass.exe 在 ldrmodules 中未链接到其 PE 文件

存在多种类型的不匹配,它们代表不同类型的进程空洞化,如下所示:

  • 当应用程序模块未链接到其 PE 文件时,如图 5.14所示,表示该进程已被空洞化,并且恶意软件已加载到同一位置。

  • 当应用程序模块出现在 dlllist 结果中,但在 ldrmodules 结果中完全没有时,这表示进程已被空洞化,且恶意软件可能已加载到另一个地址。malfind 命令可以帮助我们找到新地址,或者使用 vaddump 导出该进程中的所有内存区域,并扫描它们以查找 PE 文件(搜索 MZ 魔术字)。

  • 当应用程序出现在两个命令的结果中,并且与应用程序的 PE 文件名相关联,但在两个结果中模块地址不匹配时,这表示该应用程序并未被空洞化,而是恶意软件已被注入,且 PEB 信息已被篡改,以链接到恶意软件而不是合法应用程序的 PE 镜像。

在所有这些情况下,显示恶意软件使用进程空洞技术注入到该进程内部,vaddumpprocdump 将帮助导出恶意软件的 PE 镜像。

技术 3 – 使用 HollowFind 插件检测进程空洞化

有一个名为 HollowFind 的插件,它将所有这些命令结合起来。它可以找到可疑的内存空间或空洞进程的证据,并返回这些结果,如下图所示:

图 5.15 – HollowFind 插件用于检测空洞进程注入

图 5.15 – HollowFind 插件用于检测空洞进程注入

该插件还可以将内存镜像转储到指定目录:

图 5.16 – HollowFind 插件用于导出恶意软件的 PE 镜像

图 5.16 – HollowFind 插件用于导出恶意软件的 PE 镜像

所以,这就是关于进程注入的内容,以及如何使用 OllyDbg(或任何其他调试器)动态分析它,另外如何使用 Volatility 在内存转储中检测它。

在接下来的章节中,我们将介绍恶意软件作者使用的另一种重要技术,称为 API hooking。它通常与进程注入结合使用,用于中间人攻击(MITM)或使用用户模式根套件技术隐藏恶意软件的存在。

理解 API hooking

API hooking 是恶意软件作者常用的技术,用来拦截对 Windows API 的调用,以便更改这些命令的输入或输出。它是基于我们之前描述的进程注入技术。

这种技术使恶意软件作者能够完全控制目标进程,因此可以控制用户与该进程交互时的体验,包括浏览器和网页、杀毒软件及其扫描的文件等。通过控制 Windows API,恶意软件作者还可以从进程内存和 API 参数中捕获敏感信息。

由于 API hooking 被恶意软件作者使用,它也有不同的合法用途,例如恶意软件沙箱化和旧应用程序的向后兼容性。

因此,Windows 正式支持 API 钩取,正如我们在本章后续部分将看到的那样。

为什么需要 API 钩取?

恶意软件采用 API 钩取的原因有多个。让我们详细了解这一过程,并涵盖恶意软件作者通常钩取的 API,以实现他们的目的:

  • Process32FirstProcess32Next,这样可以将恶意软件进程从结果中移除

  • 文件列举 API,如 FindFirstFileAFindNextFileA

  • 注册表枚举 API,如 RegQueryInfoKeyRegEnumKeyEx

  • InternetConnectAHttpSendRequestAInternetReadFilewininet.dll API。ws2_32.dll 中的 WSARecvWSASend 也是可能的选择。* Firefox API,如 PR_ReadPR_WritePR_Close。* CreateProcessACreateProcessAsUserA 和类似的 API,用于注入到子进程或阻止某些进程启动。钩取 LoadLibraryALoadLibraryExA 也是可能的。

WinAPI 的 AW 版本(分别用于 ANSI 和 Unicode)可以通过相同的方式进行钩取。

使用 API 钩取

在本节中,我们将探讨不同的 API 钩取技术,从仅能更改 API 参数的简单方法,到用于不同银行木马(包括 Vawtrak)的更复杂方法。

内联 API 钩取

要钩取 API,恶意软件通常会修改 API 汇编代码的前几个字节(通常是 5 个字节),并用 jmp <hooking_function> 替换它们,从而改变 API 的参数,甚至跳过对该 API 的调用,返回一个假结果(如错误或 NULL)。在钩取之前,代码变化通常如下:

API_START:
mov edi, edi
push ebp
mov ebp, esp
...

然后,钩取后的代码如下所示:

API_START:
jmp hooking_function
...

因此,恶意软件将前 5 个字节(在本例中是三条指令)替换为一条指令,即 jmp 跳转到钩取函数。Windows 支持 API 钩取,并且添加了一条额外的指令 mov edi, edi,该指令占用 2 个字节,使得函数前导代码的大小为 5 个字节。这使得 API 钩取变得更加容易执行。

hooking_function 例程保存了替换的前 5 个字节,并使用它们来回调 API,例如,代码如下:

hooking_function:
...
<change API parameters>
...
mov edi, edi
push ebp
mov ebp, esp
jmp API+5 ; jump to the API after the first replaced 5 bytes

通过这种方式,hooking_function 可以无缝运行而不影响程序流。它可以改变 API 的参数,从而控制结果,并且可以直接执行 ret 返回程序,而不实际调用 API。

带有跳板的内联 API 钩取

在之前的简单钩取函数中,恶意软件可以更改 API 的参数。但是当使用跳板时,恶意软件还可以更改 API 的返回值及其相关数据。跳板只是一个小函数,它只执行 jmp 跳转到 API,并包含前 5 个缺失的字节(或三条指令,如前面的例子所示),如下所示:

trampoline:
mov edi, edi
push ebp
mov ebp, esp
jmp API+5 ; jump to the API after the first replaced 5 bytes

钩子函数不会跳回 API(因为这会最终将控制权交还给程序),而是将跳板作为 API 的替代调用。这个跳板将控制权转交给实际的 API,但当它完成执行后,控制权会被传回给钩子函数,API 的返回值会在返回控制权给程序之前由钩子函数进行修改,如下图所示:

图 5.17 – 带有跳板的钩子函数

图 5.17 – 带有跳板的钩子函数

钩子函数的代码看起来更加复杂:

hooking_function:
...
<change API parameters>
...
push API_argument03
push API_argument02
push API_argument01
call trampoline ; trampoline routine will execute jmp to the API, and, once done, the API will  return control back here
...
<change API return value>
...
ret ; return control back to the main program

这一步骤使恶意软件能够更好地控制 API 及其输出,例如,它可以将 JavaScript 代码注入到 InternetReadFilePR_Read 或其他 API 的输出中,从而窃取凭据或将钱转入其他银行账户。

使用长度反汇编器的内联 API 钩子

正如我们在之前的技术中看到的,API 钩子在你在每个 API 开始使用 mov edi, edi 指令时是非常简单的,这使得前 5 个字节在 API 钩子功能中是可预测的。不幸的是,并非所有 Windows API 都是这样,因此有时恶意软件家族不得不反汇编前几个指令,以避免破坏 API。

一些恶意软件家族,如 Vawtrak,使用长度反汇编器将一些指令(大小等于或大于 5 字节)替换为跳转指令(jmp),跳转到钩子函数,如下图所示。然后,它们将这些指令复制到跳板中,并向 API 添加一个 jmp 指令:

图 5.18 – 使用反汇编器的 Vawtrak API 钩子

F

图 5.18 – 使用反汇编器的 Vawtrak API 钩子

这样做的主要目的是确保跳板函数不会在指令中途跳回 API,并使 API 钩子能够无缝工作,不会对钩住的进程行为产生不可预测的影响。

使用内存取证检测 API 钩子

正如我们已经知道的,API 钩子通常与进程注入一起使用,在动态分析和内存取证中处理 API 钩子与处理进程注入非常相似。在之前的进程注入检测技术(使用 malfindhollowfind)的基础上,我们可以使用一个叫做 apihooks 的 Volatility 命令。这个命令扫描进程的库,搜索钩住的 API(以 jmpcall 开头),并显示钩住的 API 名称以及钩子函数的地址,如下图所示:

图 5.19 – 用于检测 API 钩子的 Volatility 命令 apihooks

图 5.19 – 用于检测 API 钩子的 Volatility 命令 apihooks

然后我们可以使用vaddump(如本章前面所描述)转储该内存地址,并使用 IDA Pro 或任何其他静态分析工具对 Shellcode 进行反汇编,从而理解该 API 钩子的动机。

最后,让我们来讨论 IAT 钩子。

探索 IAT 钩子

在实际 API 地址上执行jmp(或在将 API 参数推送到堆栈后执行调用),然后返回到实际程序,如下图所示:

图 5.20 – IAT 钩子机制

图 5.20 – IAT 钩子机制

这种钩子方法对于 API 的动态加载(使用GetProcAddressLoadLibrary)并不有效,但对于许多合法应用程序仍然有效,这些应用程序的大部分所需 API 都在导入表中。

摘要

本章中,我们已经介绍了许多恶意软件家族使用的两种非常著名的技术:进程注入和 API 钩子。这些技术用于多种目的,包括伪装恶意软件、绕过防火墙、维持无文件恶意软件的持久性、MITB 攻击等。

我们已经介绍了如何使用动态分析处理代码注入,以及如何检测代码注入和 API 钩子,并如何通过内存取证分析它们。

阅读本章后,您将对复杂的恶意软件以及它如何注入到合法进程中有更深入的了解。这将帮助您分析包含各种技术的网络攻击,并更有效地保护您的组织免受未来威胁。

第六章,《绕过反调试技术》中,我们将介绍恶意软件作者使用的其他技术,这些技术使逆向工程师更难分析样本并理解其行为。

第六章:绕过反向工程技术

在本章中,我们将介绍恶意软件作者用来保护其代码免受未经授权的分析师分析的各种反向工程技术。我们将熟悉各种方法,从检测调试器和其他分析工具,到断点检测、虚拟机VM)检测,甚至攻击反恶意软件工具和产品。

此外,我们还将介绍恶意软件作者用来避免垃圾邮件检测的虚拟机和沙盒检测技术,以及在各种企业中实现的自动恶意软件检测技术。由于这些反向工程技术被恶意软件作者广泛使用,因此了解如何检测和绕过它们非常重要,以便能够分析复杂或高度混淆的恶意软件。

本章分为以下几个部分:

  • 探索调试器检测

  • 处理调试器断点的规避

  • 摆脱调试器

  • 理解混淆技术和反反汇编器

  • 检测并规避行为分析工具

  • 检测沙盒和虚拟机

探索调试器检测

为了让恶意软件作者能够继续其操作而不被防病毒产品或任何打击行动打断,他们必须反击并为他们的工具配备各种反向工程技术。调试器是恶意软件分析师用来剖析恶意软件并揭示其功能的最常用工具。因此,恶意软件作者实施各种反调试技巧,以使分析更加复杂,并隐藏其功能和配置细节(主要是命令与控制服务器C&C)。

使用 PEB 信息

Windows 提供了多种方法来识别调试器的存在;其中许多方法依赖于BeingDebugged中存储的信息,当进程在调试器下运行时,它的值为True。为了访问此标志,恶意软件可以执行以下指令:

mov  eax, dword ptr fs:[30h]     ; PEB
cmp  byte ptr [eax+2], 1 ; PEB.BeingDebugged
jz  <debugger_detected>

如你所见,这里使用fs:[30h]技术找到了 PEB 的指针。恶意软件还可以通过许多其他方式获取 PEB:

  • 通过使用fs:[18h]获取指向 TEB 结构的指针,然后通过偏移量 0x30 查找 PEB。

  • 通过使用NtQueryInformationProcess API 并传递ProcessBasicInformation参数,可以返回PROCESS_BASIC_INFORMATION结构,其中第二个字段PebBaseAddress将包含 PEB 地址。

可以使用IsDebuggerPresent API 来执行完全相同的检查。

NtGlobalFlag是 PEB 中的另一个字段,在 32 位系统上位于偏移量 0x68,在 64 位系统上位于 0xBC,可以用于调试器检测。在正常执行过程中,此标志被设置为零,但当调试器附加到进程时,该标志会设置为以下三个值:

  • FLG_HEAP_ENABLE_TAIL_CHECK (0x10)

  • FLG_HEAP_ENABLE_FREE_CHECK (0x20)

  • FLG_HEAP_VALIDATE_PARAMETERS (0x40)

恶意软件可以通过执行以下指令来检查调试器的存在:

mov eax, fs:[30h] ; Process Environment Block
mov al, [eax+68h] ; NtGlobalFlag
and al, 70h ; Other flags can also be checked this way 
cmp al, 70h ; 0x10 | 0x20 | 0x40
je <debugger_detected>

在这里,恶意软件倾向于通过将这些标志组合成 0x70 的值(使用按位或操作)来检查所有这些标志的存在。

以下逻辑可用于在 64 位环境中检测调试器:

push 60h
pop rsi
gs:lodsq ; Process Environment Block
mov al, [rsi*2+rax-14h] ; NtGlobalFlag 
and al, 70h
cmp al, 70h
je <debugger_detected>

这个例子更棘手,因为我们应该记住lodsq指令会将rsi寄存器的值增加 8(QWORD 的大小)。因此,最终的偏移量将是(0x60 + 0x8)*2 – 0x14 = 0xBC,正如之前提到的那样。

最后,为了检测调试器,恶意软件还可以使用存储在 PEB 中的ProcessHeap结构(32 位的偏移量为 0x18,64 位为 0x30,WoW64 兼容性级别为 0x1030)。该结构有两个感兴趣的字段:

  • Flags(32 位:XP 上的偏移量为 0x0c,Vista+上为 0x40;64 位:XP 上的偏移量为 0x14,Vista+上为 0x70):通常,恶意软件可以检查 0x40000062 位的存在来揭示调试器,或者反过来检查值是否是默认值(2)。

  • ForceFlags(32 位:XP 上的偏移量为 0x10,Vista+上为 0x44;64 位:XP 上的偏移量为 0x18,Vista+上为 0x74):在这里,恶意软件可以检查当调试器存在时,0x40000060 位是否被设置,或者如果没有调试器,则不会设置这些位。

除了直接访问外,可以使用GetProcessHeapRtlGetProcessHeaps API 找到指向ProcessHeap结构的指针。可以通过RtlQueryProcessHeapInformationRtlQueryProcessDebugInformation API 读取ProcessHeap结构中Flags字段的值。

最后,设置这些标志的原因是,当调试器附加时,堆尾检查将被启用,系统将在分配的块的末尾添加0xABABABAB签名。因此,恶意软件可以分配一个堆块并检查该签名是否存在,从而识别调试器的存在:

图 6.1 – 通过堆尾检查检测调试器的存在

图 6.1 – 通过堆尾检查检测调试器的存在

绕过这些检查的常见方法是用NOP指令覆盖它们,或在它们的开始设置一个断点以跳过检查。此外,可以使用专用的调试器插件来更改内存中 PEB 结构的值。

使用 EPROCESS 信息

EPROCESS是另一个系统结构,包含有关进程的信息,可以揭示调试器的存在:

  • 如果进程正在使用远程调试器调试,则DebugPort字段非零。

  • Flags字段包含NoDebugInherit标志,当调试器存在时,该标志被设置为 1。

与 PEB 不同,该结构位于内核模式,因此普通进程无法直接读取它。然而,恶意软件可以使用专门的 API 读取其值:

  • CheckRemoteDebuggerPresent:它会检查 EPROCESS 结构体中的 DebugPort 字段。

  • NtQueryInformationProcess:这取决于以下参数:

    • 使用 ProcessDebugPort(7)参数时,它会检查 DebugPort 字段,如果进程正在被调试,则返回 -1。

    • 使用 ProcessDebugFlags (0x1F) 时,它会返回一个相反的 NoDebugInherit 值。

使用 DebugObject

当调试器存在时,系统会创建一个专用的 DebugObject。虽然此时恶意软件无法判断是它的样本正在被调试,还是可能是其他东西,但对于某些恶意软件编写者来说,这仍然是一个警示信号。他们可以使用以下 API 来检查其存在:

  • NtQueryInformationProcess:使用 ProcessDebugObjectHandle(0x1E)参数时,如果存在,它会返回 DebugObject 的句柄。

  • NtQueryObject:使用 ObjectAllTypesInformation 参数,它可以用来通过名称查找 DebugObject

使用句柄

在这里,恶意软件可能会利用调试器附加与不附加时句柄管理行为的差异。例如,CloseHandle(或 NtClose)API 可以用来尝试关闭一个无效句柄。如果调试器已附加,则会触发 EXCEPTION_INVALID_HANDLE(0xC0000008)异常,从而揭示其存在。

另一个不太可靠的选项是使用 CreateFile 以独占访问模式打开恶意软件的文件。由于某些调试器会保持已分析文件的句柄打开,因此在调试器下此操作可能会失败,从而揭示它。

使用异常

调试器被设计用来拦截各种类型的异常,以便能够执行它们的所有功能。恶意软件可以故意触发某些异常,并检测调试器的存在,如果其异常处理程序(关于结构化异常处理SEH 的更多信息将在后面讨论)没有接收到控制权。此方法的示例可以涉及以下 API:

  • RaiseException / RtlRaiseException / NtRaiseException 可用来触发与调试器相关的异常,例如 DBG_CONTROL_CDBG_CONTROL_BREAKDBG_RIPEVENT

  • GenerateConsoleCtrlEvent 配合 CTRL_C_EVENTCTRL_BREAK_EVENT 参数可以用来生成 Ctrl + CCtrl + Break 事件。如果 BeingDebugged 标志被设置(当调试器附加时),系统会生成 DBG_CONTROL_C 异常(或 DBG_CONTROL_BREAK 异常),恶意软件可能会尝试拦截它。

  • SetUnhandledExceptionFilter 可以用来设置一个自定义函数来处理未处理的异常。如果调试器已附加,函数将不会执行,因为控制权会传递给调试器。

使用父进程

还有一种值得一提的技术是,进程可以通过检查父进程的名称来检测它是否是由调试器创建的。Windows 操作系统在进程信息中设置进程 ID 和父进程 ID。通过父进程 ID,你可以检查它是否是正常创建的(例如,使用 explorer.exe),或者是否是由调试器创建的(例如,通过检测名称中是否存在 dbg 子字符串)。

恶意软件获取父进程 ID 的常见技术有两种,列举如下:

  • 使用 CreateToolhelp32SnapshotProcess32FirstProcess32Next 遍历正在运行的进程列表(正如我们在 第五章 中所看到的,检查进程注入与 API 劫持,涉及进程注入)。这些 API 不仅返回进程名称和 ID,还返回更多信息,例如恶意软件正在寻找的父进程 ID。

  • 使用 NtQueryInformationProcess API。将 ProcessBasicInformationSystemProcessInformation 作为参数传递时,该 API 会返回包含父进程 ID 的结构,在 InheritedFromUniqueProcessId 字段中,如下图所示:

图 6.2 – 使用 NtQueryInformationProcess 获取父进程

图 6.2 – 使用 NtQueryInformationProcess 获取父进程

在获取到父进程 ID 后,下一步是获取进程名称或文件名,以检查它是否是常见调试器的名称,或者其名称中是否包含 dbgdebug 子字符串。有两种常见的方法可以从进程 ID 获取进程名称,如下所示:

  • 以相同的方式遍历进程以获取父进程 ID,但这次攻击者通过提供先前获取的父进程 ID 来获取进程名称。

  • 使用 GetProcessImageFileNameA API 获取给定进程句柄的文件名。为了获取有效的句柄,恶意软件将使用 OpenProcess API,并将 PROCESS_QUERY_INFORMATION 作为必需参数。

此 API 返回进程文件名,稍后可以检查该文件名,以检测它是否是调试器。

另一种常见的恶意软件检测调试过程的方法是断点检测,因此我们接下来将更详细地讨论这个话题。

处理调试器断点规避

另一种检测调试器或规避它们的方法是检测其断点。无论是软件断点(如 INT3)、硬件断点、单步断点(陷阱标志)还是内存断点,恶意软件都可以检测到这些断点,并可能移除它们以逃避逆向工程控制。

检测软件断点(INT3)

这种类型的断点是最容易使用且最容易检测到的。正如我们在第二章《汇编语言与编程基础快速教程》中所述,这种断点通过将第一个字节替换为 0xCC(INT3指令)来修改指令字节,从而触发异常(错误),并将其传递给调试器处理。

由于它会修改内存中的代码,因此扫描内存中的代码段以寻找INT3字节非常容易。一个简单的扫描过程可能像这样:

图 6.3 – 一个简单的INT3扫描

](tos-cn-i-73owjymdk6/a7b6c8c5b004487ea35021a16b41532d)

图 6.3 – 一个简单的INT3扫描

这种方法的唯一缺点是,一些 C++编译器在每个函数结束时都会写入INT3指令作为填充字节。INT3字节(0xCC)还可能出现在某些指令内部,作为地址或值的一部分,因此通过代码搜索这个字节可能并不是一个有效的解决方案,且可能会返回大量误报。

恶意软件常用的另外两种技术用于扫描INT3断点,如下所示:

  • 为整个代码段预计算校验和,并在执行模式下重新计算。如果值发生变化,那么说明有一些字节被修改过,要么是通过修补,要么是通过设置INT3断点。这是使用rol指令实现的示例:

    mov esi,<CodeStart>
    mov ecx,<CodeSize>
    xor eax,eax
    ChecksumLoop:
    movzx edx,byte [esi]
    add eax,edx
    rol eax,1
    inc esi
    loop .checksum_loop
    cmp eax, <Correct_Checksum>
    jne <breakpoint_detected>
    
  • 读取恶意软件样本文件,并将文件中的代码段与内存版本进行比较。如果它们之间有任何差异,意味着恶意软件已在内存中被修补,或代码中加入了软件断点(INT3)。这种技术不常用,因为如果恶意软件样本的重定位表已被填充,这种方法并不有效(有关更多信息,请查看第三章,《x86/x64 的基本静态和动态分析》)。

避免软件断点检测的最佳解决方案是使用硬件断点、单步执行(代码跟踪),或者在代码段的不同位置设置内存访问断点。一旦内存访问断点被触发,就可以找到校验和计算代码,并通过修补校验和代码本身来处理,如下图所示:

图 6.4 – 用于检测INT3扫描/校验和计算循环的代码段内存访问断点

](tos-cn-i-73owjymdk6/02a2af2b8d3b4451b2ece86041c0dcf4)

图 6.4 – 用于检测INT3扫描/校验和计算循环的代码段内存访问断点

在前面的截图中,我们设置了一个断点,INT3扫描循环或校验和计算循环。

通过修补校验和计算器末尾的检查或使用与之相反的jz/jnz检查,可以轻松绕过此技术。

使用陷阱标志检测单步执行断点

另一种广泛使用的断点检测技术是陷阱标志检测。当您逐条跟踪指令,检查它们在内存和寄存器值上的变化时,调试器会在 EFLAGS 寄存器中设置陷阱标志位(TF),该标志位负责在下一条指令停止并将控制权交还给调试器。

这个标志并不容易捕获,因为 EFLAGS 并不是直接可读的。它只能通过 pushf 指令读取,该指令将此寄存器的值保存到堆栈中。由于该标志在返回调试器后始终被设置为 False,因此很难检查该标志的值并检测单步断点。然而,仍然有一种方法可以做到这一点。

在 x86 架构中,有多个如今不常用的寄存器。这些寄存器在虚拟内存出现之前的 DOS 操作系统中使用,尤其是段寄存器。除了您已经了解的 FS 寄存器外,还有其他段寄存器,例如 CS,指向代码段;DS,指向数据段;以及 SS,指向堆栈段。

pop SS 指令相当特殊。该指令用于从堆栈中获取一个值,并根据该值更改堆栈段(或地址)。因此,如果在执行此指令时发生任何异常,可能会导致混乱(例如,哪一个堆栈将用于存储异常信息?)。因此,在执行此指令时不允许有任何异常或中断,包括任何断点或陷阱标志。

如果您正在跟踪这条指令,调试器会移动光标,跳过下一条指令,直接跳到后面的指令。这并不意味着跳过的指令没有执行;它已经执行了,但没有被调试器中断。

例如,在以下代码中,您的调试器光标将从 POP SS 移动到 MOV EAX, 1,跳过 PUSHFD 指令,即使该指令已经执行:

PUSH SS
POP SS
PUSHFD ; your debugger wouldn't stop on this instruction
MOV EAX, 1 ; your debugger will automatically stop on this instruction.

这里的技巧是,在前面的例子中,陷阱标志会在执行 pushfd 指令时保持设置,但它不会被允许返回到调试器。因此,pushfd 指令会将 EFLAGS 寄存器推送到堆栈中,包括陷阱标志的实际值(如果已设置,它会显示在 EFLAGS 寄存器中)。然后,恶意软件可以轻松检查陷阱标志是否被设置,并检测到调试器。下面的截图展示了一个例子:

图 6.5 – 使用 SS 寄存器进行陷阱标志检测

图 6.5 – 使用 SS 寄存器进行陷阱标志检测

值得一提的是,一些调试器,如新版的 x64dbg,已经意识到这一技巧,并且不会以这种方式暴露 TF 位。

这是检查代码跟踪或单步调试的一种直接方法。另一种检测方法是通过监控执行指令或一组指令时经过的时间,这也是我们将在下一节中讨论的内容。

使用计时技术检测单步调试

有多种方法可以精确获取系统开启到当前指令执行之间的毫秒级时间。x86 指令rdtsc可以返回 EDX:EAX 寄存器中的时间。通过计算执行某条指令前后的时间差,任何延迟都会被清晰显示出来,这代表了通过代码的逆向工程追踪。以下截图展示了一个例子:

图 6.6 – 使用 rdtsc 指令检测单步调试

图 6.6 – 使用 rdtsc 指令检测单步调试

这条指令不是获取任意时刻时间的唯一方法。Windows 提供了多个 API,帮助程序员获取准确的时间,列举如下:

  • GetLocalTime/GetSystemTime

  • GetTickCount

  • QueryPerformanceCounter

  • timeGetTime/timeGetSystemTime

这种技术使用广泛,且比 SS 段寄存器技巧更常见。最好的解决方法是修补指令。如果你已经在逐步调试指令,检测它非常容易;你可以修补代码,或者直接将指令指针(EIP/RIP)设置为指向检查之后的代码。

躲避硬件断点

硬件断点基于在用户模式下无法访问的寄存器。因此,恶意软件很难检查这些寄存器并清除它们以移除这些断点。

为了让恶意软件能够访问它们,它需要将它们压入堆栈并再从中取出。为了实现这一点,许多恶意软件家族依赖于 SEH。

什么是 SEH?

为了让任何程序能够处理异常,Windows 提供了一种叫做 SEH 的机制。它基于设置回调函数来处理异常,然后继续执行。如果该回调未能处理异常,它可以将异常传递给上一个设置的回调。如果最后一个回调也无法处理该异常,操作系统会终止进程并通知用户未处理的异常,通常还会建议用户将其发送给开发公司。

第一个回调函数的指针存储在线程环境块TEB)中,可以通过 FS:[0x00]访问。该结构是一个链表,这意味着列表中的每一项都包含回调函数的地址,并且跟随在前一项地址之后(即上一个回调)。在堆栈中,链表的结构如下:

图 6.7 – 堆栈中的 SEH 链表

图 6.7 – 堆栈中的 SEH 链表

SEH 回调的设置通常如下所示:

PUSH <callback_func> // Address of the callback function
PUSH FS:[0] // Address of the previous callback item in the list
MOV FS:[0],ESP // Install the new EXCEPTION_REGISTRATION

如你所见,SEH 链表大多数保存在堆栈中。每个项都指向前一个。当发生异常时,操作系统执行这个回调函数,并将关于异常和线程状态的必要信息传递给它(寄存器、指令指针等)。这个回调函数有能力修改寄存器、指令指针和整个线程上下文。回调函数返回后,操作系统采用修改后的线程状态和寄存器(称为上下文),并基于此恢复执行。回调函数如下所示:

_cdecl _except_handler( 
   struct _EXCEPTION_RECORD *ExceptionRecord, 
   void * EstablisherFrame, 
   struct _CONTEXT *ContextRecord, 
   void * DispatcherContext 
);

重要的参数如下:

  • ExceptionRecord:该结构包含与已生成的异常或错误相关的信息。它包含异常代码号、地址和其他信息。

  • ContextRecord:这是一个结构体,表示异常发生时该线程的状态。它是一个长结构,包含所有寄存器和其他信息。该结构的一个片段如下所示:

    struct CONTEXT { 
    DWORD ContextFlags;
    DWORD DR[7];
    FLOATING_SAVE_AREA FloatSave;
    DWORD SegGs;
    DWORD SegFs;
    DWORD SegEs;
    DWORD SegDs;
    DWORD Edi;
    ....
    };
    

有多种方法可以通过 SEH 检测调试器。其中一种方法是通过检测并移除硬件断点。

检测硬件断点

为了检测或移除硬件断点,恶意软件可以使用 SEH 获取线程上下文,检查 DR 寄存器的值,如果检测到调试器,则退出。代码如下:

xor eax, eax
push offset except_callback
push d fs:[eax]
mov fs:[eax], esp
int 3 ; force an exception to occur
...
except_callback:
mov eax, [esp+0ch] ; get ContextRecord
mov ecx, [eax+4] ; Dr0
or ecx, [eax+8]  ; Dr1
or ecx, [eax+0ch] ; Dr2
or ecx, [eax+10h] ; Dr3
jne <Debugger_Detected>

另一种检测硬件断点的方法是使用GetThreadContext API 访问当前线程(或其他线程)的上下文,并检查是否存在硬件断点,或者使用SetThreadContext API 清除它们。

处理这些技术的最佳方法是,在GetThreadContextSetThreadContext或异常回调函数上设置断点,以确保它们不会重置或检测到你的硬件断点。

内存断点

我们将讨论的最后一种断点类型是内存断点。针对它们的技术并不常见,但它们是可能的。内存断点可以通过使用ReadProcessMemory API 并将恶意软件的基址作为参数、其映像大小作为大小来轻松检测。如果恶意软件的任何页面被保护(PAGE_GUARD)或设置为无访问保护(PAGE_NOACCESS),ReadProcessMemory将返回False

对于恶意软件样本,检测写入或执行时的内存断点,它可以通过VirtualQuery API 查询任何内存页的保护标志。或者,它可以通过使用带有PAGE_EXECUTE_READWRITE参数的VirtualProtect来规避这些断点,从而覆盖它们。

处理这些反调试技巧的最佳方法是,在所有这些 API 上设置断点,并强制它们返回所需的结果给恶意软件,从而恢复正常执行。

现在,是时候讨论恶意软件如何尝试逃避调试器了。

脱离调试器

除了检测调试器并移除其断点之外,恶意软件还使用多种技巧来完全逃避整个调试环境。我们来看看一些最常见的技巧。

进程注入

我们之前在第五章中讨论过进程注入,检查进程注入与 API 挂钩。进程注入是一种非常著名的技术,不仅用于浏览器中的“中间人”攻击,还用于将调试中的进程逃离,进入一个未被调试的进程。通过将代码注入到另一个进程中,恶意软件可以逃脱调试器的控制,并在调试器附加到进程之前执行代码。

一种常用的绕过该技巧的解决方案是在注入代码的入口点添加一个无限循环指令,直到代码被执行。通常,这个指令是在注入器代码中,通常是在 WriteProcessMemory 调用之前(此时代码尚未注入),或者是在 CreateRemoteThread 之前,这时代码会注入到另一个进程的内存中。

可以通过写入两个字节(0xEB 0xFE)来创建一个无限循环,这两个字节表示一个 jmp 指令,使其跳转到自身,如下截图所示:

图 6.8 – 注入的 JMP 指令,用于创建无限循环

图 6.8 – 注入的 JMP 指令,用于创建无限循环

接下来,我们将讨论另一种流行的技术——使用 TLS 回调。继续阅读!

TLS 回调

许多逆向工程师从恶意软件的入口点开始调试,这通常是有道理的。然而,一些恶意代码可能会在入口点之前就开始执行。有些恶意软件家族使用线程局部存储TLS)来执行初始化每个线程的代码(这段代码在线程的实际代码开始之前运行)。这使得恶意软件能够逃避调试,并进行一些初步检查,甚至可能在入口点使用良性代码的同时以这种方式运行大部分恶意代码。

在 PE 头的 数据目录 块中,有一个 TLS 条目的入口。它通常存储在 .tls 区段中,其结构如下所示:

图 6.9 – TLS 结构

图 6.9 – TLS 结构

这里,AddressOfCallBacks 指向一个以零结尾的回调函数数组(最后一个元素为零),这些回调函数会在每次创建线程后依次调用。任何恶意软件都可以将其恶意代码设置为在 AddressOfCallBacks 数组内启动,并确保这些代码在入口点之前执行。

针对这个技巧的一种解决方案是在调试恶意软件之前检查 PE 头,并在 AddressOfCallBacks 字段中注册的每个回调函数上设置断点。此外,IDA 会将这些回调函数与入口点和导出函数(如果存在)一起显示。

Windows 事件回调

另一种恶意软件作者用来规避逆向工程师单步调试和断点的方法是通过设置回调函数。回调函数会在特定事件发生时被调用(例如鼠标点击、键盘敲击或窗口移到最前面)。如果你在单步调试恶意软件指令时,回调函数仍会被执行,而你不会注意到。另外,如果你根据代码流设置断点,它仍然会绕过你的断点。

设置回调函数的方式有很多。因此,我们这里只提到其中的两种,如下所示:

  • 使用RegisterClass API:RegisterClass API 用于创建一个窗口类,该类可以用于创建窗口。此 API 接受一个名为WNDCLASSA的结构作为参数。WNDCLASSA结构包含与窗口相关的所有必要信息,包括图标、光标图标、样式,以及最重要的回调函数,用于接收窗口事件。代码如下所示:

    MOV  DWORD PTR [WndCls.lpfnWndProc], <WindowCallback>
    LEA  EAX, DWORD PTR SS:[WndCls]
    PUSH EAX ; pWndClass
    CALL <JMP.&user32.RegisterClassA> ; RegisterClassA
    
  • 使用SetWindowLong:设置窗口回调的另一种方法是使用SetWindowLong。如果你有窗口句柄(来自EnumWindowsFindWindow或其他 API),你可以调用SetWindowLong API 来更改窗口回调函数。以下是代码示例:

    PUSH <WindowCallback>
    PUSH GWL_DlgProc
    PUSH hWnd ; Window Handle
    CALL SetWindowLongA
    

针对这种情况,最好的解决方案是在所有注册回调或其回调函数的 API 上设置断点。你可以检查恶意软件的导入表、任何调用GetProcAddress的函数或其他动态解析和调用 API 的函数。

攻击调试器

在某些情况下,恶意软件可能会尝试攻击调试会话。例如,BlockInput API 可以用于阻止鼠标和键盘事件,使附加的调试器无法使用。另一个类似的选项是使用SwitchDesktop来隐藏调试器的鼠标和键盘事件。

说到线程,NtSetInformationThread API 与ThreadHideFromDebugger(0x11)参数可以用于将线程隐藏起来,使调试器无法看到。任何发生在隐藏线程中的异常,包括触发的断点,都不会被调试器拦截,反而会导致程序崩溃。最后,恶意软件还可以使用SuspendThread/NtSuspendThread API 来对调试器的线程本身进行攻击。

这些是恶意软件可能尝试影响调试过程的最常见方式。接下来,让我们谈谈各种类型的混淆技术。

理解混淆技术和反反汇编器

反汇编工具是逆向工程中最常用的工具之一,因此它们是恶意软件作者的攻击目标。现在,我们将看看恶意软件在代码混淆上使用的不同技术,使其更难以被逆向工程师分析。

加密

加密是最常见的技术,它还能保护恶意软件免受静态杀毒软件签名的检测。恶意软件可以加密自己的代码,并拥有一个小的存根代码,在执行恶意代码之前解密它。此外,恶意软件还可以加密自己的数据,例如包括 API 名称的字符串或整个配置块。

处理加密并不总是容易的。一种解决方案是执行恶意软件并在解密后转储内存。例如,现在许多沙箱可以对被监控的进程进行转储,这有助于你获得解密后的恶意软件。

但是,对于像加密字符串并按需解密每个字符串这样的情况,你需要逆向加密算法,并编写脚本遍历所有解密函数的调用,利用它的参数解密字符串。你可以查看第四章解包、解密和去混淆,以了解如何处理加密并编写此类脚本。

垃圾代码

另一个在许多样本中使用并在 1990 年代末和 2000 年代初变得越来越流行的技术是垃圾代码插入。通过这种技术,恶意软件作者插入大量永远不会被执行的代码。例如,这些代码可以放在无条件跳转、永不返回的调用或条件跳转且条件永远不会满足的地方。此代码的主要目的是浪费逆向工程师分析无用代码的时间,或者让代码图看起来比实际复杂。

另一个类似的技术是插入无效代码。这些无效代码可能是像noppushpopincdec,或者是重复相同的指令。这些指令的组合看起来像真实的代码;然而,实际上相同的操作会被编码得简单得多,正如你在以下截图中看到的那样:

图 6.10 – 无意义的垃圾代码

](github.com/OpenDocCN/f…)

图 6.10 – 无意义的垃圾代码

这种垃圾代码有不同的形式,包括指令的扩展;例如,inc edx变成add edx, 3sub edx, 2,以此类推。通过这种方式,可以混淆实际的值,如 0x5a4D(MZ)或任何其他可能代表此子程序特定功能的值。

这种技术自 1990 年代起就在变形引擎中存在,但一些家族仍然使用它来混淆他们的代码。

值得提到的是,虽然存储在本地变量中的字符串分析起来更复杂,但以下不是这种技术的示例,而是一个合法编译器的行为:

图 6.11 – 存储在本地变量中的字符串

](tos-cn-i-73owjymdk6/9b958149495e483297bee4a22fe5cee8)

图 6.11 – 存储在本地变量中的字符串

现在,让我们来谈谈代码传输技术。

代码传输

恶意软件作者常用的另一个技巧是代码传输。这种技术不会插入垃圾代码,而是通过大量的无条件跳转(包括call + pop 或总为真或总为假的条件跳转)重新安排每个子程序中的代码。

这使得函数图看起来非常复杂,难以分析,从而浪费逆向工程师的时间。以下截图展示了这样的代码示例:

图 6.12 – 使用无条件跳转进行代码传输

图 6.12 – 使用无条件跳转进行代码传输

还有一种更复杂的形式,恶意软件会将每个子程序的代码重新安排到其他子程序的中间。这种形式使得反汇编工具更难连接每个子程序,因为它会错过函数末尾的ret指令,从而不将其视为一个函数。

其他一些恶意软件家族不会在子程序末尾放置ret指令,而是用popjmp来代替,隐藏该子程序不被反汇编工具识别。这些只是代码传输和垃圾代码插入技术的多种形式之一。

使用校验和的动态 API 调用

动态 API 调用是许多恶意软件家族使用的一种著名的反反汇编技巧。其背后的主要原因是,通过这种方式,它们可以将 API 名称隐藏在静态分析工具之外,使得理解恶意软件中每个函数的功能更加困难。

对于恶意软件作者来说,要实现这一技巧,他们需要预先计算该 API 名称的校验和,并将该值作为参数传递给一个扫描不同库的导出表并通过此校验和查找 API 的函数。以下截图展示了这个例子:

图 6.13 – 库和 API 名称的校验和(哈希)

图 6.13 – 库和 API 名称的校验和(哈希)

解决函数的代码实际上会通过库的 PE 头,遍历导出表,并计算每个 API 的校验和,与作为参数提供的给定校验和(或哈希值)进行比较。

这种方法的解决方案可能需要编写脚本,遍历所有已知的 API 名称并计算它们的校验和。或者,它可能需要多次执行此函数,分别输入每个校验和,并保存相应的 API 名称。

代理函数和代理参数堆叠

Nymaim 银行木马通过增加一些额外的技巧,如代理函数和代理参数堆叠,将反反汇编技巧提升到了另一个层次。

使用代理函数技术,恶意软件不会直接调用所需的函数;相反,它调用一个代理函数,该函数计算所需函数的地址并将执行转移到那里。Nymaim 包含了超过 100 个不同的代理函数,使用了四到五种不同的算法。代理函数调用如下所示:

图 6.14 – 用于计算函数地址的代理函数参数

图 6.14 – 用于计算函数地址的代理函数参数

代理函数的代码如下所示:

图 6.15 – Nymaim 代理函数

图 6.15 – Nymaim 代理函数

对于参数,Nymaim 使用一个函数将参数推送到堆栈,而不是仅仅使用 push 指令。这一技巧可以防止反汇编工具识别传递给每个函数或 API 的参数。代理参数堆叠的示例如下:

图 6.16 – Nymaim 中的代理参数堆叠技术

图 6.16 – Nymaim 中的代理参数堆叠技术

该恶意软件包括了我们在本节中介绍的多种不同形式的技术。因此,只要掌握了主要思路,你应该能够理解所有这些技术。

使用 COM 功能

恶意软件可能尝试通过不同的技术实现与动态解析哈希值来隐藏 API 名称相同的效果。一个好的例子是使用 Wscript.Shell COM 对象的功能来执行程序,而不是直接调用 CreateProcessShellExecuteWinExec 等 API,这些 API 会立刻引起研究人员的注意。为了创建该对象,恶意软件可以使用 CoCreateInstance API,并指定所需对象的类形式为 IID,如下截图所示:

图 6.17 – 通过其 IID 创建 Wscript.Shell 对象的实例,F935DC21-1CF0-11d0-ADB9-00C04FD58A0B

图 6.17 – 通过其 IID 创建 Wscript.Shell 对象的实例,F935DC21-1CF0-11d0-ADB9-00C04FD58A0B

之后,实际方法将通过其偏移量进行访问。为了通过偏移量查找方法的名称,你可以使用 COMView 工具:

图 6.18 – 通过在汇编中找到的偏移量查找 COM 对象方法的名称

图 6.18 – 通过在汇编中找到的偏移量查找 COM 对象方法的名称

如你所见,Wscript.Shell 类的 Run 方法通过其偏移量 36 (0x24) 来访问。

如我们所见,混淆可以有多种形式,所以你了解的示例越多,找到处理它的正确方法所需的时间就越短。现在,是时候学习如何使用恶意软件检测行为分析工具了。

检测和规避行为分析工具

恶意软件可以通过多种方式检测并规避行为分析工具,如 ProcMon、Wireshark、API Monitor 等,即使这些工具并没有直接调试恶意软件或与其交互。在本节中,我们将讨论两种常见的恶意软件规避方法。

查找工具进程

恶意软件处理恶意软件分析工具(以及杀毒工具)的一种最简单且最常见的方法是循环检查所有正在运行的进程,并检测任何不需要的条目。然后,它可以终止或停止它们,以避免进一步分析。

第五章检查进程注入和 API Hook 中,我们讲解了恶意软件如何使用 CreateToolhelp32SnapshotProcess32FirstProcess32Next APIs 循环检查所有正在运行的进程。在这个反反向工程技巧中,恶意软件以完全相同的方式使用这些 API 来检查进程名是否与不需要的进程名或其哈希值匹配。如果匹配,恶意软件会终止自身,或者通过调用 TerminateProcess API 来杀死该进程。以下截图展示了这个技巧在 Gozi 恶意软件中的实现:

图 6.19 – Gozi 恶意软件循环检查所有正在运行的进程

图 6.19 – Gozi 恶意软件循环检查所有正在运行的进程

以下截图展示了 Gozi 恶意软件代码的一个示例,使用 TerminateProcess API 通过自定义的 ProcOpenProcessByNameW 程序终止其选择的进程:

图 6.20 – Gozi 恶意软件借助 ProcOpenProcessByNameW 函数终止进程

图 6.20 – Gozi 恶意软件借助 ProcOpenProcessByNameW 函数终止进程

这个技巧可以通过在执行工具之前重命名它们来绕过。如果你避免在新名称中使用任何已知的关键词,如dbg反汇编器AV等,这个简单的解决方案可以完美地隐藏你的工具。

查找工具窗口

另一种技巧是,不搜索工具的进程名,而是搜索其窗口名称(窗口标题)。通过搜索程序窗口名称,恶意软件可以绕过任何可能对进程名进行的重命名,这也为它提供了检测新工具的机会(大多数情况下,窗口名称比进程名称更具描述性)。

这个技巧可以通过以下两种方式实现:

  • 使用FindWindow:恶意软件可以使用完整的窗口标题,如Microsoft network monitor,或者窗口类名。窗口类名是在窗口创建时分配给它的名称,它与窗口上显示的标题不同。例如,OllyDbg的窗口类名是OLLYDBG,而完整标题可能会根据正在分析的恶意软件进程名称而变化。以下是一个示例:

    push NULL
    push .szWindowClassOllyDbg
    call FindWindowA
    test eax,eax
    jnz <debugger_found>
    push NULL
    push .szWindowClassWinDbg
    call FindWindowA
    test eax,eax
    jnz <debugger_found>
    .szWindowClassOllyDbg db "OLLYDBG",0
    .szWindowClassWinDbg db "WinDbgFrameClass",0
    
  • 使用EnumWindows:另一种避免搜索窗口类名或处理窗口标题变化的方法是遍历所有可访问的窗口名称并扫描它们的标题,寻找诸如DebuggerWiresharkDisassembler等可疑的窗口名称。这是一种更灵活的处理新工具或恶意软件作者遗忘覆盖的工具的方法。使用EnumWindows API 时,你需要设置一个回调函数来接收所有窗口。

对于每个顶级窗口,这个回调函数将接收到该窗口的句柄,从中可以使用GetWindowText API 获取其名称。以下是一个示例:

图 6.21 – FinFisher 威胁利用 EnumWindows 设置其回调函数

图 6.21 – FinFisher 威胁利用 EnumWindows 设置其回调函数

回调函数的声明如下所示:

BOOL CALLBACK EnumWindowsProc(
_In_ HWND hwnd,
_In_ LPARAM lParam);

hwnd值是窗口的句柄,而lParam是用户定义的参数(由用户传递给回调函数)。恶意软件可以使用这个句柄(hwnd)与GetWindowText API 获取窗口标题,并与预定义的关键词列表进行比对。

修改窗口标题或类比实际上在这些 API 上设置断点并跟踪回调函数要复杂。流行工具如 OllyDbg 和 IDA 的插件可以帮助重命名它们的标题窗口,以避免被检测(如 OllyAdvanced),你也可以使用它作为一种解决方案。

现在我们了解了行为分析工具如何被检测到,接下来让我们了解沙盒和虚拟机检测。

检测沙盒和虚拟机

恶意软件作者知道,如果他们的恶意软件样本正在虚拟机上运行,那么它很可能正在被逆向工程师分析,或者它可能正在沙盒等自动化工具的分析下运行。有多种方法可以检测虚拟机和沙盒,接下来我们将逐一介绍。

虚拟机和真实机器之间的不同输出

恶意软件作者可以利用一些汇编指令在虚拟机上执行时的独特特征来检测虚拟机。一些例子如下:

  • CPUID指令返回有关 CPU 的信息,并提供该信息在eax中的叶/ID。对于叶 0x01(eax = 1),CPUID指令将第 31 位设置为 1,表示操作系统正在虚拟机或虚拟化程序中运行。

  • CPUID 指令,给定 eax = 0x40000000,它可以返回虚拟化工具的名称(如果存在),并将其作为单个字符串存储在 EBX、ECX 和 EDX 寄存器中。这些名称字符串的示例包括 VMwareVMwareMicrosoft HvVBoxVBoxVBoxXenVMMXenVMM

  • MMX 寄存器:MMX 寄存器是英特尔推出的一组寄存器,旨在加速图形计算。虽然很少见,但有些虚拟化工具不支持它们。一些恶意软件或打包工具利用它们进行解包,以检测或避免在虚拟机上运行。

  • IN 指令,当在 VMware 虚拟机上执行并将端口参数设置为 0x5658(在 ASCII 中表示 VX,即 VMware 超级监视器端口)时,且 EAX 值等于 0x564D5868(VMXh 魔术值),将返回 EBX 寄存器中的相同魔术值 VMXh,从而揭示虚拟机的存在。

检测虚拟化进程和服务

虚拟化软件通常会在客户机上安装一些工具,以启用剪贴板同步、拖放、鼠标同步和其他有用的功能。这些工具可以通过扫描这些进程,使用 CreateToolhelp32SnapshotProcess32FirstProcess32Next API 来轻松检测到。以下是一些此类进程:

  • VMware

    • vmacthlp.exe

    • VMwareUser.exe

    • VMwareService.exe

    • VMwareTray.exe

  • VirtualBox

    • VBoxService.exe

    • VBoxTray.exe

可以使用相同的方法搜索文件系统中的特定文件或目录。

通过注册表键检测虚拟化

有多个注册表键可以用来检测虚拟化环境。它们中的一些与硬盘名称(通常以虚拟化软件命名)、已安装的虚拟化同步工具或虚拟化过程中的其他设置相关。以下是一些注册表条目:

HKEY_LOCAL_MACHINE\SOFTWARE\Vmware Inc.\Vmware Tools
HKEY_LOCAL_MACHINE\SOFTWARE\Oracle\VirtualBox Guest Additions
HKEY_LOCAL_MACHINE\HARDWARE\ACPI\DSDT\VBOX

使用 WMI 检测虚拟机

不仅是注册表值能揭示有关虚拟化软件的很多信息——Windows 管理的信息也能揭示这些信息,例如通过 PowerShell 访问的内容,如下图所示:

图 6.22 – 检测 VMWare 的 PowerShell 命令

图 6.22 – 检测 VMWare 的 PowerShell 命令

可以通过 WMI 查询访问此信息,例如以下内容:

SELECT * FROM Win32_ComputerSystem WHERE Manufacturer LIKE "%VMware%" AND Model LIKE "%VMware Virtual Platform%"

对于 Microsoft Hyper-V,命令如下:

SELECT * FROM Win32_ComputerSystem WHERE Manufacturer LIKE "%Microsoft Corporation%" AND Model LIKE "%Virtual Machine%"

这些技术使得隐藏恶意软件运行在虚拟化软件中而非真实机器上的事实变得更加困难。

其他虚拟机检测技术

恶意软件家族可以使用许多其他技术来检测虚拟化环境,例如以下方法:

  • 命名管道和设备,例如 \.\pipe\VBoxTrayIPC

  • 窗口标题或类名,例如 VBoxTrayToolWndClassVBoxTrayToolWnd

  • 网络适配器的 MAC 地址的第一部分:

    • 00:1C:14, 00:50:56, 00:05:69, 00:0C:29 – VMWare

    • 08:00:27 – VirtualBox

    • 00:03:FF – Hyper-V

上述列表可以通过许多类似的技巧和方法进一步扩展,用以检测虚拟化环境。

使用默认设置检测沙箱

沙箱也很容易被检测到。它们有许多默认设置,恶意软件作者可以用来识别它们:

  • 用户名可能是默认值,例如cuckoouser

  • 文件系统可以包含相同的诱饵文件和相同的文件结构(如果没有,则是相同数量的文件)。即使是样本本身的名称也可以始终相同,例如sample.exe

这些设置可以很容易地被常用沙箱检测到,甚至不需要查看它们已知的工具和进程。

除此之外,沙箱通常通过以下特点被检测到:

  • 系统硬件过于薄弱(主要是磁盘空间和内存)

  • 不寻常的系统设置(非常低的屏幕分辨率或没有安装软件)

  • 没有用户交互(缺乏鼠标移动或近期的文件修改)

另一种常见的逃避沙箱的方法是避免在它们的分析时间窗口内执行恶意活动。在许多情况下,沙箱只执行恶意软件几秒钟或几分钟,然后收集必要的信息后终止虚拟机。一些恶意软件家族使用如Sleep这样的 API,或者执行长时间的计算来延迟执行,或者在机器重启后再执行。这种技巧可以帮助恶意软件逃避沙箱,确保它们不会收集重要信息,比如 C&C 域名或恶意软件持久化技术。

这些是一些最常见的沙箱检测技巧。值得一提的是,恶意软件开发者不断发明更多新颖的方法来实现这一目标,因此,跟上它们的步伐需要持续学习和实践。

总结

在本章中,我们介绍了恶意软件作者用来检测和逃避逆向工程的许多技巧,从检测调试器及其断点,到检测虚拟机和沙箱,再到融合混淆和逃避调试器技术。现在你应该能够分析更先进的恶意软件,这些恶意软件配备了多个反调试或反虚拟机的技巧。此外,你还将能够分析实现大量反反汇编技巧的高度混淆恶意软件。

第七章《理解内核模式 Rootkit》中,我们将进入操作系统的核心。我们将覆盖内核模式,学习每个 API 调用和操作如何在 Windows 操作系统内部工作,以及 Rootkit 如何挂钩这些步骤,以隐藏恶意活动,从而避开杀毒软件和用户的眼睛。

第七章:理解内核模式下的 Rootkit

本章我们将深入探讨 Windows 内核及其内部结构和机制。我们将介绍恶意软件作者用来隐藏恶意软件存在的不同技术,避免被用户和杀毒软件发现。

我们将研究不同的高级内核模式挂钩技术、内核模式中的进程注入,以及如何在其中进行静态和动态分析。

在深入了解 Rootkit 和它们是如何实现之前,我们需要理解 操作系统OS)是如何工作的,以及 Rootkit 如何针对操作系统的不同部分并加以利用。

本章将涵盖以下主题:

  • 内核模式与用户模式

  • Windows 内部结构

  • Rootkit 和设备驱动程序

  • 挂钩机制

  • DKOM

  • 内核模式中的进程注入

  • x64 系统中的 KPP(PatchGuard)

  • 内核模式中的静态和动态分析

内核模式与用户模式

你已经看到计算机上运行的多个用户模式进程(所有你看到的应用程序都在用户模式下运行),并学习了如何修改文件、连接互联网并执行大量操作。然而,你可能会惊讶地发现,用户模式应用程序并没有执行所有这些操作的权限。

为了让任何进程创建文件或连接到域,它需要向内核模式发送请求来执行该操作。这个请求是通过所谓的系统调用来完成的,系统调用会切换到内核模式以执行该操作(如果权限允许)。内核模式和用户模式不仅得到操作系统的支持,还得到处理器通过保护环(或硬件限制)的支持。

保护环

x86 处理器提供四个特权环(x64 稍有不同)。每个环的特权比前一个低,如下图所示:

图 7.1 – 处理器环

图 7.1 – 处理器环

Windows 主要使用这两个环:RING 0 用于内核模式,RING 3 用于用户模式。现代处理器如 Intel 和 AMD 还有另一个环(RING 1)用于虚拟机监控程序(Hypervisor)和虚拟化,以便每个操作系统可以原生运行,同时由虚拟机监控程序控制某些操作,如硬盘访问。

这些保护环用于处理故障(如内存访问故障或任何类型的异常)以及安全性。RING 3 具有最少的权限——也就是说,处于此环中的进程无法影响系统,无法访问其他进程的内存,也无法访问物理内存(它们必须在虚拟内存中运行)。相比之下,RING 0 可以做任何事——它可以直接影响系统及其资源。因此,只有 Windows 内核和设备驱动程序可以访问该环。

Windows 内部结构

在我们深入探讨 rootkit 的恶意活动之前,先来了解一下 Windows 操作系统的工作原理以及用户模式与内核模式之间的交互是如何组织的。这些知识将帮助我们理解内核模式恶意软件的具体情况,以及它可能针对系统的哪些部分。

Windows 解剖图

正如我们之前提到的,操作系统分为两部分:用户模式和内核模式。以下图示展示了这一点:

图 7.2 – Windows 操作系统设计

图 7.2 – Windows 操作系统设计

现在,让我们了解一下这些应用程序的作用范围:

  • kernel32.dll 在 Win32 和 Win64 子系统中的作用。

这些 ntdll.dll,它直接与内核模式通信。Ntdll.dll 是一个库,使用特殊指令(如 sysentersyscall,具体取决于模式和是 Intel 还是 AMD 处理器;在本章中我们将交替使用它们)向内核发送请求。请求 ID 是通过 eax 寄存器传递的:

图 7.3 – 系统调用指令

图 7.3 – 系统调用指令

  • 内核模式:管理所有资源,包括内存、文件、用户界面、声音、图形等。它还负责调度线程和进程,并管理所有应用程序的 UI。内核模式与直接发送命令或接收硬件输入的设备驱动程序进行通信。它管理所有这些请求以及在操作前后的任何工作。

这是对 Windows 操作系统工作原理的简要解释。现在,我们将深入探讨从用户模式到内核模式的请求生命周期,以便更好地理解这一切是如何协同工作的。此外,我们还将探讨 rootkit 如何干扰系统并执行恶意活动。

从用户模式到内核模式的执行路径

首先,来看一下需要内核模式功能的一个 API 调用的生命周期(在这个例子中,我们将使用 FindFirstFileA)。我们将详细拆解每一个步骤,以便理解系统中每个部分在处理进程请求时所扮演的角色。这是我们理解恶意软件如何介入这一系列操作的一个重要前提:

图 7.4 – API 调用生命周期

图 7.4 – API 调用生命周期

让我们逐步解析前面的图示,如下所示:

  1. 首先,进程调用了 FindFirstFileA API,该 API 实现于 kernel32.dll 库中。

  2. 然后,kernel32.dll(与所有子系统 DLL 相同)调用 ntdll.dll 库中的一个函数。在这个例子中,它调用了一个名为 ZwQueryDirectoryFile(或 ZwQueryDirectoryFileEx)的 API。

  3. 所有 Zw* API 都执行 syscall 指令,正如你在 图 7.3 中看到的那样。ZwQueryDirectoryFile 通过将命令 ID 以 eax 形式提供来执行 syscall(这里,命令 ID 会随着 Windows 版本的不同而变化)。

  4. 现在,应用程序进入内核模式,执行被重定向到一个名为系统服务调度器的内核模式函数。它在 32 位机器上以KiSystemService(或直接为KiFastCallEntry)的名称提供,在 64 位机器上则是KiSystemCall64;兼容模式下将使用KiSystemCall32名称。系统还可能使用带有Shadow后缀的它们的影像版本(例如,KiSystemServiceShadowKiSystemCall64Shadow)。

  5. 系统服务调度器搜索表示eax形式的命令 ID(在本例中为 0x91)在NtQueryDirectoryFile中的函数。它调用这个函数,并传递所有传递给FindFirstFileA的参数:

图 7.5 – SSDT 解释

图 7.5 – SSDT 解释

  1. 接下来,执行NtQueryDirectoryFile,此函数会发送一个请求,称为fastfat.sysntfs.sys驱动程序(这取决于已安装的文件系统)。更多关于 IRP 的细节将在稍后提供。

  2. 这个请求经过多个附加到文件系统驱动程序的设备驱动程序。这些设备驱动程序可以修改任何请求中的输入以及文件系统返回的输出(或响应)。

  3. 最后,文件系统驱动程序处理请求。IRP 请求通过一个名为sysret(或sysexit)的指令返回到NtQueryDirectoryFile。然后,控制权返回到用户模式进程,并带回结果。

这听起来可能相对复杂,但目前为止,这些就是你需要知道的内容,以便理解内核模式 rootkit 是如何工作的,更重要的是,rootkit 如何利用这个过程中存在的弱点来实现它们的目标。

Rootkit 和设备驱动程序

现在你已经理解了 Windows 内部结构以及用户模式和内核模式交互的工作原理,让我们深入探讨 rootkit。在这一部分,我们将了解 rootkit 是什么以及它们是如何设计的。在我们掌握了 rootkit 的基本概念后,我们将讨论设备驱动程序。

什么是 rootkit?

Rootkit 本质上是提供隐匿功能的低级工具。它们的主要目的是通过隐藏相关的伪迹来使恶意模块更难被检测和修复,从而使目标机器的恶意软件检测和修复过程复杂化。实现这一目标有多种方法,接下来我们将详细讨论这些方法。

Rootkit 的类型

在用户模式、内核模式甚至启动模式中都有各种类型的 rootkit:

  • 用户模式或应用程序 rootkit:我们在第五章中介绍了用户模式 rootkit,检查进程注入和 API 钩取;它们将恶意代码注入到其他进程中,并挂钩其 API,以隐藏恶意软件文件、注册表键和其他妥协指标IoC)不被这些进程发现。它们可以用来绕过防病毒程序、任务管理器等。

  • 内核模式根套件:本章将主要介绍这些根套件。它们是设备驱动程序,钩住内核模式中的不同功能,以隐藏恶意软件的存在并赋予恶意软件内核模式的权限。它们还可以向其他进程注入代码和数据,终止 AV 进程,拦截网络流量,执行中间人攻击MITM)等。

  • 引导根套件:引导根套件是修改引导加载程序的根套件。它们用于在操作系统启动之前加载恶意文件。这使得恶意软件可以在操作系统及其安全机制启动之前完全控制计算机。

  • 固件根套件:这一类威胁针对固件(如统一可扩展固件接口UEFI)或基本输入输出系统BIOS))进行攻击,以实现尽早的执行。

  • 虚拟机监控程序或虚拟根套件:在撰写本文时,这些威胁大多以概念验证PoCs)的形式存在。它们应该位于环 1(虚拟机监控程序)中。

在本章中,我们将重点关注内核模式根套件及其如何钩住多个功能或修改内核对象来隐藏恶意软件。在了解它们的钩子机制之前,首先,让我们理解什么是设备驱动程序。

什么是设备驱动程序?

设备驱动程序是内核模式工具,用于与硬件交互。每个硬件制造商都会创建一个设备驱动程序来与他们自己的硬件通信,并将 IRP 转换为硬件设备能够理解的请求。

操作系统的主要目的之一是标准化与任何类型设备的通信渠道,无论设备供应商如何。例如,如果你将有线鼠标换成了来自不同厂商的无线鼠标,它不应影响与鼠标交互的应用程序。同样,如果你是开发者,你也不必担心用户使用的是什么类型的键盘或打印机。

设备驱动程序使得可以理解 I/O 请求,并以标准化格式返回输出,无论设备的工作方式如何。

还有其他一些驱动程序与实际设备无关,例如防病毒模块,以及在我们的案例中,根套件。内核模式根套件是设备驱动程序,利用内核模式提供的功能来支持实际的恶意软件,确保其隐蔽性和持久性。

现在,让我们看看根套件如何实现它们的目标,以及它们如何利用从用户模式到内核模式的执行路径中的弱点。

钩子机制

在本节中,我们将探讨不同类型的钩子机制。在下面的图示中,我们可以看到根套件在请求处理流程的不同阶段使用的各种钩子技术:

图 7.6 – 根套件的钩子机制

图 7.6 – 根套件的钩子机制

根套件可以在这个过程流的不同阶段安装钩子:

  • 用户模式挂钩/API 挂钩:这些是用于隐藏恶意软件进程、文件、注册表项等的用户模式 API 挂钩机制。我们在第五章中讨论过,检查进程注入和 API 挂钩

  • sysenter将把执行转移到内核模式,并拦截从用户模式到内核模式的所有请求。

  • SSDT 挂钩:该技术与 rootkit 希望挂钩的函数更紧密地合作。这种挂钩类型修改 SSDT,使其将请求重定向到恶意函数,而不是实际处理请求的函数(类似于 IAT 挂钩)。

  • 代码修补:与其修改 SSDT,这种 rootkit 会修补处理请求的函数,使命令一开始就调用恶意函数(类似于 API 挂钩)。

  • 分层驱动程序/IRP 挂钩:这是一种合法的挂钩技术,用于拦截请求并修改输入输出。它更难以检测,因为它是微软官方支持的。

我们还将探索 rootkit 使用的其他技术,例如EPROCESSETHREAD,这些我们在第三章中提到过,x86/x64 的基本静态和动态分析。除此之外,sysenter成为了执行这一操作的首选方法。

现在,让我们更详细地了解这些技术。

挂钩 SYSENTER 入口函数

当用户模式应用程序执行sysenter(在 Windows 2000 及更早版本中为int 0x2e)时,处理器会将执行切换到内核模式,特别是切换到存储在模型特定寄存器MSR)中的特定地址。MSR 是用于调试、监控、切换或禁用各种 CPU 功能的控制寄存器。

在使用sysenter进行用户模式到内核模式的切换过程中,有几个重要的寄存器:

  • sysenter;在这里,SS 段寄存器将是一个值为+8 的 CS 值。

  • sysenter被执行时,它将是参数被复制到的地方。

  • sysenter。它指向系统服务调度器。

  • KiSystemCall64).* KiSystemCall32).

这些寄存器可以分别通过rdmsrwrmsr汇编指令进行读取和修改。rdmsr指令会将寄存器 ID 放入ecx/rcx寄存器中,并将结果返回到edx:eax(在 x64 中为rdx:rax寄存器;这两个寄存器的高 32 位未使用)。以下是一个示例:

mov ecx, 0x176 ; IA32_SYSENTER_EIP
rdmsr ; read msr register
mov <eip_low>, eax
mov <eip_high>, edx

wrmsrrdmsr非常相似。wrmsr将寄存器 ID 放入ecx中,并将要写入的值放入edx:eax寄存器对中。以下是挂钩代码:

mov ecx, 0x176 ; IA32_SYSENTER_EIP
xor edx, edx
mov eax, <malicious_hooking_function>
wrmsr ; write this value to IA32_SYSENTER_EIP

这种技术有多个缺点,具体如下:

  • 对于有多个处理器的环境,仅挂钩一个处理器。这意味着攻击者必须创建多个线程,希望它们能在所有处理器上运行,从而使得挂钩所有处理器成为可能。

  • 攻击者需要从用户模式堆栈中获取参数并解析它们。

  • 以这种方式,所有函数都被挂钩,因此有必要实现一些过滤,以便只检查应挂钩的函数。

这是恶意软件可以在内核模式下挂钩的第一个地方。接下来我们来看第二个地方,就是修改 SSDT 时。

在 x86 环境中修改 SSDT

首先,SSDT 表与 ntoskrnl.exe 中的第一个元素不同,并且由其指向,名称为 KeServiceDescriptorTable。该表有四个不同的 SDT 条目的插槽,但在写作时,Windows 只使用了其中两个:KeServiceDescriptorTableKeServiceDescriptorTableShadow

当用户模式应用程序使用 sysenter 时,正如你在 图 7.3 中看到的,应用程序会将函数编号或 ID 提供到 eax 寄存器中。在 eax 中,这个值以如下方式分割:

图 7.7 – sysenter eax 参数值

图 7.7 – sysenter eax 参数值

这些值如下所示:

  • bits 0-11:这是 系统服务编号 (SSN),它是该函数在 SSDT 中的索引。

  • bits 12-13:这是 SDT,表示 SDT 编号(这里,KeServiceDescriptorTable 是 0x00,KeServiceDescriptorTableShadow 是 0x01)

  • bits 14-31:此值未使用,填充为零

SDT 存储一个 SYSTEM_SERVICE_TABLE 条目的数组,现代操作系统主要使用第一个元素。它包含以下字段:

  • KiServiceTable:这是一个 SSDT 表,表示每个可以通过 eaxsysenter 之前传递的 SSN 的函数地址数组。

  • CounterBaseTable:在 Windows 的免费(零售)版本中未使用。

  • nSystemCalls:这是 KiServiceTableKiArgumentTable 表中项的数量。

  • KiArgumentTable:这是一个数组,其排序方式与 KiServiceTable 相同。这里,每个项包含应为每个函数的参数分配的字节数。

为了让恶意软件挂钩这个表,它需要获取由 ntoskrnl.exe 导出的 KeServiceDescriptorTable,然后移动到 KiServiceTable 并修改它想要挂钩的函数。为了能够修改这个表,必须禁用写保护(因为这是一个只读表)。有多种方法可以实现这一点,最常见的方法是通过修改 CR0 寄存器值并将写保护位设置为零:

PUSH EBX
MOV EBX, CR0
OR EBX, 0x00010000
MOV CR0,EBX
POP EBX

完整的挂钩机制如下所示:

图 7.8 – 来自 winSRDF 项目的 SSDT 挂钩代码

图 7.8 – 来自 winSRDF 项目的 SSDT 挂钩代码

如您所见,应用程序能够获取KeServiceDescriptorTable的地址,该地址在ntoskrnl.exe中以该名称导出。然后,它获取KiServiceTable数组,禁用写保护,最后使用InterlockedExchange在没有其他线程使用时修改表格(InterlockedExhange可以防止应用程序在另一个线程读取时进行写操作)。

在 x64 环境中修改 SSDT

对于 x64 环境,Windows 实现了更多的保护措施来阻止对 SSDT 的修改。最初,SSDT hooking 被恶意软件和反恶意软件产品共同使用,也被沙盒和其他行为型病毒防护工具使用。然而,在 64 位版本中,微软决定完全停止这种做法,并开始提供合法应用程序和其他替代方案,而不是 SSDT hooking。

微软实施了多种形式的保护措施来阻止 SSDT hooking,如通过ntoskrnl.exe中的KeServiceDescriptorTable

由于KeServiceDescriptorTable没有导出,恶意软件家族开始寻找使用该表的函数,以便访问地址。他们使用的其中一个函数是KiSystemServiceRepeat

该功能包含以下代码:

lea r10, <KeServiceDescriptorTable>
lea r11, <KeServiceDescriptorTableShadow>
test DWORD PTR [rbx + lOOh] , 80h

如您所见,该函数使用了两个 SSDT 条目的地址。然而,找到这个函数及其内部代码并不容易。由于该函数接近KiSystemCall64(x64 环境中的sysenter入口函数),恶意软件通常使用IA32_SYSENTER_EIP MSR 寄存器获取KiSystemCall64的地址。通过这样做,它可以从该地址开始搜索,直到找到前面的代码。通常,恶意软件通过搜索特定的操作码来找到这个函数,如下图所示:

图 7.9 – zer0m0n 项目在 x64 环境下的 SSDT hooking

图 7.9 – zer0m0n 项目在 x64 环境下的 SSDT hooking

该机制并不完全可靠,且很容易在以后的 Windows 版本中被打破;然而,它是寻找 x64 环境中 SSDT 地址的最著名机制之一。

打补丁 SSDT 函数

在 SSDT hooking 中,最后一个值得提及的技巧是钩取 SSDT 中引用的函数。这与 API hooking 非常相似。在这种情况下,恶意软件通过函数 ID 从 SSDT 中获取函数,并用jmp <malicious_func>修改前几个字节。然后,在检查调用该函数的进程及其参数后,它将执行返回到原始函数。

采用这一技术是因为 SSDT hooks 很容易被杀毒软件或 rootkit 扫描程序检测到。通过遍历 SSDT 中的所有函数并搜索在合法驱动程序或应用程序内存映像之外的函数,能够轻松地发现钩子。

这就是 SSDT hooking 的全部内容;现在,让我们来看看分层驱动程序,也就是 IRP hooking。

IRP hooking

IRP 是代表设备输入(请求)和输出(响应)的主要对象。在许多情况下,请求包会通过一链条驱动程序进行处理,直到该消息能够被最终设备或用户模式应用程序理解(取决于请求的方向):

图 7.10 – IRP 结构,来自官方文档

图 7.10 – IRP 结构,来自官方文档

例如,假设你想播放一个音乐文件(例如 MP3 文件)。一旦文件被理解 MP3 格式的应用程序打开,它将被转换为内核模式驱动程序可以理解的格式。接着,这个驱动程序会简化该格式并传递给下一个驱动程序,直到它到达实际的扬声器,并以编码的波形组的形式输出。另一个例子是来自键盘的电信号,它被简化为通过 ID(例如 r 键)点击一个按钮。然后,它被传递给键盘驱动程序,驱动程序理解这是字母 r 并将其传递给下一个驱动程序。这一过程一直持续,直到它到达文本编辑器,比如记事本,来写下字母 r

那么,这一切与 rootkit 有什么关系呢?其实,存在于处理 IRP 请求包的驱动链中的 rootkit 可以改变输入或输出,从而操控结果。例如,当研究人员或杀毒软件寻找恶意文件时,驱动程序可以让它变得不可见。这是 Windows 允许开发人员唯一合法的方式,通过它可以挂钩任何来自用户模式的请求并修改其输入和输出。

现在,让我们来看看它在汇编语言中的表现。

设备和主要功能

为了让任何驱动程序能够接收和处理 IRP 请求,必须创建一个设备对象。该设备可以附加到处理特定类型 IRP 请求的设备驱动程序链上。例如,如果攻击者想要挂钩文件系统请求,他们需要创建一个设备并将其附加到文件系统设备链上。之后,便可以开始接收与该文件系统相关的 IRP 请求(例如打开文件或查询目录)。

创建设备对象很简单:驱动程序可以直接调用 IoCreateDevice API 并提供与其要附加的设备相对应的标志。对于恶意软件分析,这些标志可以帮助你理解该设备的目的,例如 FILE_DEVICE_DISK_FILE_SYSTEM 标志。

驱动程序还需要设置所有调度函数(遵循 DRIVER_DISPATCH 结构),这些函数将接收并处理这些请求。每个 IRP 请求都有一个 IRP_MJ_XXX 格式的主要功能代码。此代码帮助我们理解此 IRP 请求的内容,例如 IRP_MJ_CREATE(可用于创建文件或打开文件)或 IRP_MJ_DIRECTORY_CONTROL(可用于查询目录)。初始化是通过将调度函数的指针放置到 DriverObjectMajorFunction 数组中的正确位置来完成的(遵循 _DRIVER_OBJECT 结构),其中 IRP_MJ_XXX 代码充当索引。以下是实现此设置的代码示例:

图 7.11 – 设置主要功能

图 7.11 – 设置主要功能

在这些功能中,驱动程序可以从所谓的 IRP 堆栈中获取此请求的参数。IRP 堆栈包含与此请求相关的所有必要信息,驱动程序可以在处理过程中添加、修改或删除它们。为了获取指向此堆栈的指针,驱动程序调用 IoGetCurrentIrpStackLocation API,并提供感兴趣的 IRP 地址。以下是一个示例,展示了一个过滤名称为 _root_ 文件的主要功能:

图 7.12 – 一个主要功能创建一个过滤器来处理具有“root”名称的文件

图 7.12 – 一个主要功能创建一个过滤器来处理具有“root”名称的文件

在 rootkit 创建了其设备并设置了主要功能后,它可以通过将自己附加到接收 rootkit 感兴趣的请求的设备上来拦截相应的请求。

从用户模式侧,软件也可以利用 DeviceIoControl API 向驱动程序发送自定义请求。调用此函数会创建一个 IRP_MJ_DEVICE_CONTROL 请求。某些 IOCTL 是公开的,它们是系统定义的,并由 Microsoft 文档化,而一些则是私有的,特定于某个软件,包括恶意软件。还值得一提的是,上级驱动程序可以通过 IRP_MJ_DEVICE_CONTROLIRP_MJ_INTERNAL_DEVICE_CONTROL 请求将 IOCTL 代码发送给下级驱动程序。驱动程序会像处理其他 IRP 一样处理它们,通过在驱动对象中注册专门的 DRIVER_DISPATCH 回调函数。

附加到设备

为了让 rootkit 附加到一个命名设备(例如,\\FileSystem\\fastfat,以接收文件系统请求),它需要获取该命名设备的设备对象。有多种方法可以实现这一点,其中一种方法是使用未记录的 ObReferenceObjectByName API。一旦找到设备对象,rootkit 就可以使用 IoAttachDeviceToDeviceStack API 将其附加到设备驱动链中,从而接收发送给它的 IRP 请求。代码可能如下所示:

图 7.13 – 附加到 FastFat 文件系统

图 7.13 – 附加到 FastFat 文件系统

执行 IoAttachDeviceToDeviceStack API 后,驱动程序将被添加到链条的顶部,这意味着 rootkit 驱动程序将是第一个接收到 IRP 请求的驱动程序。然后,它可以通过 IoCallDriver API 将请求传递给下一个驱动程序。此外,在设置完成例程后,rootkit 会是最后一个修改 IRP 请求响应的驱动程序。

修改 IRP 响应并设置完成例程

完成例程涵盖了请求被最后一个驱动程序处理后仍需要进一步处理的情况。对于 rootkit 来说,完成例程允许它修改请求的输出;例如,从特定目录中的文件列表中删除文件名。设置完成例程时,它需要将请求参数复制到链条中较低的驱动程序。为了将这些参数复制到下一个驱动程序的堆栈,rootkit 可以使用 IoCopyCurrentIrpStackLocationToNext API。

一旦所有参数都被复制到下一个驱动程序,恶意软件可以通过 IoSetCompletionRoutine 设置完成例程,然后通过 IoCallDriver 将请求传递给下一个驱动程序。以下是来自微软文档的示例:

IoCopyCurrentIrpStackLocationToNext( Irp ); IoSetCompletionRoutine(
  Irp, // Irp
  MyLegacyFilterPassThroughCompletion, // CompletionRoutine
  NULL, // Context
  TRUE, // InvokeOnSuccess
  TRUE, // InvokeOnError
  TRUE); // InvokeOnCancel
return IoCallDriver(NextLowerDriverDeviceObject, Irp);

一旦链条中的最后一个驱动程序执行 IoCompleteRequest API,完成例程将按顺序执行,从最低的驱动程序的完成例程开始,依次到最高的。如果 rootkit 是附加到该设备的最后一个驱动程序,它的完成例程将在最后执行。

现在,让我们了解另一种常常被 rootkit 用来隐藏恶意活动的技术。

DKOM

DKOM 是 rootkit 用来隐藏恶意用户模式进程的最常见技术之一。该技术依赖于操作系统如何表示进程和线程。要理解这一技术,你需要更多地了解 rootkit 操作的对象:EPROCESSETHREAD

内核对象 – EPROCESS 和 ETHREAD

Windows 为系统中每个创建的进程创建一个叫做 EPROCESS 的对象。该对象包含关于此进程的所有重要信息,例如它的 ActiveProcessLinks,该链连接所有进程的 EPROCESS 对象。每个 EPROCESS 对象包含指向下一个 EPROCESS 对象(表示下一个进程)的地址,称为 FLink,以及指向前一个 EPROCESS 对象(与前一个进程相关)的地址,称为 BLink。这两个地址都存储在 ActiveProcessLinks 中:

图 7.14 – EPROCESS 结构

图 7.14 – EPROCESS 结构

EPROCESS 的确切结构会随着操作系统版本的不同而变化。也就是说,某些字段会被添加,某些字段会被删除,有时还会发生重排。rootkits 必须跟上这些变化,才能操控这些结构。

在深入探讨对象操控策略之前,还有一个你需要了解的对象:ETHREADETHREAD 及其核心 KTHREAD 包含与特定线程相关的所有信息,包括线程上下文、状态以及相应进程对象 (EPROCESS) 的地址:

图 7.15 – ETHREAD 结构

图 7.15 – ETHREAD 结构

当 Windows 在线程之间切换时,它会遵循 ETHREAD 结构中的链接(即连接所有 ETHREAD 对象的链表)。从这个对象,它加载线程的进程(跟踪其 EPROCESS 地址),然后加载线程上下文来执行它。加载每个线程的过程与连接所有进程的链表(特别是它们的 EPROCESS 表示)没有直接关系,这使得 DKOM 攻击非常有效。

rootkits 如何执行对象操控攻击?

为了隐藏进程,rootkit 只需修改前后两个 EPROCESS 对象(相对于恶意软件)的 ActiveProcessLink,跳过它想要隐藏的进程的 EPROCESS 地址。步骤很简单,具体如下:

  1. 使用 PsLookupProcessByProcessId API 获取当前进程的 EPROCESS

  2. 跟踪 ActiveProcessLinks,找到需要隐藏的进程的 EPROCESS 对象。

  3. 更改前一个 EPROCESSFLink 属性,使其不指向此 EPROCESS,而是指向下一个进程。

  4. 更改下一个进程的 BLink 属性,使其不指向此 EPROCESS,而是指向前一个进程。

在此过程中具有挑战性的一部分是,可靠地找到 ActiveProcessLinks,因为 Windows 从一个版本到另一个版本引入了许多变化。处理 ActiveProcessLinks(以及进程 ID)偏移量的技术有多种,如下所示:

  1. 获取操作系统版本,并根据该版本选择合适的偏移量(从为每个操作系统版本预先计算的偏移量中选择)。

  2. 查找进程 ID(可以通过 PsGetCurrentProcessId 获取),并找到与进程 ID 相关的 ActiveProcessLinks 偏移量。

这是第二种技术的示例:

图 7.16 – 从 EPROCESS 对象中查找进程 ID

图 7.16 – 从 EPROCESS 对象中查找进程 ID

一旦 rootkit 能够在 EPROCESS 对象(epocs)中找到进程 ID(pids),它可以使用 ActiveProcessLinks 和进程 ID 之间的偏移量(通常是预先计算好的,并且是结构中的下一个字段)。最后一步是删除进程之间的链接,如下图所示:

图 7.17 – 移除进程链接以执行 DKOM 攻击

图 7.17 – 移除进程链接以执行 DKOM 攻击

结果将如下所示:

图 7.18 – DKOM 攻击 – 遍历时跳过中间的进程

图 7.18 – DKOM 攻击 – 遍历时跳过中间的进程

检测 DKOM 攻击的最常见技术是遍历所有正在运行的线程,并通过它们的链接找到EPROCESS,然后将结果与通过ActiveProcessLinks获得的数据进行比较。如果在ActiveProcessLink中缺少一个出现在活跃线程中的EPROCESS对象,这意味着根套件正在执行 DKOM 攻击,隐藏该进程及其EPROCESS对象。

现在,让我们讨论恶意软件如何在内核模式下执行进程注入。

内核模式下的进程注入

内核模式下的进程注入是一种被多个恶意软件家族广泛使用的技术,包括Stuxnet(其MRxCls rootkit)利用该技术在合法进程名称下维护持久性并隐藏恶意活动。为了让设备驱动程序能够读写进程内存,它需要将自身附加到该进程的内存空间。

一旦驱动程序附加到该进程的内存空间,它就可以看到该进程的虚拟内存,并能够直接读写。举个例子,如果进程可执行文件的 ImageBase 是0x00400000,那么驱动程序可以正常访问它,如下所示:

CMP WORD PTR [00400000h], 'ZM'
JNZ <not_mz>

为了让驱动程序能够附加到进程内存,它需要使用PsLookupProcessByProcessId API 获取其EPROCESS,然后使用KeStackAttachProcess API 附加到该进程的内存空间。在反汇编代码中,代码如下所示:

图 7.19 – 使用 PID 获取 EPROCESS 对象(来自 Stuxnet rootkit,MRxCls)

图 7.19 – 使用 PID 获取 EPROCESS 对象(来自 Stuxnet rootkit,MRxCls)

然后,要附加到该进程的内存空间,你可以使用以下代码:

图 7.20 – 附加到进程的内存空间

图 7.20 – 附加到进程的内存空间

一旦驱动程序附加,它就可以读取和写入其内存空间,并且可以使用ZwAllocateVirtualMemory API 分配内存,通过ZwOpenProcess API 提供进程句柄(相当于用户模式下的OpenProcess)。

驱动程序要从进程内存中分离,可以执行KeUnstackDetachProcess API,如下所示:

KeUnstackDetachProcess(APCState);

还有其他技术,但这种技术是任何驱动程序轻松访问任何进程虚拟内存作为自身内存的最常见方式。现在,让我们来看一下它是如何在该进程内执行代码的。

使用 APC 排队执行注入代码

在调用 SleepExSignalObjectAndWaitMsgWaitForMultipleObjectsExWaitForMultipleObjectsExWaitForSingleObjectEx 等 API 后,线程被恢复之前,所有排队的用户模式和内核模式 APC 函数都将在该线程的上下文中执行,从而使恶意软件能够在该进程内执行用户模式代码,然后再将控制权交还给它。

对于一个恶意软件样本排队 APC 函数,它需要执行以下步骤:

  1. 通过提供 PsLookupThreadByThreadId API,获取要排队 APC 函数的线程的 ETHREAD 对象。

使用 KeInitializeApc API 将用户模式函数附加到该线程。

  1. 使用 KeInsertQueueApc API 将此函数添加到该线程中待执行的 APC 函数队列,如下图所示:

图 7.21 – APC 排队执行用户模式函数(来自 winSRDF 项目)

图 7.21 – APC 排队执行用户模式函数(来自 winSRDF 项目)

在这个示例中,KeInitializeApc API 将在线程从其可警报状态返回后执行一个内核模式函数 ApcKernelRoutine 和一个用户模式函数 Entrypoint

如果线程没有执行之前提到的任何 API,并且在终止之前从未进入可警报状态,那么队列中的 APC 函数将不会被执行。因此,一些恶意软件家族倾向于将它们的 APC 线程附加到应用程序中的多个运行线程上。

其他 rootkit,例如 MRxCls(来自 Stuxnet),在应用程序执行之前修改其入口点。这允许恶意代码在应用程序运行之前在主线程的上下文中执行,并且不使用任何 APC 排队功能。

到这一阶段,我们已经了解了 rootkit 的一般工作原理,接下来让我们谈谈为了对抗 rootkit 所开发的保护机制。

x64 系统中的 KPP(PatchGuard)

在 x64 系统中,微软引入了一种新的保护机制,防止内核模式钩取和打补丁,称为 KPP,也叫 PatchGuard。此保护机制禁用了对 SSDT 和核心内核代码的任何打补丁,并且不允许使用内核分配以外的内核栈。

此外,微软只允许在 x64 系统中加载签名的驱动程序,除非系统处于测试模式或禁用了驱动程序签名强制。

当 KPP 最初推出时,受到防病毒和防火墙厂商的强烈批评,因为 SSDT hooking 和其他钩取方法在多个安全产品中被广泛使用。微软为帮助防病毒产品替换其钩取方法,创建了一个新的 API。

尽管已有多种绕过 PatchGuard 的方法被文档化,但在过去的几年里,微软仅发布了少数几个主要更新来应对这些技术。此外,PatchGuard 代码在内核模式中的位置会随着每次更新发生变化,使其成为一个移动目标,并且打破了所有先前能够绕过 PatchGuard 的恶意软件家族。

现在,让我们看看一些之前的恶意软件家族介绍的不同绕过技术。

绕过驱动程序签名强制检查

除了能够使用被窃取的证书签名恶意驱动程序(例如 Stuxnet 驱动程序),还可以通过命令提示符禁用驱动程序签名强制选项,如下所示:

bcdedit.exe /set testsigning on

在这种情况下,系统将开始允许使用未由微软颁发的证书签名的驱动程序。此命令需要管理员权限,并且之后需要重启机器。然而,通过社会工程学的帮助,可以欺骗用户执行此操作。以前可用的另一个选项是以下命令:

bcdedit /set nointegritychecks on

然而,在撰写时,这个选项在现代版本的 Windows 上被忽视。

此外,一些恶意软件家族滥用合法产品的有漏洞签名驱动程序,这些驱动程序要么存在代码执行漏洞,要么存在允许修改内核内部任意内存的漏洞。一个例子就是 Turla 恶意软件(被认为是国家支持的 APT 恶意软件)。它加载了一个 VirtualBox 驱动程序,并利用它修改了 g_CiEnabled 内核变量,从而在运行时禁用驱动程序签名强制检查(无需重启系统)。

绕过 PatchGuard – Turla 示例

Turla 还通过禁用系统完整性检查失败时显示蓝屏死机的功能绕过了 PatchGuard。PatchGuard 在检测到未经授权的系统内核修补或其重要表格(如 SSDT 或 IDT)被修改时,调用 KeBugCheckEx API 来显示蓝屏死机。Turla 恶意软件挂钩了这个 API,并正常执行。

PatchGuard 的一个后续版本会动态克隆这个 API,以确保验证得到执行并导致系统关闭。然而,Turla 能够挂钩 KeBugCheckEx API 中的一个早期子程序,确保在完整性检查失败后能够正常恢复系统执行。以下代码是 KeBugCheckEx API 的一个代码片段:

mov qword ptr [rsp+8],rcx
mov qword ptr [rsp+10h],rdx
mov qword ptr [rsp+18h],r8
mov qword ptr [rsp+20h],r9
pushfq
sub rsp,30h
cli
mov rcx, qword ptr gs:[20h]
add rcx,120h
call nt!RtlCaptureContext

如你所见,它执行了一个名为 RtlCaptureContext 的函数,这是 Turla 恶意软件选择挂钩的地方,用来绕过这个更新。

绕过 PatchGuard – GhostHook

该技术由 CyberArk 研究团队在 2017 年提出。它利用了英特尔引入的一个新功能,称为 Intel 处理器跟踪 (Intel PT)。该技术允许调试软件跟踪单个进程、用户模式和内核模式的执行,或进行指令指针跟踪。Intel PT 技术设计用于性能监控、诊断代码覆盖、调试、模糊测试、恶意软件分析和漏洞检测。

Intel 处理器及其 callback 例程来处理内存空间问题。这个 callback 函数(即 PMI 处理程序)是恶意软件的目标,它在被监控的运行线程的上下文中执行。

在特定情况下,恶意软件可以通过使用非常小的缓冲区,在每次 sysenter 调用后强制执行其 PMI 处理程序,并执行另一种技术,称为 sysenter 钩取,而不会触发 PatchGuard 保护,也不需要进行 API 钩取。

现在,我们将看看如何分析 rootkits,特别是如何动态分析 rootkits。

内核模式下的静态和动态分析

一旦我们了解了 rootkits 的工作原理,就可以开始分析它们。值得一提的是,并非所有内核模式恶意软件家族只是隐藏实际负载的存在——其中一些还可以执行恶意操作。在本节中,我们将熟悉一些工具,它们可以促进 rootkit 分析,帮助理解恶意软件功能,并学习一些特定使用的细微差别。

静态分析

从静态分析开始总是明智的,尤其是在调试环境无法立即使用的情况下。在某些情况下,使用相同的工具可以同时进行静态和动态分析。

Rootkit 文件结构

Rootkit 样本通常是实现传统 MZ-PE 结构的驱动程序,并在 IMAGE_OPTIONAL_HEADER32 结构的子系统字段中指定 IMAGE_SUBSYSTEM_NATIVE 值。它们使用我们已经熟悉的传统 x86 或 x64 指令。因此,任何支持这些指令的工具(不包括用户模式调试器如 OllyDbg)应该能够处理 rootkits,而不会遇到重大问题。它们的例子包括 IDA、radare2 等工具。此外,IDA 插件如 win_driver_pluginDriverBuddy 对于辅助操作非常有用,比如解码涉及的 IOCTL 代码。

分析工作流程

一旦打开样本,第一步是追踪 DriverObject,它作为主函数的第一个参数提供(在 32 位系统中通过堆栈,在 64 位系统中通过 rcx 寄存器)。通过这种方式,我们可以监视是否有任何主要功能是由恶意软件定义的。这个对象实现了 _DRIVER_OBJECT 结构,并在其末尾列出了主要功能。对应的结构成员如下:

PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];

在汇编语言中,它们可能通过偏移量进行访问,可以通过应用此结构轻松映射。

此外,值得检查是否通过IoSetCompletionRoutine API 指定了任何完成例程。

然后,我们需要搜索允许禁用安全措施的指令,例如前面提到的写保护,涉及使用CR0寄存器。通过这种方式,可以轻松识别代码中实现此功能的确切位置。

接下来,我们需要跟踪已经讨论过的重要导入函数,这些函数通常被 rootkit 使用,并检查相应的参数字符串以了解其用途。恶意软件是否附着到任何设备上?是否提到任何进程或文件名?一旦这些问题得到解答,就可以弄清楚 rootkit 的目标。

最后,如果导入函数是动态解析的,在继续分析之前恢复它们是有意义的。通常,可以通过脚本或借助动态分析来完成此操作。

动态与行为分析

内核模式威胁的动态分析是一个更为棘手的部分,因为它在低级别进行,任何错误都可能导致系统崩溃。因此,强烈建议在虚拟机VMs)上进行操作,这样调试状态可以快速恢复到之前的状态。另一种选择是使用通过串口连接的独立机器。然而,在这种情况下,恢复之前的调试状态通常需要更多的努力。

调试器

当我们谈论动态分析时,我们所指的主要工具是调试器。最流行的调试器如下:

  • ."),以及扩展命令(以 "!" 开头的命令)。以下是进行 rootkit 分析时最常用的一些命令:

    • ?:用于显示常规命令。

    • .help:用于显示元命令。

    • .hh:用于打开指定命令的文档。

    • bpbu,和ba:用于设置断点,包括常规断点、未解析断点(当模块加载时激活)和访问断点。

    • blbdbe,和bc:分别用于列出、禁用、启用和清除断点。

    • gp,和t:这些命令分别表示继续执行(go),单步执行(single step)和单步追踪(single trace)。

    • du:分别用于显示内存和反汇编指令。

    • e:用于将指定的值输入内存(即编辑内存)。

    • dt:用于解析和描述数据类型。例如,dt ntdll!_PEB将显示带有偏移量、字段名和数据类型的 PEB 结构。

    • r:用于显示或修改寄存器。这里,r eip=<val>可以用来更改指令指针。

    • x: 用于列出匹配模式的符号;例如,x ntdll!* 将列出来自 ntdll 的所有符号。

    • lm: 用于列出模块;它通过显示加载的驱动程序及其对应的内存范围来工作。

    • !dh: 这是一个转储头命令;它可以用来解析和显示 MZ-PE 头信息,通过 ImageBase。

    • !process: 显示指定进程的各种信息,包括 PEB 地址。例如,!process 0 0 lsass.exe 将显示 lsass.exe 的基本信息,使用 7 标志可以显示完整的细节,包括 TEB 结构。

    • .process: 此命令设置进程上下文。例如,.process /i <PROCESS>(其中 <PROCESS> 值可以从之前提到的 !process 命令的输出中获取),然后执行 g.reload /user 可以让你切换到指定进程的调试模式。

    • !peb: 解析并显示指定进程的 PEB 结构。此命令可以帮助你通过首先使用 .process 命令切换到进程上下文。

    • !teb: 解析并显示指定的 TEB 结构。

    • .shell: 允许你从 WinDbg 使用 Windows 控制台命令。例如,.shell -ci "<windbg_command>" findstr <value> 允许你解析执行命令的输出。

    • .writemem: 该命令将内存转储到文件中。

  • IDA: 虽然不能单独调试内核模式代码,但它可以作为 WinDbg 的 UI 使用。通过这种方式,它可以让你将静态分析的所有标记和调试代码存储在同一地方。

  • radare2: 与 IDA 相同,这个工具可以在 WinDbg 上使用,且有专门的插件支持。

  • SoftICE (已废弃): 这曾是最流行的 Windows 低级动态分析工具之一。本文写作时,该工具已经废弃,不支持新系统。

除此之外,还有一些其他内核模式调试器,如 SyserRasta Ring 0 Debugger (RR0D)、HyperDbgBugChecker,这些工具似乎不再维护。

监视器

这些工具可以帮助我们洞察与内核模式相关的各种对象和事件:

  • DriverView: 这是 NirSoft 开发的一个工具;它可以让你快速获取已加载驱动程序及其在内存中的位置。

  • DebugView: 这是一个 Sysinternals 工具,允许你监视来自用户模式和内核模式的调试输出。

  • WinObj: 这是 Sysinternals 提供的另一个有用工具,可以列出与内核模式调试相关的各种系统对象,例如设备和驱动程序。

使用它们可能让你快速了解当前系统的全局状态。

Rootkit 检测器

这组工具检查系统中是否存在 rootkit 常用的技术,并提供详细信息。它们在行为分析中非常有用,可以确认样本是否已正确加载。此外,它们还可以用来相对快速地确定样本的功能。一些最受欢迎的工具如下:

  • GMER:这个强大的工具支持多种 rootkit 模式,并提供相对详细的技术信息。它可以搜索各种隐藏的伪装物,如进程、服务、文件、注册表项等。此外,它还具有 rootkit 清除工具。

  • RootkitRevealer:这是另一个先进的 rootkit 检测工具,来自 Sysinternals。与 GMER 不同,它的输出不太技术化,并且已经有一段时间没有更新了。

  • 其他 rootkit 检测工具(现已停用)包括 Rootkit UnhookerDarkSpyIceSword

除了这些,许多杀毒厂商还在开发多种 rootkit 清除工具;然而,它们通常没有提供足够的技术信息来分析威胁。

设置测试环境

有几种可用的选项来执行内核模式调试:

  • 调试器客户端在目标机器上运行:这种设置的一个例子是 WinDbg 或 KD 调试器,利用本地内核调试或与LiveKd工具协作。这种方法不需要工程师设置远程连接,但在这种情况下,并非所有命令都可用。

  • 调试器客户端在主机机器上运行:在这种情况下,虚拟机或其他物理机用于执行样本,所有调试工具以及以标记形式保存的工作结果都存储在外部。此方法可能需要稍多的设置时间,但通常推荐使用,因为它会在后期节省大量时间和精力。

  • 调试器客户端在远程机器上运行:这种设置并不常见;这里的想法是,主机机器运行一个调试服务器,可以与目标机器进行交互,工程师从第三台机器远程连接到该服务器。这种技术被微软称为远程调试。

设置主机和目标机器之间的连接的具体方法可能会有所不同,取决于工程师的偏好。通常,通过网络或电缆进行连接。对于虚拟机,通常通过将串口映射到管道来完成;例如,如果使用的是 COM1 端口,你可以按照以下步骤进行:

  1. 在 VMWare 中,转到 \\.\pipe\<any_pipe_name>。在其余选项中,选择 该端是服务器另一端是应用程序,然后勾选 在轮询时让 CPU 休眠 复选框。

  2. 在 VirtualBox 中,打开虚拟机的设置并转到 \\.\pipe\<any_pipe_name>

图 7.22 – 通过 COM 端口进行内核模式调试的 VirtualBox 设置

图 7.22 – 使用 VirtualBox 进行内核模式调试通过 COM 端口

也可以通过网络进行远程调试,但在这种情况下,来宾机和主机应该共享网络连接,这可能并不总是可取的。

除此之外,要能够进行内核模式调试,目标系统还需要显式允许。执行以下步骤以实现此功能:

  1. 在现代 Windows 操作系统上,以管理员身份运行标准的bcdedit工具并输入以下命令:

    bcdedit /debug on
    
  2. 如果使用本地内核调试,请执行以下命令:

    bcdedit /dbgsettings local
    
  3. 另外,如果使用的是串口,请执行以下命令(适用于 COM1):

    bcdedit /dbgsettings serial debugport:1 baudrate:115200
    
  4. 如果您还想保留原始启动设置,可以创建一个单独的条目,设置如下:

    bcdedit /copy {current} /d "<any_custom_display_name>"
    
  5. 然后,您可以获取生成的<guid>值,并使用它来将所需的设置应用到新条目中:

    bcdedit /set <guid> debug on
    bcdedit /set <guid> debugport 1
    bcdedit /set <guid> baudrate 115200
    

在较旧的操作系统(如 Windows XP)上,可以通过在boot.ini文件中复制默认的启动条目,并使用新的显示名称,添加/debug参数来启用内核模式调试。也可以结合设置调试端口,添加/debugport=com1 /baudrate=115200参数。最终的条目将如下所示:

multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="<any_custom_display_name>" /fastdetect /debug /debugport=com1 /baudrate=115200

确保指定的系统位置与原始条目中使用的匹配。

之后,需要重启计算机,并在启动过程中选择新添加的选项。此步骤也可以在稍后进行,方法是禁用安全检查后进行。

如果需要设置网络调试或使用 Hyper-V 机器,请始终遵循最新的官方 Microsoft 文档。

设置调试器

现在,我们可以运行调试器并检查一切是否按预期工作。如果使用的是本地调试,可以通过以管理员身份运行 WinDbg 并使用以下命令行来完成:

windbg.exe -kl

对于通过串口调试,可以使用_NT_DEBUG_PORT_NT_DEBUG_BAUD_RATE环境变量,或使用正确的命令行参数来指定端口和波特率。对于 COM 端口,设置如下:

windbg.exe -k com:pipe,port=\\.\pipe\<pipe_name>,baud=115200,resets=0,reconnect

也可以通过图形界面进行此操作,使用文件| 内核调试...

图 7.23 – 使用 VirtualBox 和 WinDbg 通过 COM 端口进行内核模式调试

图 7.23 – 使用 VirtualBox 和 WinDbg 通过 COM 端口进行内核模式调试

别忘了之后重启来宾机。

另一种选择是使用一个独立的VirtualKD项目,它旨在提高内核调试的性能,特别是在使用 VMWare 或 VirtualBox 虚拟机时。请遵循官方安装文档,确保它按预期工作。

如果您使用的是 IDA 和 WinDbg 的组合,可以按以下方式进行设置:

  1. 最好确保在PATH环境变量或%IDA%\cfg\ida.cfg文件中指定了正确的 WinDbg 路径(即DBGTOOLS变量)。

  2. 对于内核模式调试,通常建议使用 WinDbg 的 32 位版本;请再次确认在 IDA 的 输出 窗口中使用的是哪个版本。

  3. 打开 IDA 实例,不打开任何文件,但选择 开始 快速启动选项。

  4. 转到 调试器 | 附加 | Windbg 调试器,并指定以下连接字符串,管道名称与虚拟机中使用的名称匹配:

    com:pipe,port=\\.\pipe\<pipe_name>,baud=115200,resets=0,reconnect
    
  5. 然后,在同一对话框中,转到 调试选项 | 设置特定选项,并选择 带有重连和初始中断的内核模式调试 选项(重连是可选的,但应与连接字符串中指定的值匹配)。

一旦确认,以下对话框将会出现:

图 7.24 – IDA 附加到目标机器上的 Windows 内核

图 7.24 – IDA 附加到目标机器上的 Windows 内核

  1. 确定。调试器将中断到内核,WINDBG 命令行将在窗口底部显示。

  2. 查看 | 打开子视图 | 类型库 中添加与内核模式相关的类型库(通常,它们的名称中包含 ddkwdk),这样可以访问多个标准枚举和结构体(你也可以使用 Shift + F11 键盘快捷键)。

一旦确认调试器成功执行,就需要设置符号信息,以便可以在各种 WinDbg 命令中使用标准的 Windows 名称。为此,请在 WinDbg 控制台中执行以下命令:

.sympath srv*<local_path_for_downloaded_symbols>*https://msdl.microsoft.com/download /symbols
.reload /f

在 WinDbg 图形用户界面中,可以通过 -y 命令行参数来指定此项。此外,也可以在 _NT_SYMBOL_PATH 环境变量中进行设置。

如果目标机器和主机机器没有互联网访问权限,则也可以通过使用在目标机器上创建的符号清单文件,从另一台计算机下载符号。为此,请执行以下步骤:

  1. 在目标机器上执行以下命令:

    symchk /om manifest.txt /ie ntoskrnl.exe /s
    <path_to_any_empty_dir>
    
  2. ntkrnlpa.exe 可以替代 ntoskrnl.exe。最后一个参数 /s 旨在避免名称解析延迟。

重要提示

某些 WinDbg 版本存在一个 bug,导致输出文件为空。在这种情况下,请尝试使用不同的版本。

  1. 将创建的 manifest.txt 文件移动到具有互联网访问权限的机器上。

  2. 运行以下命令:

    symchk /im manifest.txt /s srv*<local_path_for_downloaded_symbols>*https://msdl.microsoft. com/download/symbols
    
  3. 完成此操作后,可以将下载的符号文件移动到主机机器并用于调试目的:

    .sympath <local_path_to_downloaded_symbols>
    .reload /f
    

请记住,如果更新目标机器,符号可能会变得无效,应该重复该过程。

停止在驱动程序入口点

现在,我们应该设置一个调试器来拦截驱动程序代码执行的瞬间,以便在其开始时立即控制它。在大多数情况下,我们没有分析样本的符号信息,因此无法使用常见的 WinDbg 命令(如 bp <driver_name>!DriverEntry)来停止在驱动程序入口点。有几种其他方法可以做到这一点,如下所示:

  • 通过设置未解析的断点:可以使用以下命令设置一个断点,当模块加载时将触发:

    bu <driver_name>!<any_string>
    

尽管调试器在此处不会准确地停在入口点,但在第一次停下后,可以手动到达入口点。为此,从控制台输出窗口获取驱动程序的基址,添加入口点的偏移量,然后为结果地址设置一个断点。接着,移除或禁用先前的断点并继续执行。

  • 通过模块加载中断:以下命令允许你拦截所有新加载的模块(可以使用冒号或空格):

    sxe ld:<driver_name>.sys
    

这在调试器中的显示方式如下:

图 7.25 – 在特定模块加载时中断

图 7.25 – 在特定模块加载时中断

一旦调试器中断,就可以在驱动程序的入口点设置断点,并继续使执行在那里停下来:

图 7.26 – 在驱动程序入口点设置断点

图 7.26 – 在驱动程序入口点设置断点

在 IDA 中,与 WinDbg 一起工作时,可以通过转到调试器 | 调试器选项...并启用在库加载/卸载时挂起选项来全局实现此功能。

  • IopLoadDriver API 将控制权转移到驱动程序。不同版本的 Windows 中会有所不同,可以通过以下命令找到它:

    .shell -ci "uf /c nt!IopLoadDriver" grep -B 1 -i "call.*ptr
    \.*h"
    Or, on newer systems:
    .shell -ci "uf nt!guard_dispatch_icall" grep -i "jmp.* rax" | head -n 1
    

一旦找到了偏移量(它将类似于nt!IopLoadDriver+N),就可以在该地址设置断点,并拦截系统将控制权转移到新加载的驱动程序的所有时刻。好处是它可以多次重用,直到系统接收到更新并改变它:

![图 7.27 – 拦截系统将控制权转移到加载的驱动程序的时刻图 7.27 – 拦截系统将控制权转移到加载的驱动程序的时刻+ int 3 指令表示软件断点),重新计算其头部中的校验和字段(在Hiew编辑器中,可以通过选择头部中的此字段,按一次F3重新计算它,然后按F9保存更改),并加载它。调试器会在此指令处断开,因此可以恢复修改后的值为原始值。通常,修改后的指令在修补后不会执行。这意味着需要单步执行,确保它不起作用,返回 IP 寄存器到已更改的指令,然后再继续像往常一样进行分析。这种方法通常需要更多时间,而且也会破坏驱动程序的签名,但在必要时仍然可以使用。## 加载驱动程序现代 64 位 Windows 系统或开启了安全启动的 32 位系统不允许加载未签名的驱动程序。如果示例驱动程序未签名,通常需要弄清楚它是如何在实际环境中执行的(例如,通过滥用其他合法驱动程序),并复现它。通过这种方式,我们可以确保恶意软件的行为完全符合预期。或者,也可以禁用系统安全机制。暂时禁用它最可靠的方法是进入启动过程的高级选项,并选择 bcdedit.exe /set testsigning on 命令,但不建议用于分析,因为它仍然要求驱动程序必须由某个证书正确签名。现在,是时候加载分析过的驱动程序了。这也可以通过 Windows 控制台直接使用标准的 sc 功能来完成:sc create <any_name> type= kernel binpath= "<path_to_driver>" sc start <same_name>下面是前面代码块的一个示例:图 7.28 – 使用 sc 工具加载自定义驱动程序

图 7.28 – 使用 sc 工具加载自定义驱动程序

请注意 type=binpath= 参数后的空格;它们对于确保操作如预期般顺利至关重要。

恢复调试状态

如果使用 IDA,许多工程师在重新加载驱动程序时会遇到一个问题,即它的基地址在内存中发生了变化,导致 IDA 无法应用现有的标记。一个解决方案是将标记保存为 IDC 文件,并创建一个脚本,根据新的位置重新映射所有地址。然而,还有一种更好的组织方式:建议通过调试状态创建 VM 快照,并在必要时通过 IDA 重新连接到这些快照。这样,所有地址都能保持一致,因此可以无更改地应用相同的 IDC 文件。

总结

在本章中,我们熟悉了 Windows 内核模式,学习了如何将请求从用户模式传递到内核模式,再从内核模式返回。然后,我们讨论了 Rootkit,它们可能针对这个过程的哪些部分,以及为什么这样做。我们还介绍了现代 Rootkit 中实施的各种技术,包括恶意软件如何绕过现有的安全机制。

最后,我们探讨了用于执行内核模式威胁静态和动态分析的工具,学习了如何设置测试环境,并总结了进行分析时可以遵循的通用指南。通过完成本章,您应该对高级内核模式威胁的工作原理以及如何使用各种工具和方法进行分析有了深入的了解。

第八章,《处理漏洞与 Shellcode》中,我们将探讨各种类型的漏洞,并了解合法软件如何被滥用,以便攻击者执行恶意操作。

第三部分 检查跨平台和字节码恶意软件

能够使用相同的源代码支持多个平台始终是攻击者和专注于有针对性攻击的人士首选的方法。因此,在过去几年中出现了多个跨平台恶意软件家族,这导致了对能够分析它们的工程师的需求增加。通过学习本节,您将了解跨平台恶意软件的具体情况,并深入理解如何处理它们。

本节包括以下章节:

  • 第八章*,处理利用和 Shellcode*

  • 第九章*,逆向字节码语言 – .NET、Java 及其他*

  • 第十章*,脚本和宏 – 逆向、反混淆和调试*