THMDynamicAnalysisDebugging
Introduction
在 基本动态分析 房间中,我们学习了如何在执行期间识别受感染系统中的恶意软件痕迹。但是,恶意软件作者知道恶意软件将被分析并希望阻止它。这可以通过各种规避技术来实现。为了击败这些规避技术,恶意软件分析师希望对恶意软件的执行有更多控制权。在这个房间里,我们将了解恶意软件分析师如何更好地控制恶意软件的执行以实现预期结果。
学习目标:
在这个房间里,我们将学习:
- 用于逃避基本动态分析的规避技术。
- 调试器简介以及它们如何帮助我们控制恶意软件的执行流程。
- 通过更改寄存器或其他参数在运行时操纵执行流程。
- 修补恶意软件以迫使它越过规避技术并进入实际的恶意内容。
先决条件:
为了从这个房间获得最佳学习成果,建议您完成以下房间:
The Need for Advanced Dynamic Analysis
分析恶意软件就像猫捉老鼠的游戏。恶意软件分析师不断设计新技术来分析恶意软件,而恶意软件作者则设计新技术来逃避检测。这项任务将回顾一些阻碍我们使用静态或基本动态分析来分析恶意软件的技术。
逃避静态分析:
在静态分析室,我们学习了对恶意软件进行静态分析的技术。恶意软件经常隐藏或试图看起来像合法软件,以逃避恶意软件分析师的窥探。由于我们在静态分析期间没有执行恶意软件,因此逃避静态分析的主要重点是混淆程序的真实功能,直到程序被执行。以下技术通常可用于实现此目的。图像显示放大镜聚焦在计算机屏幕上,其中错误正试图隐藏在文件夹后面。
更改哈希值
:我们之前了解到每个文件都有一个唯一的哈希值。恶意软件作者通过稍微更改他们的恶意软件来利用此功能。这样,恶意软件的哈希就会发生变化,从而绕过基于哈希的检测机制。即使恶意软件的某个部分发生变化,哈希也会发生变化(除非我们讨论的是上下文触发的分段哈希或模糊哈希),因此只需添加 NOP 指令或其他此类更改即可击败基于哈希的检测技术。击败 AV 签名
:反病毒签名和其他基于签名的检测通常依赖于恶意软件内部的静态模式。恶意软件作者会更改这些模式以试图逃避签名。这种技术通常伴随着恶意软件代码的一般混淆。字符串混淆
:一些恶意软件作者通过在运行时解码恶意软件中的字符串来混淆它们。当我们在恶意软件中搜索字符串时,我们可能找不到任何有用的东西。但是,当恶意软件运行时,它会在执行过程中解码这些字符串。恶意软件作者可能会混淆重要的字符串,例如 URL、C2 域等,以避免基于单字符串搜索而烧毁基础设施。运行时加载 DLL
:由于我们可以在分析 PE 标头时识别恶意软件导入,因此恶意软件作者经常使用 Windows 库的 LoadLibrary 或 LoadLibraryEx 在运行时加载 DLL。在静态分析此恶意软件时,我们可能无法在分析其标头时看到它所链接的所有函数。打包和混淆
:打包在恶意软件作者中非常流行。打包恶意软件就像打包礼物。当我们看一个打包的礼物时,除非我们打开包装并取出礼物,否则我们无法知道里面可能是什么。同样,打包程序通过编写在运行时解码恶意软件的代码将恶意软件打包在包装器中。因此,在执行静态分析时,我们可能无法看到打包程序中的内容。但是,当我们执行恶意软件时,它会解开代码,将实际的恶意代码加载到内存中,然后执行它。
正如我们可能已经观察到的那样,大多数这些技术都适合隐藏在显眼的地方,但在执行动态分析时执行恶意软件时,它们可能会被击败。
逃避基本动态分析:
恶意软件作者不会接受自己的命运,让动态分析检测他们的样本。为了逃避动态分析,人们采用了一系列技术。这些技术通常依赖于识别恶意软件是否在受控分析环境中运行。以下技术通常用于此目的:
识别虚拟机
:虽然其中一些技术现在可能会适得其反,因为许多企业基础设施都托管在虚拟机上,但恶意软件作者最喜欢的方法之一是识别恶意软件是否在虚拟机内运行。为此,恶意软件通常会检查与流行的虚拟化软件(如 VMWare 和 Virtualbox)相关的注册表项或设备驱动程序。同样,最小资源(例如单个 CPU 和有限的 RAM)可能表明恶意软件正在虚拟机内运行。在这种情况下,恶意软件将采取不同的非恶意执行路径来欺骗分析师。定时攻击
:恶意软件通常会尝试使自动分析系统超时。例如,当执行恶意软件时,它会尝试使用 Windows Sleep 库休眠一天。几分钟后,自动分析系统将关闭,找不到任何恶意活动的痕迹。较新的恶意软件分析系统可以识别这些攻击,并尝试通过缩短恶意软件休眠时间来缓解它们。但是,恶意软件可以通过执行有针对性的计时检查来识别这些缓解措施,以查看时间是否被操纵。这可以通过记录执行时间并将其与执行睡眠调用后的当前时间进行比较来完成。
-用户活动痕迹
:恶意软件试图识别机器中是否有用户活动的痕迹。如果没有发现或发现很少的痕迹,恶意软件将决定它正在被在受控系统内执行并采取不同的良性执行路径。用户活动的痕迹可能包括没有鼠标或键盘移动、缺少浏览器历史记录、没有最近打开的文件、系统正常运行时间很短等。- 分析工具的识别:恶意软件可以使用 Process32First、Process32Next 或类似功能向 Windows 操作系统询问正在运行的进程列表。如果在正在运行的进程列表中识别出流行的监视工具,则恶意软件可以采取良性执行路径。例如,如果 ProcMon 或 ProcExp 正在运行,恶意软件可以识别它并切换到良性活动。识别分析工具的另一种方法是查看系统中打开的不同窗口的名称。如果恶意软件在打开的窗口中发现 Ollydbg 或 ProcMon,它可以切换到不同的执行路径。
通过使用这些技术,恶意软件作者使恶意软件分析师难以执行分析。但是,恶意软件分析师可以使用一些工具和技术来更好地控制恶意软件的执行,从而帮助他们击败这些规避技术。在接下来的任务中,我们将了解其中一些技术。
Introduction to Debugging
软件程序员广泛使用“调试”一词来识别和修复程序中的错误。同样,试图逃避检测或逆向工程的恶意软件样本也可以被视为有错误的程序。恶意软件逆向工程师通常必须调试程序以消除阻止其执行恶意活动的任何障碍。因此,交互式调试成为高级恶意软件分析的重要组成部分。调试器为恶意软件分析师提供了所需的控制,以便更密切地监视正在运行的程序,查看执行每条指令时不同寄存器、变量和内存区域的变化。调试器还允许恶意软件分析师更改变量的值以控制程序在运行时的流程,从而更好地控制恶意软件遵循的执行路径。图片显示一个人一手拿着放大镜,另一手拿着手术工具,看着电脑屏幕,试图移除一个错误。图片描绘了移除错误的行为,即对计算机进行调试
调试器的类型:
我们可以将调试器大致分为以下三类之一。
源代码级调试器:
源代码级调试器在源代码级上工作。大多数软件程序员在编写代码时使用源代码级调试器来检查代码中的错误。与其他两个选项相比,源代码级调试器是一种高级调试器。使用源代码级调试器时,我们经常会看到局部变量及其值。
汇编级调试器:
当程序被编译后,其源代码就会丢失,无法恢复。恶意软件分析就是这种情况。我们没有正在调查的恶意软件的源代码;相反,我们有一个编译的二进制文件。汇编级调试器可以帮助我们在汇编级调试编译的程序。在使用汇编级调试器进行调试时,我们经常会看到 CPU 寄存器的值和被调试者的内存。这是用于恶意软件逆向工程的最常见调试器类型。调试器附加到需要调试的程序上,并根据分析师的意愿执行它。
内核级调试器:
内核级调试器比汇编级调试器还要低一级。顾名思义,这些调试器在内核级调试程序。对于此级别的调试,通常需要两个系统。一个系统用于调试另一个系统上运行的代码。这是因为如果使用断点停止内核,整个系统都会停止。
现在让我们进入下一个任务,学习如何调试恶意软件。
Familiarization With a Debugger
对于恶意软件分析,有许多选项可供选择调试器。这些选项包括 Windbg、Ollydbg、IDA 和 Ghidra。对于这个房间,我们将使用 x32dbg 和 x64dbg。
附加的 VM 本质上是一个 FLARE VM,由 Mandiant(以前是 FireEye,现在是 Google 的一部分)分发,广泛用于逆向工程恶意软件。它包括我们在这个房间进行分析所需的所有工具。首先,我们可以导航到桌面 > 工具 > 调试器 > x32dbg.exe 来运行调试器。我们将看到以下界面。
要在调试器中打开文件,我们可以导航到文件 > 打开并打开所需的文件。下面的屏幕截图显示了在调试器中打开示例的界面。我们目前在界面中看到 CPU 选项卡。请注意,我们必须对 32 位样本使用 x32dbg,对 64 位样本使用 x64dbg。
正如我们在左下角看到的,程序的执行暂停,因为已到达系统断点。我们可以控制是一次执行一条指令还是执行整个程序。但在此之前,让我们看一下上面的屏幕截图。在这里,我们可以在中间窗格中看到反汇编,指令指针指向启动程序时执行的下一条指令。在右侧窗格中,我们可以看到寄存器及其值。我们可以注意到 EIP 中的值是 EIP 在反汇编窗格中指向的地址。同样,在底部窗格中,我们可以看到堆栈视图(右侧)、转储视图(左侧)和显示我们调试示例所花费时间的计时器(右上角)。
让我们看看其他一些选项卡。断点选项卡显示断点的当前状态。断点是暂停程序执行以便分析师分析寄存器和内存的点。可以通过单击 CPU 选项卡中该指令前面的点来启用特定指令上的断点。
内存映射显示程序的内存。
我们还可以看到程序的调用堆栈。
程序中运行的线程显示在线程选项卡中。
任何文件、进程或进程访问的其他资源的句柄都会显示在句柄选项卡中。
在下一个任务中,我们将学习如何利用这些信息进行恶意软件分析。
Debugging in Practice
现在我们已经熟悉了 x32dbg 的 UI,让我们通过逐步执行程序来学习如何调试程序。首先,从附加的 VM 打开其中一个 crackmes。我们可以通过单击文件 > 打开来执行此操作。我们将看到下面的窗口,要求我们选择要选择的文件。选择 crackme-arebel.exe 作为我们要调试的文件。
我们选择需要打开的文件。调试器将自身附加到进程并在启动之前将其暂停。我们可以看到在后台打开了一个空白的命令窗口。这表明进程已启动但被调试器停止。请注意,此窗口可能不会随所有进程一起打开。这取决于进程的用户界面。在调试器窗口中,我们可以看到熟悉的反汇编、寄存器、堆栈和其他有用信息。请注意,地址可能并不总是如图所示。
在调试器窗口中,我们有一些功能可以帮助我们控制执行。在下面的屏幕截图中,我们可以看到其中一些功能被突出显示。
其中,从左到右,我们有打开新文件、从头开始重新启动打开的文件的执行以及停止执行的功能。箭头键将执行程序,直到它被其他控件停止或暂停(请不要立即按下此按钮,因为它可能会冻结程序。如果这样做了,请通过任务管理器终止程序以重新启动程序)。然后我们有暂停选项。最后两个选项用于进入和跳过。其余选项也很有用,但对于这个房间的范围,我们将仅使用上面提到的选项。
现在我们知道了如何控制执行,让我们使用箭头键执行程序(请只按一次)。当我们这样做时,我们可以在右下角看到程序的状态变为正在运行,然后是暂停。除了状态之外,我们还有暂停执行的原因。它显示“INT3 断点“TLS 回调 1”……”。这意味着我们遇到了 TLS 回调,并且调试器被编程为中断 TLS 回调的执行。
在调试器中,我们可以通过转到“选项”>“首选项”菜单来设置在程序上放置自动断点的位置。以下屏幕截图显示了自动放置断点的不同点。我们可以看到已选中“TLS 回调”和“系统 TLS 回调”,这意味着执行将在这些事件上中断。
由于 TLS 回调通常用作反逆向工程技术,因此我们在导航此 TLS 回调时应小心谨慎。因此,在回调中单步执行每条指令是明智的。我们可以通过单击上面讨论的步入选项来做到这一点。在步入每条指令后,我们会看到 EIP 移动到下一条指令,寄存器和堆栈中的值也会随之变化。在步入几条指令后,我们到达一个条件跳转指令。在反汇编下方的窗格中,我们可以看到调试器告诉我们未执行跳转。寄存器窗格显示已设置 ZF,因此未执行跳转。
如果我们分析这两条执行路径,我们会看到,如果跳转被执行,它会转到地址 D116E,弹出 EBP 并返回。相反,当前执行路径(未执行跳转)会将我们带到地址 D1000。我们可以通过双击该地址来查看此地址上的内容。那么我们就去那里吧。或者,我们可以将鼠标悬停在地址上以查看该地址上的指令。下面的屏幕截图显示了我们跟踪地址时的代码。如果我们继续跟踪,我们会看到一些 API 调用,例如 CreateToolhelp32Snapshot、LoadLibrary 和 GetProcAddress。如果我们确定这个函数调用是有意的,并且我们想在函数执行后返回,我们可以跳过,这将使我们在函数执行后返回。但是,该函数似乎太重要了,不能立即绕过。
这些库可用于逃避检测或用于合法目的,但目前我们还不知道。因此,我们可以沿着这条路径继续,如果我们看到红旗,我们只需使用重新启动执行选项重新启动它即可。继续前进,我们看到正在加载的库是 Suspend Thread。
SuspendThread API 正在加载。此 TLS 回调将根据检测到正在运行的进程(例如调试器)来挂起线程(CreateToolhelp32Snapshot API 有助于识别正在运行的进程)。这就是为什么如果我们继续执行,程序将冻结的原因。我们可以通过沿着此代码路径前进来验证这一假设。经过验证,我们发现这是一个规避代码路径。因此,我们想绕过它。为此,我们希望在 TLS 回调的开头进行跳转。让我们重新启动执行并在下一个任务中转到 TLS 回调。
Bypassing unwanted execution path
在下面的屏幕截图中,我们可以看到执行已重新启动并返回到我们识别出检测逃避的代码分支,即 TLS 回调断点。
按照几条指令到达条件跳转,我们到达以下点。
我们看到此时跳转未执行。在上一个任务中,我们确定 ZF 是跳转未执行的原因。通过双击右侧窗格中的 ZF,我们可以将 ZF 更改为 0。
我们在这里可以看到 ZF 现在为 0,并且跳转已执行。这样,我们就可以绕过不需要的执行路径。如果我们进一步操作或执行程序,它将进行跳转并移动到地址 D116E。
我们在这里所做的是通过操纵寄存器在运行时更改执行路径。但是,如果我们再次执行样本,除非我们更改寄存器,否则它将再次进入规避路径。如果我们想解除二进制文件的攻击,我们将不得不对其进行更改,以便如果只是通过双击运行,则规避路径不是一个选项。这称为修补。这样,在解除攻击后,我们可以执行动态分析以达到我们想要的结论。让我们看看我们如何做到这一点。
我们可以右键单击要编辑的指令以找到与该指令相关的选项。其中包括要编辑的选项,如下面的屏幕截图所示。
当我们按下编辑时,我们会看到以下窗口。
在最下方的窗格中,我们看到十六进制的 75 05,这是条件跳转指令的操作码。我们可以通过以下方式更改它。
- 将 jne 更改为 je。这样,无需更改 ZF 即可执行跳转。
- 将条件跳转更改为无条件跳转。这样,无论上述条件如何,跳转都会始终执行。
在这两个选项中,第二个选项更好,因为它为跳转提供了保证。但是,还有一种方法可以编辑二进制文件,使其采用我们首选的执行路径。也就是说,通过用 NOP 填充地址 D1169 处的调用指令。正如在上一个房间中讨论的那样,NOP 是不执行任 何操作的指令。下面的屏幕截图显示,我们可以通过右键单击并转到二进制 > 用 NOP 填充指令,用 NOP 填充指令。
完成后,我们可以转到文件 > 补丁文件将此文件写入磁盘。导航到该窗口后,我们可能会看到类似下面的窗口。
通过单击此窗口上的“修补文件”,调试器将询问我们将修补后的文件保存在哪里。这是我们用 NOP 填充后的样子。请注意,一条指令已被五条 NOP 指令取代。这是因为一条指令的大小等于五条 NOP 指令。这在上面的屏幕截图中更加明显,其中每条 NOP 指令都映射到大小相同的操作码上。
在附加的虚拟机中,我们可以看到文件 crackme-arebel1.exe 已被修补。这是修补并保存后的文件的样子。
好极了,我们已经使用调试器修补了一个逃避调试器的文件!
Conclusion
这就是这个房间的全部内容。在这个房间里,我们学习了以下内容:
- 逃避静态和基本动态恶意软件分析的常用技术。
- 使用调试器对恶意软件进行更深入的分析。
- 使用调试器在运行时更改环境。
- 使用调试器修补恶意软件样本。
然而,这不是全部。恶意软件作者还使用许多技术来逃避动态分析和调试。前往 反逆向工程 房间了解更多逃避技术以及如何应对它们。