Fortinet 分享用模拟执行的方式对抗恶意软件中的 Anti-RE 的实现
2022-5-18 11:55:0 Author: www.4hou.com(查看原文) 阅读量:17 收藏

对于逆向工程人员来说,他们会经常使用模拟技术来对抗此示例中的函数调用混淆和字符串加密。我们将使用flare-emu 框架实现一个IDAPython 脚本,以使IDA Pro 中的反汇编更具可读性。这将对样品的静态分析有很大帮助。

以Pandora为例

在这篇文章中,我将讨论在 Pandora 中看到的恶意程序研发人员使用的两种特定的反逆向工程技术:

1.使用不透明谓词进行函数调用混淆;

2.加密字符串;

使用不透明谓词的函数调用混淆

下图显示了 Pandora 勒索软件中一个简单的函数调用在解压后的样子。

graphic-01.png

Pandora 中的标准函数调用

我们可以看到正在调用的函数的地址是在运行时计算的。 cs:qword_7FF6B6FF9AB8 似乎是某种函数地址表的基地址。然后我们使用硬编码值在该表中找到正确的函数指针,这就是我们在调用它之前加载到 rax 中的内容。不透明谓词通常表示程序中的表达式,其结果为程序员所知,但仍需要在运行时进行评估。它以许多不同的方式用作混淆和反分析技术。在这种情况下,进入 rax 的值是固定的,但是因为它仍然必须在运行时计算,它会破坏静态分析工具。

如果我们以上图为例,rax 中的地址是这样计算的:

2.png

或十进制:

3.png

此类问题的简单解决方案是在调试器中运行恶意软件并从那里获取地址。但是在这个示例中,所有函数调用都是这样的,静态链接库中的函数调用除外。这意味着我们需要在调试器中的每个函数调用处中断,以便对恶意软件中发生的事情进行自动化处理。

加密字符串

这个特定勒索软件样本的另一个挑战是所有有趣的字符串都被加密了。二进制文件中有很多纯文本字符串(如下图所示),但它们大多是 Windows API 函数名称和嵌入式库中的字符串。没有任何可以帮助我们了解恶意软件正在做什么的字符串以纯文本形式提供。这在现代恶意软件中非常常见,因此该挑战的解决方案可用于对抗各种恶意软件。

4.png

Pandora 示例中的字符串

通常当遇到带有加密字符串的恶意软件时,有两种方法:

1.使用动态方法,例如调试或模拟,并使用恶意软件自己的字符串解密函数来完成工作。

2.如此详细地了解解密功能,以至于可以在一个简单的脚本中重新实现它。当加密是简单的单字节异或时,这通常是最简单的途径。

就 Pandora 而言,至少有 14 个不同的字符串解密函数,因此重新实现解密算法可能并不总是可行的。

模拟

模拟允许我们假装代码运行在 CPU 上,但模拟软件运行的不是真正的 CPU,而是运行代码。与实际执行相比,模拟通常非常慢。但是,它允许我们完全控制我们想要运行的内容以及与模拟代码的高度交互。例如,使用模拟器,我们可以只模拟恶意软件的一个功能,甚至只是几行代码,并在每条指令处评估程序的状态。在这种情况下,模拟的一大优势是我们可以直接在 IDA Pro 中进行。

flare-emu

flare-emu 是由 Mandiant 的 FLARE 团队创建的模拟框架。它建立在著名的模拟引擎 Unicorn Engine 和 IDAPython 之上。可以直接使用 Unicorn 引擎,但flare-emu 隐藏了它的一些复杂性。本质上,人们可以定义想要模拟的内容,并为特定的挂钩定义回调函数,当模拟到达该挂钩时将调用这些函数。一个很好的例子是 callHook 参数,它接受一个回调函数,每次将要模拟 CALL 指令时调用该函数。在这个回调函数中,我们可以实现在那种情况下我们想做的任何事情,即转储寄存器、更改数据、跳过调用等。flare-emu 变得非常简单且相对易于使用。

解决挑战

