作为恶意软件研究人员,您经常会遇到复杂、严重混淆的样本。我们今天处理的恶意软件 GuLoader 就是一个典型的例子。
看看这段伪代码,它是通过反编译其汇编代码生成的——它很难看且不可读。
面对这样的难题,你可能会不知所措。你应该从哪里开始呢?您如何处理该样本的分析?让我们分解一下。
那么,在本文中,我们将使用 GuLoader 作为参考,探讨反混淆此类代码的策略。您将了解:
本文基于 ANY.RUN 之前发布的GuLoader 恶意软件分析。请访问我们的博客,找到我们将分析的示例,以及解压说明和 Ghidra 脚本,该脚本部分自动化了我们从现在开始将要介绍的大部分内容。
本文重点关注静态分析。但如果您想动态分析 GuLoader 样本,则可以使用ANY.RUN云恶意软件沙箱。
使用您的企业电子邮件申请企业计划的14 天免费试用。充分利用 VM 实例的延长生命周期、无限任务以及 Windows 版本 7 至 11。
动态分析可让您通过将恶意软件的行为与其技术设置联系起来,了解恶意软件在现实世界中的工作原理。这是一种检查其在不同系统设置中的操作并收集 IOC 的方法。
事不宜迟,让我们开始吧。
解压样本后,我们开始手动分析 shellcode,并很快意识到它已被混淆。研究代码使我们能够将 GuLoader 使用的混淆技术分为几类:
现在是停下来分析正在使用的混淆方法并制定反混淆策略的好时机。
例如,在我们的例子中,大多数技术引入的代码不会改变最终的执行结果。因此,我们通常可以安全地“NOP”它们以提高可读性。但请谨慎行事——正如我们很快就会发现的那样,并非所有混淆的代码都与程序的操作无关。
现在,让我们单独检查这些混淆技术,看看如何击败它们。
代码中散布着许多 XMM 指令。这些看起来是无序的,增加了分析过程的复杂性。我们从解压的 shellcode 的第一个字节就遇到了它们。
请注意,由于缺乏默认支持,许多仿真引擎都会遇到困难。我们对 Angr、Triton 和 Ghidra 的嵌入式引擎进行了测试,结果都没有达到要求。
在 GuLoader 的例子中,XMM 指令实际上并不影响代码的预期行为。您会在许多恶意软件中遇到类似的混淆方法。因此,我们可以安全地将所有 XMM 指令替换为“NOP”,如下表所示:
以下是 Ghidra 反汇编程序中的结果:
无条件 JMP 指令将代码分割成更小的块。此方法经常用于避免防病毒软件和其他安全工具的检测。此外,它还可能使分析师的工作变得更加耗时和令人沮丧,因为他们必须在这些块之间跳转,尤其是在处理大量代码时。 GuLoader 和其他恶意软件通常采用这种技术。
这种混淆方法很容易被破解。反编译代码中的反汇编器通常会成功连接这些块,从而提高代码可读性,即使存在这些无条件跳转也是如此。因此,我们可以保留小块原样,而不需要合并它们。
垃圾汇编指令通常作为额外的混淆层发挥作用。这些指令不执行任何有形功能,通常保持寄存器值、执行流程或内存不变。
您也会在 GuLoader 中遇到这些。
留意不执行任何操作的指令(“NOP”、“FNOP”)以及移位或循环零位的指令(“SHL reg, 0”;“ROL reg, 0”)。还存在其他无影响的指令,例如“OR reg, 0”、“XOR reg, 0”、“CLD”、“WAIT”。
处理虚假比较指令比仅仅用“NOP”替换垃圾指令更具挑战性。我们无法删除所有比较指令,因为有些比较指令对于正确的代码功能是必需的。解决这个问题的一种方法是“标记”我们遇到的所有比较指令。如果没有找到使用比较结果的指令,则可以安全地执行 NOP。如果我们发现条件跳转或类似的情况,我们会取消比较标记以避免删除。
下表显示了一个示例,其中除“CMP EDX,0x0”之外的所有比较指令都选择性地替换为 NOP:
GuLoader 还采用了使用假“PUSHAD”指令以及匹配的“POPAD”指令的混淆策略。它们可以临时修改寄存器值,但会被“POPAD”恢复原始寄存器值而无效。
我们的研究表明 GuLoader 中的所有“PUSHAD”指令都是无关的。因此,我们通过用 NOP 替换“PUSHAD”、“POPAD”和中间指令来解决这个问题:
然而,并非GuLoader中的所有“POPAD”指令都是垃圾指令。我们对那些没有相应“PUSHAD”的部分保持不变。
另一种与前一种类似的混淆技术是使用假的“PUSH”指令。这些指令将一个值压入堆栈,然后立即将其弹出。
一个示例是包含“PUSH SS”指令,其后可能是修改寄存器或存储器位置的指令。然而,随后的“POP SS”将堆栈指针恢复到其初始值。
击败假 PUSH 指令类似于假 PUSHAD 的过程,但保持非推送寄存器不变至关重要。
不透明谓词是总是返回 true 或 false 的条件语句,但它们很难分析或预测。这些可以在 GuLoader 的代码中找到,并且使逻辑理解变得复杂。
例如,诸如“MOV BL,0xB6”和“CMP BL,0xB6”之类的一对指令后面可以跟有诸如“JNZ ADDR”之类的条件跳转。比较总是返回 false,因为比较的值等于移动的值,使得跳转不必要且令人困惑。
由于需要“预测”跳转条件,克服不透明谓词似乎具有挑战性。然而,我们的情况更加简单,因为所有不透明谓词都落在“PUSHAD”和“POPAD”块内。因此,我们只需将这些指令之间的所有谓词替换为 NOP。
混淆算术表达式是 GuLoader 使用的更有趣的技术之一。它们使得理解实际执行的操作变得更加困难。这些表达式包含算术运算,例如加法或减法。有时,它们与其他混淆混合在一起,例如虚假比较、不透明谓词和垃圾指令。
一个示例是将常量值移入寄存器并执行算术运算:
另一个例子是将常量值压入堆栈并在内存上执行计算:
为了反混淆 GuLoader 的算术表达式,我们采用了类似于处理假比较的方法。我们标记第二个参数是标量值的所有“MOV”指令,以及参数是标量的所有“PUSH”指令。当遇到算术运算时,我们更新第一条指令中的常量值并用 NOP 替换当前值。这样,第一条指令就有了结果,后续的算术指令被 NOP 代替。
以下是优化“MOV”和“PUSH”操作的示例:
请谨慎对待操作数的大小。在操作过程中保持正确的尺寸至关重要。
在ANY.RUN专家的这篇文章中,我们使用 GuLoader 作为现实示例来强调典型的反混淆策略。我们首先分析代码,然后对类似的混淆策略进行分组,最后想出独特的方法来解决每个问题。
但重要的是要了解这些技术并不是一刀切的解决方案。每个恶意软件样本可能会呈现其独特的混淆策略,需要独特的对策。
我们的讨论强调了将反混淆等复杂任务分解为清晰、可管理步骤的重要性。请记住,成功反混淆任何恶意软件都需要详细分析、发现混淆模式并创建优化策略。