Stantinko’ s 僵尸网络独特的混淆技术

Andrew 2020-07-31
专栏 - 观点观察 发布于 2020-07-31 14:55:03 阅读 32 评论 0

Stantinko僵尸网络背后的攻击者使用了了几种混淆技术(obfuscation techniques),其中一些尚未公开描述。在本文中,我们将剖析这些技术并进行分析。

为了阻止分析并避免检测,Stantinko的新模块使用了多种混淆技术:

  • 字符串混淆-构造有意义的字符串,并且仅在使用它们时出现在内存中
  • 控制流混淆-将控制流转换为难以阅读的形式,并且如果不进行大量分析,则无法预测基本块的执行顺序
  • 死码–从未执行的代码的添加;它还包含从未调用的导出。其目的是使文件看起来更合法以防止检测
  • 无效代码–已执行的代码的添加,但对整体功能没有实质影响。它旨在绕过行为检测
  • 死字符串和资源–添加资源和字符串而不会影响功能

在这些技术中,最值得注意的是字符串的混淆和控制流的混淆。我们将在以下各节中详细介绍它们。

字符串混淆

模块中嵌入的所有字符串均与实际功能无关。它们的来源是未知的,它们要么用作构建实际使用的字符串的构建块,要么根本不使用。

恶意软件使用的实际字符串会在内存中生成,以避免基于文件的检测和干扰分析。它们是通过重新排列诱骗字符串的字节(嵌入在模块中的字节)并使用字符串操作的标准函数(如strcpy()、strcat()、strncat()、strncpy()、sprintf()、memmove()及其Unicode版本)形成的。

由于要在特定功能中使用的所有字符串始终在该功能的开头按顺序进行组装,因此可以模拟功能的入口点,并提取出现以显示这些字符串的可打印字符序列。

Stantinko’s的新型密码器具有独特的混淆技术

图1.字符串混淆的示例。图像中有7个突出显示的诱饵字符串。例如,红色标记的将生成字符串“NameService”。

控制流平坦化

控制流平坦化是一种混淆技术,用于阻止分析并避免检测。

通过将单个功能拆分为基本块,可以实现常见的控制流平坦化。然后,将这些块作为分派放置在循环内的switch语句中(即,每个分派恰好由一个基本块组成)。有一个控制变量来确定应在switch语句中执行哪个基本块。其初始值在循环之前分配。

基本块均分配有一个ID,并且控制变量始终保持要执行的基本块的ID。

所有基本块都将控制变量的值设置为其后继的ID(一个基本块可以具有多个可能的后继;在这种情况下,可以在某种情况下选择直接后继)。

Stantinko’s的新型密码器具有独特的混淆技术

图2.通用控制流平坦化循环的结构

解决这种混淆的方法有很多种,例如使用IDA的微码API。Rolf Rolles使用这种方法来启发式识别这些循环,从每个平坦化的块中提取控制变量,并根据控制变量对其进行重新排列。

这种方法(和类似方法)不适用于Stantinko的混淆,因为与常见的控制流平坦化混淆相比,它具有一些独特的功能:

  • 代码在源代码级别上被平坦化,这也意味着编译器可以在生成的二进制文件中引入一些异常
  • 控制变量在控制块(稍后说明)中递增,而不在基本块中递增
  • 调度包含多个基本块(划分可能是分离的,即每个基本块完全属于一个调度,但是有时调度会交织在一起,这意味着它们共享一些基本块)
  • 平坦化循环可以嵌套和连续
  • 多功能合并

这些特征表明,Stantinko为这项技术引入了新的障碍,必须对其进行分析其最终有效负载。

Stantinko中的控制流平坦化

在Stantinko的大多数函数中,代码分为几个调度程序(如上所述)和两个控制块(一个头和一个尾巴)来控制功能的流程。

负责人通过检查控制变量来决定执行哪个调度。尾部将控制变量增加一个固定常数,然后返回到头部或退出平坦化循环:

Stantinko’s的新型密码器具有独特的混淆技术

图3. Stantinko的控制流平坦化循环的规则结构

Stantinko似乎正在使所有函数和高层构造体(例如for循环)的代码趋于平坦化,但是有时它也倾向于选择看似随机的代码块。由于它在函数和高级构造上都应用了控制流平坦化循环,所以它们可以自然地嵌套,并且碰巧也有多个连续的循环。