我们可以编写一些代码来使用 IDAPython 脚本解决这些挑战。

函数调用混淆

下图再次显示了我们首先要解决的问题。这是 Pandora 代码解包部分中 main() 函数中的第一个函数调用。我们可以相当确定,如果我们模拟 main() 函数并在调用之前检查 rax 的值,那么我们会得到正确的结果。我们也可以读出函数调用的参数,并将所有这些信息作为 IDA Pro 中的注释添加到汇编代码中。

5.png

函数调用混淆

让我们开始整理我们的 IDAPython 脚本。下图显示了我们如何初始化模拟。当我们启动脚本时,它应该模拟 IDA 中光标当前所在的函数(由 get_screen_ea() 返回)。

6.png

初始化模拟

要初始化flare-emu,我们只需要实例化一个EmuHelper。 Flare-emu 提供了不同的方式来运行我们的模拟。我们使用 emulateRange() 函数,它用于指定我们想要模拟的内存范围。我们将起始地址设置为函数的开头,结束地址可以省略(python 中为 None),这意味着模拟将一直运行,直到到达返回类型指令。请注意,iterateAllPaths() 而不是 emulateRange() 也应该可以工作,但是由于 Pandora 中的另一种混淆技术导致了问题,这不在本文的讨论范围内。但在不太复杂的恶意软件中 iterateAllPaths() 可能是更好的选择。

当调用flare-emu 的一个模拟函数(在这种情况下为emulateRange())时,模拟开始。该框架允许我们为模拟提供额外的细节,例如带有寄存器和堆栈的处理器状态,或回调函数的数据,但我们现在不需要这些。

emulateRange() 允许我们为不同的挂钩定义回调函数:

1.指令挂钩:在模拟每条指令之前调用。我用它为 IDA 中模拟的每条指令涂色,以可视化模拟的覆盖范围。

2.调用挂钩:每当模拟 CALL 类型的指令时调用。请注意,默认情况下不会模拟被调用的函数。

3.内存访问挂钩:每当访问内存以进行读取或写入时调用。

对于我们当前的任务,我们只需要 callHook。如上图中的第 9 行所示,我们已经将 call_hook 函数名称作为 callHook 参数传递。接下来,我们需要定义 callHook 函数,如下图所示。

7.png

call_hook() 的第一个实现

我们创建了 call_hook() 函数,每次在模拟 CALL 指令之前,模拟器都会调用该函数。在其当前状态下,该函数将记录它已被执行,然后使用 analysisHelper 检查当前 CALL 指令中的操作数是否为寄存器。如果没有,那么我们可以返回,因为只有注册案例对我们来说是有趣的。然后我们恢复寄存器的名称 (operand_name) 及其值 (operand_name) 并暂时记录它们。如果我们针对 main 函数运行脚本,那么我们会得到下图中的结果。请注意,由于 Pandora 代码中存在许多其他恶意混淆,这个简单的脚本将无法模拟整个函数。但这可以通过扩展脚本来完成。

8.png

第一次测试的结果

通过模拟找到了三个 CALL 指令并打印了操作数寄存器的值。仔细想想,我们基本上解决了函数调用混淆的问题,因为我们现在知道不同的 CALL 指令调用了哪些地址。现在我们只需要将它添加到 IDA 中的反汇编中。这些是每当我们解析 CALL 指令时我们想要在 IDA 中做的事情:

1.添加一个带有被调用函数地址的注释。

2.为该函数调用添加带有参数的注释。

在 IDA 中为调用的函数添加交叉引用

更新后的代码如下图所示。

9.png

添加评论和交叉引用

在创建评论时,我们使用了来自flare-emu 的一个功能。它允许我们以独立于架构的方式获取函数参数。这个恶意软件是 x86_64,所以我们可以只使用 rcx、rdx、r8、r9 和堆栈。调用挂钩获取的参数之一是参数变量,它将包含flare-emu认为是此函数调用的参数的值。当然,如果不分析被调用的函数,我们将不知道需要多少参数,所以我们只打印所有参数。

