使用独特的混淆技术:Stantinko的新挖矿软件分析
2020-03-26 09:51:15 Author: www.4hou.com(查看原文) 阅读量:174 收藏

概述

近期,我们在分析新型挖矿模块的过程中,发现Stantinko僵尸网络背后的攻击者新使用了几种混淆技术,其中所使用的一些技术我们还没有找到公开的分析。在本文中,我们将会剖析这些技术,并描述针对其中某些技术的可能对策。

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

1、字符串混淆:构造有意义的字符串,并且仅在使用它们时再出现在内存中。

2、控制流混淆:将控制流转换为难以阅读的形式,如果不进行大量分析,将无法预测基本块的执行顺序。

3、死代码:添加从来不会被执行的代码,其中还包含从未调用的导出,目的是使文件看起来更像是合法文件,以防止检测。

4、无效代码:添加已执行的代码,但对整体功能没有实质影响,它旨在绕过行为检测。

5、死字符串和资源:添加不影响功能的资源和字符串。

在上面这些技术中,最值得关注的是字符串混淆和控制流混淆,我们将在接下来对其进行详细介绍。

字符串混淆

在模块中嵌入的所有字符串都于实际功能无关。其来源是未知的,它们是作为构建实际使用字符串的编译块,或者是完全不使用的字符串。

恶意软件实际会使用的字符串会在内存中生成,以避免基于文件的检测,并阻止研究人员对其进行分析。恶意软件作者通过重新排列诱饵字符串的字节(嵌入在模块中的字节),并使用标准函数进行字符串操作来实现,这些函数包括:strcpy()、strcat()、strncat()、strncpy()、sprintf()、memmove()以及其Unicode版本。

由于要在特定函数中使用的所有字符串都使用在该函数的开始部分按顺序进行编译,因此可以模拟函数的入口点,并提取显示这些字符串的可打印字符序列。

下面是字符串混淆的示例,在图中有7个突出显示的诱饵字符串,例如以红色标记的字符串将会生成字符串“NameService”。

Figure-1-2-768x951.png

控制流平坦化

控制流平坦化(Control-flow flattening)是一种混淆技术,可以用于阻止分析并避免检测。

通过将单个函数拆分为基本块,可以实现最为常见的控制流平坦化。随后,可以将这些块分别放在循环内的switch语句中,每个部分恰好由一个基本块组成。使用一个控制变量,来确定应该在switch语句中执行哪个基本块。其初始值在循环之前分配。

这些基本块都会分配有一个ID,并且控制变量始终保存要执行的基本块的ID。

所有基本块都将控制变量的值设置为其继承者的ID。一个基本块可以有多个可能的继承者,但也可以直接指定某一个继承者。

常规控制流平坦化循环结构:

Figure-2-1.png

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

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

1、代码在源代码级别被平坦化,这也意味着编译器可以在生成的二进制文件中引入一些异常。

2、控制变量在控制块中递增,而不是在基本块中递增。

3、调度程序中包含多个基本块。基本块的划分可能是分开的,即每个基本块都完全属于某一个调度程序,但有些时候调度程序会互相影响,不同的调度程序会共享一些基本块。

4、平坦化循环可以嵌套和连续。

5、可以将多个函数进行合并。

这些特征表明,Stantinko为这种技术引入了新的障碍,我们必须要对其进行分析,以弄清楚其最终Payload。

Stantinko中的控制流平坦化

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

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

Stantinko的控制流平坦化循环中的规则结构:

Figure-3-2.png

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

通过合并多个函数的代码以创建控制流平坦化循环时,会基于调用哪个原始函数,以不同的值初始化合并后函数中的控制变量,并将控制变量的值作为参数传递给结果函数。

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

请务必注意,我们在某些平坦化的循环中观察到了多个异常情况,这使得反混淆过程的自动化变得更加困难。其中的大多数似乎都是由编译器生成的。这让我们相信,在编译以前,这部分内容就已经应用了控制流平坦化混淆。

我们发现了以下的异常,这些异常可能是单独出现,也有可能是组合出现:

1、有些调度程序可能只是死代码,它们将永远不会执行。可以参考后面“控制流平坦化循环内的死代码”一节中的示例。

2、调度程序的基本块可以结合在一起,这意味着其中可以包含共享的代码。

包含共享代码的调度程序的平坦化循环结构:

Figure-4-1.png

3、从调度程序直接跳转到平坦化循环外部,紧跟在结尾后面,并且返回从函数返回的块。