通过合并多个函数的代码创建控制流平坦化循环时,使用不同的值来初始化所得合并函数中的控制变量。控制变量的值作为参数传递给结果函数。

通过重新排列二进制文件中的块,我们克服了这种混淆技术;我们的方法将在下一节中介绍。

重要的是要注意,我们在某些平坦化循环中观察到多个异常,这使得去模糊过程的自动化更加困难。它们中的大多数似乎是由编译器生成的。这使我们相信在编译之前已经应用了控制流平坦化混淆。

我们目睹了以下异常情况;它们可以单独或组合出现:

  1. 有些调度可能只是死码-它们将永远不会执行。(下面的“控制流展平循环内的死代码”部分中的示例。)
  2. 调度内的基本块可以交织在一起,这意味着它们可以包含联合代码。

Stantinko’s的新型密码器具有独特的混淆技术

图4.带有共享共享代码的调度的平坦化循环的结构

  1. 从分派直接跳转到平坦化循环外部,紧接在尾部后面的块,以及从函数返回的块。

Stantinko’s的新型密码器独特的混淆技术

图5.平坦化循环的结构,其调度直接中断了循环。仅出现一条虚线。

  1. 可以有多个尾巴,也可以根本没有尾巴–在后一种情况下,控制变量在每次调度结束时都会增加。

Stantinko’s的新型密码器具有独特的混淆技术

图6.没有任何尾巴(左)而有多个尾巴(右)的展平环的结构

  1. 头部不立即包含跳转表。取而代之的是,可以有多个跳转表,并且在跳转表之前有一系列分支,二进制搜索正确的调度。
  2. 控制变量的值可以在调度内部使用;这意味着即使在去混淆的代码中也必须保留/计算控制值。

Stantinko’s的新型密码器具有独特的混淆技术

图7. EDI寄存器包含传递给EAX并在分派中使用的控制变量。调度以红色突出显示。

  1. 有时,尾部包含的指令对于恢复寄存器和局部变量的正确值至关重要。在去混淆过程中,我们删除了尾部,因此,即使这些指令不属于尾部,我们也必须确保在每次分配后执行这些指令。
  2. 在某些情况下,此时没有ID等于控制变量当前值的调度。

反混淆技术

我们的目标是构建一个反混淆功能,该功能能够在二进制级别上重新排列代码,以使反向工程师易于阅读,同时保持结果代码的可执行性。它必须能够识别属于每个调度的所有基本块,并能够任意复制和移动它们。

在基本块操作期间,必须确保重新计算分支目标的相对地址和正确形成合法跳转表的地址。

我们的解决方案没有考虑重新定位;因此,始终需要确保样本加载到相同的基地址。

我们使用了一种反向工程框架,该框架为我们提供了一些有用的功能,例如汇编操纵和符号执行引擎。

该函数的核心参数是控制块的地址(头部和尾部),控制变量的范围和步长,寄存器的名称以及包含该控制变量的存储器位置,control_locations和最后的地址。循环之后的第一个基本块,我们将其定义为next_block。显然,它还要求对函数的地址进行反混淆,以及反混淆函数应放置的地址。

由于上述异常4,我们预计会有多个条尾巴。

反混淆函数通过其步长值在控制变量的范围内进行迭代,以模拟实际的控制流平坦化循环。在每次迭代中,该函数均通过生成一个上下文来处理异常6和7。该上下文将放置在相应的调度之前。

上下文是一个基本块,其中包含分配寄存器和存储器地址并保持control_locations更新的指令。第一次迭代的上下文仅保留控制变量的值。(注意:处理第4个异常不需要上下文。)

先前调度的最后一个基本块(或者,如果是第一次调度,则在头部之前的基本块)被重定向到创建的上下文。

(在每个迭代中)要执行的调度的初始基本块由控制变量的当前值(调度ID)确定。

通过符号执行二进制搜索算法找到实际的基本块,该算法使用当前ID搜索基本块。符号执行的初始状态包含分配给控制变量当前值的control_locations。

我们在(i)包含无条件分支,或者(ii)具有无法由控制变量确定的目标的第一个基本块处停止符号执行。