最后(第 23 行)我们添加了一个 IDA 交叉引用,这将对我们的分析有很大帮助。如果我们在 main 函数上再次运行此代码,我们会得到下图中的结果。

10.png

函数调用解析的结果

加密字符串

现在我们已经解决了第一个问题,并且有了一个可以使用的模拟框架,我们可以继续我们的第二个挑战,解密字符串。为了能够知道要模拟哪个函数来解密字符串,我们唯一的要求是我们需要知道哪些函数是解密函数。与往常一样,逆向工程是一个迭代过程。一旦我们运行我们在 main 函数上编写的脚本,那么我们就可以开始分析调用的函数了。那么我们如何判断一个函数是否是一个解密函数呢?

1.我们在 IDA 中看到了这一点。无需深入研究 0x7ff6b6f971e0 处的函数,我们可以在图形视图中看到它相当简单并且有一些循环。

11.png

0x7ff6b6f971e0 处函数的图形视图

如果我们滚动浏览代码,我们会在下图中找到基本块,我们可以看到它迭代了某个值并对其进行异或。这表明它可能是基于 XOR 的编码/加密。

12.png

XOR 表示解码/解密

2.我们在调试器中看到它。在进行静态分析的同时,我们当然也可以调试恶意软件(在安全的环境中)。在调试器中,当我们看到一个函数获取一些地址作为输入并返回一个字符串时,这可能意味着它是一个解密函数。下图显示了 0x7ff6b6f971e0 处的函数何时返回,并且确实在 rcx 中返回了字符串“ThisIsMutexa”。

13.png

解密后的字符串出现在 rcx 中

一旦我们知道一个函数是一个解密函数,我们就可以相应地重命名它(我们使用了 mw_decrypt_str())。有趣的是,Pandora 使用了多个解密函数,随着我研究深入,我们慢慢发现了这些函数。最后,我们确定了 14 个不同的解密函数,但其中大多数看起来与图 9 非常相似,这使我们能够快速查看一个函数是否只是另一个解密函数。

一旦我们知道了(一些)解密函数,我们就可以改进我们的idpython脚本,以便在看到解密函数被调用时模拟函数调用。这实际上非常类似于flare-emu文档中的一个示例,该文档展示了此类代码通常可以很好地重用。

下图显示了更新后的 call_hook() 函数。从第 23 行开始,我们首先检查我们正在调用的地址处的函数是否具有包含字符串 mw_decrypt_str 的名称。这就是我们判断被调用函数是否为解密函数的方式。

14.png

向 call_hook() 添加解密

如果它是一个解密函数,那么我们在脚本中调用decrypt()函数。这将返回解密后的纯文本字符串。然后,我们创建一个注释,其中也将包含解密后的字符串。

解密过程如下图所示。我们创建一个新的 EmuHelper 实例,在启动 emulateRange 时,我们使用函数名称 (fname) 来获取函数的地址作为起始地址。我们还将 argv 数组的前四个元素作为参数寄存器传递。最后我们返回argv[0]中的值。

15.png

模拟解密进程

在 IDA 中运行脚本后,结果如图 12 所示。解密后的字符串是 ThisIsMutexa,它被添加到注释中并记录在输出中。

16.png

字符串解密成功

现在我们可以自动解密字符串了。随着我们对代码的分析和更多解密函数的发现,我们可以在调用这些解密函数的函数上重新运行脚本来恢复纯文本字符串。

结论

Pandora 勒索软件包含混淆和反逆向工程技术。在这篇文章中,我们研究了其中两个:函数调用混淆和字符串加密。我们使用flare-emu 模拟框架编写了一个IDAPython 脚本来解析函数调用的地址和参数,并模拟解密函数以将字符串恢复为纯文本。最终脚本可以进一步开发,以应对 Pandora 勒索软件深入分析中讨论的其他反逆向工程挑战。

本文翻译自:https://www.fortinet.com/blog/threat-research/Using-emulation-against-anti-reverse-engineering-techniques如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/GW1y
如有侵权请联系:admin#unsafe.sh