平坦化循环的结构,其调度程序直接中断了循环:

Figure-5-2.png

4、可以有多个尾部,也可以不包含尾部。在后一种情况下,控制变量会在每次调度程序结束时递增。

没有任何尾部(左图)和有多个尾部(右图)的平坦化结构:

Figure-6-2-768x297.png

5、头部并没有在一开始就是跳转表。取而代之的是,可以有多个跳转表,并且在跳转表之前有一系列分支条件,以二进制方式搜索正确的调度程序。

6、控制变量的值可以在调度程序内部使用,这意味着即使在反混淆的代码中,也必须保留或计算控制值。

EDI寄存器包含传递给EAX并在调度程序内部使用的控制变量,调度程序以红色突出显示:

Figure-7-2-768x321.png

7、有时,尾部包含对恢复寄存器和局部变量正确值的关键指令。在反混淆的过程中,我们移除了尾部,因此,即使它们不属于这些部分,我们也必须确保在每次调度之后执行这些指令。

8、在某些情况下,会出现没有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,可能发生两种比较少见的情况:二者都会导致循环过早停止。这种情况发生在调度:

(1)从函数返回时;

(2)指向next_block时。

最后,当循环结束时,将先前调度的最后一个基本块重定向到平坦化循环之外的第一个基本块。

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

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

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

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

混淆(右侧)与反混淆(左侧)控制流程图的示例:

Figure-9-768x257.png

后续改进

上述方法仅在汇编级别上运行,但这并不足以让我们的反编译过程完全自动化。

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

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

可以通过使用中间语言(intermediate representation)来进一步改进这种方法,这种中间语言表示提供了一些优化技术,这些技术除了其他事项之外,将消除编译器生成的大多数异常情况。从而允许反混淆函数自动识别编译器所需的参数。

与此同时,我们也可以使用特定的中间语言来进行模糊和反混淆处理。而其中的后者主要用于重新排列基本块。

这种方式的缺点在于,生成的代码也位于中间语言中,这意味着必须同时使用中间语言进行连续的分析。可供使用的中间语言工具及其功能的数量可能会非常有限,特别是在可视化方面。因此,很难去分析比较复杂的样本,特别是当存在其他混淆方式的时候。同时,我们也将无法执行结果代码。

死代码

“死代码”是指永远不会执行的代码,或者对功能没有整体影响的代码。该恶意软件的死代码大部分位于平坦化的循环之中(已经通过上述的反混淆函数实现清除),但其中还包含未使用的导出,并且无法将未使用的导出与合法的导出区分开来。

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

包含合法WinSpy++代码的分支中,死代码的反混淆部分:

Figure-10-768x620.png

WinSpy++正式发布版本中的等效代码部分:

Figure-11.png

无实际操作的代码

即使是在进行平坦化操作之后,其中也有一些无实际操作的代码与实际代码混合在一起。这可能是为了进一步混淆,或者绕过行为检测。

其中标记的部分是冗余的代码,该代码遍历前两个磁盘卷名,然后对返回的值不执行任何操作:

Figure-12.png

由于代码不难阅读,因此我们决定不做任何处理,并进行分析。

一般而言,为了优化这段无实际操作的代码,我们需要生成所有存在Windows API调用的片段。其中的片段将由每个独立片段中的所有调用参数组成。

随后,我们将在可控的环境中,使用准备好的调用堆栈进行分段,如果得到的片段可以执行以下至少一项操作,我们就可以认为这个片段可以正常工作:

1、对底层操作系统进行一些更改;

2、需要已知函数的参数或全局变量的初始值;

3、分配函数参数或全局变量的值;

4、直接影响函数的整体控制流。

总结

Stantinko僵尸网络背后的攻击者正在不断改进和开发新的模块,这些模块中通常包括不常见并且非常新鲜的技术。

我们在之前已经描述了该攻击者的新型加密货币挖掘模块。有关该模块的功能分析,请参考我们在2019年11月的分析文章。该模块中展现了几种混淆技术,旨在防止被检测并阻止研究人员的分析。我们分析了攻击者所使用的这些技术,并描述了一种针对这种技术进行模糊处理的可能方法。

请注意,有关威胁指标和MITRE ATT&CK分类的技术清单,请参考我们之前的文章,该文章中详细介绍了该挖矿软件的功能。

本文翻译自:https://www.welivesecurity.com/2020/03/19/stantinko-new-cryptominer-unique-obfuscation-techniques/如若转载,请注明原文地址:


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