也可以模仿这一部分,或者使用一种框架,该框架可以将二进制搜索算法简化为跳转表,然后将其转换为switch语句。这些方法处理异常5。

如果没有针对特定ID的调度,则循环将继续并由于异常8而增加控制变量。

然后,将整个调度(即,从其初始基本块到其头部,尾部或next_block可以到达的每个基本块)复制到前一个上下文块之后(如上所述)。由于异常2.它不能仅被移动。

当前由于异常3可能发生两种罕见的情况;两者都会导致迭代过早终止。情况发生在调度时:

  • 从函数返回
  • 指向next_block

最终,当迭代结束时,先前调度的最后一个基本块(或在第一次调度的情况下,位于头之前的基本块)被重定向到展平循环之外的第一个基本块。

此方法将自动解决异常1,因为无效调度不会复制到结果代码中。

Stantinko’s的新型密码器具有独特的混淆技术

图8.混淆函数(左)及其反混淆函数(右)的示例。调度按以下顺序执行:dispatch1→dispatch2→dispatch3。

然后将这些更改写入应放置去混淆功能的虚拟地址。

如果要处理合并函数的平坦化,则将指向目标函数的引用指向参数中控制变量的初始值相同的目标函数指向新的反混淆函数的地址。

Stantinko’s的新型密码器具有独特的混淆技术

图9.混淆后的(右)和反混淆后的(左)控制流程图的示例

可能的改进

上面描述的方法完全在汇编级操作,这不足以使反混淆完全自动化。

原因是很难准确识别所有模式,这主要是由于源代码级混淆中存在各种编译器优化。在我们的情况下,模式识别是必需的,例如,要自动填写核心反混淆功能的参数。

这种方法的优势在于,可以立即执行生成的代码,并且可以使用任意的逆向工程工具进行进一步的分析。

可以通过使用渐进式中间表示(IR)来进一步改进此方法,该中间表示提供了一些优化技术,该优化技术将消除由编译器生成的大多数异常,从而允许自动识别反混淆功能所需的参数。

一个人也可以使用选定的IR进行识别和反混淆处理,在我们的案例中,后者的反混淆处理包括重新排列基本块。

此选项的缺点是生成的代码也将位于IR中,这意味着必须同时使用IR进行连续分析。使用IR的工具及其功能的数量可能会非常有限,尤其是在可视化方面。因此,很难分析更复杂的样本,尤其是当存在其他混淆层时。我们也将无法执行结果代码。

死码

“死码”是指永远不会执行的代码,或者对功能没有整体影响的代码。该恶意软件包含的死码大部分位于展平的循环中(已通过我们上述的反混淆功能有效删除),但是例如,还有未使用的出口,并且无法将未使用的出口与合法出口区分开。

至于展平循环中的无效代码:对于Stantinko而言,它始终位于从不执行的分派中。它可能包含合法软件的修改部分,例如以相同方式混淆的WinSpy ++(请参见下面的示例)。

Stantinko’s的新型密码器具有独特的混淆技术

图10.包含合法WinSpy ++代码调度中反混淆的死码部分

Stantinko’s的新型密码器具有独特的混淆技术

图11. WinSpy ++正式发行版中的等效代码部分(如图10所示)

无效代码

即使在展开操作之后,也有一些根本没有目的的代码部分与“实际代码”混合在一起。这可能意味着使分析更加混淆或绕过行为检测。

Stantinko’s的新型密码器具有独特的混淆技术

图12.标记的部分是冗余代码,它遍历前两个磁盘卷名称,然后对返回的值不执行任何操作

由于代码不难阅读,因此我们决定不采取任何措施并在此时进行分析。

通常,要优化此无效代码:例如,我们必须生成包含所有存在的Windows API调用的分离切片。切片标准将由每个分离切片中的所有调用参数组成。

随后,我们将在受控环境中使用准备好的调用堆栈执行切片,并且如果切片执行以下至少一项操作,我们将认为该切片可以正常工作:

  • 对基础操作系统进行一些更改
  • 要求知道函数参数或全局变量的初始值
  • 为函数参数或全局变量赋值
  • 直接影响功能的整体控制流程
本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!
请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!