首页
论坛
专栏
课程

[翻译]手把手静态分析FinSpy VM:第一部分,x86去混淆

2018-3-2 19:55 3127

[翻译]手把手静态分析FinSpy VM:第一部分,x86去混淆

2018-3-2 19:55
3127

原文:http://www.msreverseengineering.com/blog/2018/1/23/a-walk-through-tutorial-with-code-on-statically-unpacking-the-finspy-vm-part-one-x86-deobfuscation

1.介绍

通常当我发布有关分析VM保护的内容时会介绍新的技术。过去的例子包括:

Writing an IDA processor module to unpack a VM

Logging VM execution with DLL injection

Compiler-based techniques to unpack commercial-grade VMs

Abstract interpretation-based techniques to deobfuscate control flow

Automated generation of peephole superdeobfuscators

Program synthesis-based deobfuscation of metamorphic behavior in VM handlers

今天的文章重点有所不同。我不打算展示任何新的技术。相反,我将逐步介绍我分析FinSpy VM的过程,包括我整个过程中的想法,我使用的方法和代码,以及我所做笔记的摘要。我鼓励感兴趣的读者获取样本并自己完成分析过程。

我发表这个系列博客的原因有三个:

1.我认为如果每个病毒分析师都能够在他们遇到FinSpy时分析FinSpy病毒的VM(出于显而易见的原因)对安全防御社区来说有着最大的利益。

2.现在逆向工程缺少动手实践的教程。我很幸运地在这样的教程很常见时开始学习逆向工程,并且它们对于帮助我学习这些技术来说是无价的。幻灯片适用于大型分析,但对于小型分析,为了关注我们的人我们还是用教程的形式。

3.在过去的五年中发表的关于混淆的资料变得极其深奥,尤其是关于虚拟化混淆的。对于那些不了解硕士学位(或以上)具有的程序分析的知识的人来说,这些资料中的大部分基本上是无法理解的。我想证明,对于一些现代混淆技术,更简单的技术仍然可以产生令人惊讶的快速和有用的结果。(如果您想了解更多基于程序分析的反混淆方法,目前有公开的我的基于SMT的程序分析培训课程,该课程有超过200张关于现代反混淆的幻灯片,并且具有可以运行的包含文档的代码。)

2.开始

我在了解到一个带有VM的新FinSpy样本公开的时候做的第一件事当然是获得样本。VirusTotal给了SHA256,我从Hybrid-Analysis中获得了相应的样本

下一步是将样本加载到IDA中。导航栏立即提示我该二进制文件被混淆了:


text节的前半部分大多是灰色和红色,分别表示数据和非功能代码。

text节的后半部分在导航栏中为灰色,表示数据转换为数组。

普通的二进制文件的text节几乎全部都是蓝色,表示函数中的代码。

3.分析WINMAIN:怀疑基于VM的混淆

IDA的自动分析功能确定该二进制文件是由Microsoft Visual C编译器编译的。我从确定WinMain函数开始。通常IDA会为我做这件事,但该位置的代码被混淆了,所以IDA没有将其命名或将其变为函数。我通过检查Visual C运行库中的___tmainCRTStartup函数并找到它调用用户编写代码的位置来找到WinMain。前几个指令类似于一个正常的函数开头,接下来立即就是混淆了。

.text:00406154     mov     edi, edi                 ; Normal prologue
.text:00406156     push    ebp                      ; Normal prologue
.text:00406157     mov     ebp, esp                 ; Normal prologue
.text:00406159     sub     esp, 0C94h               ; Normal prologue
.text:0040615F     push    ebx                      ; Save registers #1
.text:00406160     push    esi                      ; Save registers #1
.text:00406161     push    edi                      ; Save registers #1
.text:00406162     push    edi                      ; Save registers #2
.text:00406163     push    edx                      ; Save registers #2
.text:00406164     mov     edx, offset byte_415E41  ; Obfuscation - #1
.text:00406169     and     edi, 0C946B9C3h          ; Obfuscation - #2
.text:0040616F     sub     edi, [edx+184h]          ; Obfuscation - #3
.text:00406175     imul    edi, esp, 721D31h        ; Obfuscation - #4
.text:0040617B     stc                              ; Obfuscation
.text:0040617C     sub     edi, [edx+0EEh]          ; Obfuscation - #5
.text:00406182     shl     edi, cl                  ; Obfuscation
.text:00406184     sub     edi, [edx+39h]           ; Obfuscation - #6
.text:0040618A     shl     edi, cl                  ; Obfuscation
.text:0040618C     imul    edi, ebp                 ; Obfuscation
.text:0040618F     mov     edi, edi                 ; Obfuscation
.text:00406191     stc                              ; Obfuscation
.text:00406192     sub     edi, 0A14686D0h          ; Obfuscation

; ... obfuscation continues ...

.text:004065A2     pop     edx                      ; Restore registers
.text:004065A3     pop     edi                      ; Restore registers
上述序列中的混淆持续数百条指令,几乎所有指令都由对EDI寄存器的随机修改组成。我想知道EDI上的计算是完全无关紧要的垃圾指令还是会产生一个有用的值以及标记为#1,#3,#5和#6处的代码对内存的引用是否有意义。

对于第一个问题,注意输入这个序列的寄存器的值是未知的。毕竟我们在WinMain()中使用__cdecl调用约定,这意味着调用者不会在寄存器中传递参数。因此,#2处计算的值是不可预知的,并且可能会在不同的执行过程中发生变化。此外,#4处计算的值完全没有意义——堆栈指针的值将在运行时发生变化(并且对EDI的修改会覆盖#1-#3中计算的值)。

对于第二个问题,我浏览了混淆的代码,并注意到没有写入操作只有读取操作,所有这些都与刚刚描述的乱七八糟的指令交织在一起。最后,edi的原始值在标记为Restore registers末尾附近的位置从堆栈中弹出。所以我相当确信这一连串的指令什么都不做,对程序的状态没有任何有意义的改变。

之后是一个短的序列:

.text:004065A4     push    5A403Dh                  ; Obfuscation
.text:004065A9     push    ecx                      ; Obfuscation
.text:004065AA     sub     ecx, ecx                 ; Obfuscation
.text:004065AC     pop     ecx                      ; Obfuscation
.text:004065AD     jz      loc_401950               ; Transfer control elsewhere
.text:004065AD ; ---------------------------------------------------------------------------
.text:004065B3     db 5 dup(0CCh)
.text:004065B8 ; ---------------------------------------------------------------------------
.text:004065B8     mov     edi, edi
.text:004065BA     push    ebp
.text:004065BB     mov     ebp, esp
.text:004065BD     sub     esp, 18h

; ... followed by similar obfuscation to what we saw above ...
该序列只是将值5A403Dh压入堆栈,并将控制权交给loc_401950。(上面的sub ecx,ecx指令将zf标志寄存器设置为1,因此JZ指令将始终转移到loc_401950分支)。 

接下来我们看到指令db 5 dup(0CCh),后面跟着mov edi,edi。逆向工程师会将这些序列识别为Microsoft Visual C编译器对热补丁支持的实现。我认为在混淆之前,原始的二进制序列包含一个函数,它开始于第一个序列的地址并在db 5 dup(0CCh)序列之前结束。也就是说,我认为混淆器反汇编这个函数中的所有代码,用垃圾指令代替它们,在最后放置一个跳转到其它位置的分支,然后在下一个函数中做同样的事情。

这是一个好兆头,我们正在处理基于虚拟化的混淆器:它看起来像是把用普通编译器编译的二进制文件传递给覆盖原始指令的组件(而不是如同普通的壳一样仅仅就地加密它们)。 

4.了解有关VM入口点和进入VM之前的更多信息

再次回顾上一节中的第二个汇编代码序列:

.text:004065A4     push    5A403Dh                  ; Obfuscation - #1
.text:004065A9     push    ecx                      ; Obfuscation
.text:004065AA     sub     ecx, ecx                 ; Obfuscation
.text:004065AC     pop     ecx                      ; Obfuscation
.text:004065AD     jz      loc_401950               ; Transfer control elsewhere

由于(通过假设)来自这个函数的所有代码被替换为乱码,没有太多有意义的东西可供分析。我唯一的选择是检查位于最后一行的JZ指令跳转的目标:位于loc_401950处的代码。我在这个位置注意到的第一件事是loc_401950被引用了125次,几乎所有的引用形式为jz loc_401950,其中一些形式为jmp loc_401950。根据我分析的一些基于VM的混淆器的经验,这个位置符合被称为入口点的VM模式——虚拟CPU开始执行的部分。通常这个位置会在执行任何必要的设置之前将寄存器和标志保存到栈中,并最终开始执行VM指令。VM入口点通常需要一个指针或其它标识符来指向VM将要执行的字节码,也许就是上面序列中标号为#1的指令的值?让我们检查另一个到该位置的引用来验证:

.text:00408AB8     push    5A7440h ; #2
.text:00408ABD     push    eax
.text:00408ABE     sub     eax, eax
.text:00408AC0     pop     eax
.text:00408AC1     jz      loc_401950
除了将不同的值压入栈之外,通向入口点的另一个位置在功能上是相同的。这个值不是一个指针,它不对应于可执行文件内存中的地址。尽管如此,我们期望这个值负责以某种方式告诉VM入口点字节码的位置。

5.分析VM入口点代码

到目前为止,我们已经确定loc_401950是VM入口点,它由二进制文件中的125个分支位置定位,每个分支在跳转之前都会压入一个不同的非指针DWORD。我们开始分析该代码:

.text:00401950 loc_401950:
.text:00401950 0F 82 D1 02 00 00 jb loc_401C27
.text:00401956 0F 83 CB 02 00 00 jnb loc_401C27

我们立即看到一个明显而且众所周知的混淆形式。如果小于条件为真,则第一行跳转到loc_401C27,如果不小于条件为真,则第二行跳转到loc_401C27。也就是说,无论标志寄存器中的内容是什么,​​这两条指令都会将控制流转移到loc_401C27——我们不妨将这两条指令替换为jmp loc_401C27,因为效果是相同的。

继续在loc_401C27进行分析,我们看到了另一个基本思想相同的例子:

.text:00401C27 loc_401C27:
.text:00401C27 77 CD ja short loc_401BF6
.text:00401C29 76 CB jbe short loc_401BF6

这里我们有一个跳转到loc_401BF6的无条件分支,分成两个指令——如果大于跳转和如果小于或等于跳转,其中大于和小于或等于在逻辑上相反并且互斥。

之后在loc_401BF6处,有一条看起来合法的指令(push eax),后面跟着到loc_401D5C的条件跳转对。在那个位置,还有另外一个看似合法的指令(push ecx),后面跟着到loc_4019D2的条件跳转对。在那个位置,还有另外一个合法的指令(push edx),然后是另一个条件跳转对。很快就可以知道,显然每个合法的指令都散布在一两个条件跳转对之间——在整个二进制文件中有成百上千个这样的对。

虽然是一种非常古老而又不是特别复杂的混淆形式,但它仍然令人讨厌并减弱了反编译器的作用。正如我之前在TRANSPARENT DEOBFUSCATION WITH IDA PROCESSOR MODULE EXTENSIONS中所讨论的,IDA不会将到相同位置的两个相反的条件分支识别为到该位置的无条件分支。IDA认为第二个条件分支之后的地址必须包含代码,混淆器作者通过在第二个条件分支之后放置垃圾字节来利用这一点,然后使反汇编程序生成垃圾指令,由于X86的可变长度编码方案,这些指令可能会重叠并遮挡分支之后的合法指令。(请注意,IDA不应该为这个难题负责——这些问题在普通的基于冯诺伊曼的程序执行模型下是不可判定的)。结果导致许多合法的指令在这个过程生成的垃圾指令中丢失了,按照通常的静态分析的方式去除混淆需要花费大量的时间来手动地取消定义那些无用的指令,并重新定义合法的指令。

6.反混淆条件分支混淆:理论与实践

如前所述取消定义和重新定义指令将浪费时间,所以我们不这样做。说起IDA处理器模块,一旦明确了这种模式在每个合法的非控制流指令之间重复出现,我就有了编写IDA处理器模块扩展来自动消除混淆的想法。每当反汇编器遇到指令时,IDA处理器模块扩展使我们能够调用自己的函数。如果我们能够认识到我们反汇编的指令是一个条件分支,并且确定下面的指令包含与第一条分支目标相同条件相反的分支,我们可以将第一个分支替换为无条件分支,并且将第二个分支替换为NOP。

因此,第一个任务是提出一种方法来识别这种混淆的实例。看起来最简单的方法是使用字节模式识别来完成此操作。在反汇编指令之前执行的我的回调函数中,我可以检查原始字节以确定是否正在处理条件分支,如果是,则确定条件是什么以及分支目标。然后,我可以应用相同的逻辑来确定以下指令是否为条件分支并确定其条件和目标。如果条件相反并且目标相同,我们就找到了一个混淆的实例并可以使其无效。

在实践中,这比听起来更简单!回想一下上面的第一个例子,为便于阅读,在这里重复:

.text:00401950 0F 82 D1 02 00 00 jb loc_401C27
.text:00401956 0F 83 CB 02 00 00 jnb loc_401C27

这两条指令中的每条指令都是六个字节。它们都以字节0F(x86两字节转义操作码)开始,之后是80到8F范围内的一个字节,然后是一个DWORD将指令的结尾到分支目标的距离编码。作为x86指令编码的一个偶然巧合,相反的条件分支采用相邻字节进行编码。即82代表JB的长形式,83代表JNB的长形式。当且仅当它们的第二操作码字节最低位(即0x82^0x83==0x01)不同时,两个长分支具有相反的条件。还要注意,第二个操作码字节之后的DWORD恰好相差6——1个长条件分支指令的长度。

这就是对于长条件分支全部需要知道的,短条件分支如上面的第二个示例所示,为便于阅读,在这里重复:

.text:00401C27 77 CD ja short loc_401BF6
.text:00401C29 76 CB jbe short loc_401BF6

几乎相同的知识适用于这些序列。两条指令的第一个字节都在0x70到0x7F的范围内,相反的条件具有不同的最低位,第二个字节相差正好是2——1个短条件分支指令的长度。

7.反混淆条件分支混淆:实现

我从TRANSPARENT DEOBFUSCATION WITH IDA PROCESSOR MODULE EXTENSIONS中复制粘贴了一些我的代码。我首先删除了特定于上次保护的所有代码。在此期间我升级到了IDA 7.0,IDA 7.0对以前的API做出了突破性的改变,我不得不做一些修改——即将自定义分析函数deobX86Hook::custom_ana(self)重命名为deobX86Hook::ev_ana_insn(self,insn),并用insn.ea替换对idaapi.cmd.ea的每个引用。另外,我之前的例子只有在二进制文件的MD5与特定值匹配时才会运行,所以我用来自IDA数据库的md5覆盖了原来的。

我不得不改变custom_ana的逻辑,结果比我上次的处理器模块扩展更简单。以下是用于识别和反混淆短条件分支混淆的逻辑:

b1 = idaapi.get_byte(insn.ea)
if b1 >= 0x70 and b1 <= 0x7F:
    d1 = idaapi.get_byte(insn.ea+1)
    b2 = idaapi.get_byte(insn.ea+2)
    d2 = idaapi.get_byte(insn.ea+3)
    if b2 == b1 ^ 0x01 and d1-2 == d2:
        # Replace first byte of first conditional with 0xEB, the opcode for "JMP rel8"
        idaapi.put_byte(insn.ea, 0xEB) 
        # Replace the following instruction with two 0x90 NOP instructions
        idaapi.put_word(insn.ea+2, 0x9090)
对长条件分支进行反混淆几乎相同,详情请参阅代码

8.欣赏我的作品,清理数据库

现在我将处理器模块扩展复制到%IDA%\ plugins并重新加载样本。它已经工作了!VM入口点已被替换为:

.text:00401950 loc_401950:
.text:00401950 jmp loc_401C27

尽管导航栏仍然大部分是红色很不好看,但我立即注意到text节中间有一个很大的函数:


在图形模式中看起来有点丑,并不像未混淆的X86代码看起来那么舒服,但考虑到这一步在整个分析过程中是多么微不足道,我以后就会用它来代替混淆的版本了。红色节点表示位于白色节点中有效位置之上的错误指令。IDA的图形算法包括图形显示中函数块的物理连续区域内的任何代码,无论它们是否具有传入代码交叉引用,都可能使异常处理程序显示得更好。如果你想编写一个插件来删除这些内容并去掉JMP指令,那将很容易。


接下来,我对导航栏text节中的灰色区域感到好奇。(这些区域表示已定义的数据项,与text节中的混淆代码混合在一起)。我认为那里保存的数据很可能与混淆器相关。我花了一些时间查看灰色区域,并在定义的函数后立即发现:

.text:00402AE0     dd offset loc_402CF2
.text:00402AE4     dd offset loc_402FBE

; ... 30 similar lines deleted ...

.text:00402B60     dd offset loc_4042DC
.text:00402B64     dd offset loc_40434D
34个偏移,每个偏移都包含代码。这些可能是VM指令处理程序。让我们用IDAPython单行程序将它们转换为函数:
for pFuncEa in xrange(0x00402AE0, 0x00402B68, 4):
    idaapi.add_func(idaapi.get_long(pFuncEa))
现在text节的大块连续导航栏是蓝色的。此时,我意识到我忘记了在原始调度程序位置创建一个函数,所以我手动完成了这个工作,现在导航栏如下:


Hex-Rays没有很好处理我们刚刚定义的函数,因为它们最初是用汇编语言编写的,并且使用的指令和构造并不是通常编译器产生的那些。我并不怪Hex-Rays,我希望它们能够继续优化基于标准编译器的用例,而不是像这样奇怪的用例。

最后,我按下PageDown在text节滚动查看剩下的内容。其中大部分是VM入口点,就像我们在第3部分中看到的那样。有几个函数看起来像是由编译器生成的。

所以现在我们已经知道了text节的内容——一个有34个处理程序,125个以上的虚拟化函数和一些未虚拟化的函数的VM。下次我们来看看VM。

9.预告第2部分和第3部分,以及更多

第二部分中我花了几个小时分析VM入口点和VM指令处理程序。接下来,通过静态分析,我获得了样本中包含的VM程序的字节码。然后我为VM写了一个反汇编程序。

第三部分中通过反汇编的VM字节码,我编写了一个简单的基于模式的反混淆器。之后,我重新生成了X86机器代码,这并不是非常困难,但比我原本预期的要复杂得多。

之后,我将X86机器代码重新插入原始二进制文件并进行分析。事实证明,它是两个第二阶段二进制文件中相当复杂的一个dropper。这在系统内部非常重要,并且有一些技巧没有被广泛记录,所以我可能会将它们中的一个或多个作为单独的条目发布,并且/或者我会发布对整个dropper的分析。

最后,我分析了——或者说,还在分析第二阶段的二进制文件。他们可能会或可能不会值得发表文章。




[公告]LV6级以上的看雪会员可以免费获得《2019安全开发者峰会》门票一张!!

最后于 2019-2-1 18:39 被admin编辑 ,原因:
最新回复 (3)
pxhb 2 2018-3-2 21:36
2
0
虽然看不懂,但感觉好厉害的样子,先收藏有时间仔细看看
Alfik 2018-3-11 05:17
3
0
再次感谢您的翻译!  如何找到原始的可执行文件? 
最后于 2018-3-11 05:18 被Alfik编辑 ,原因:
houjingyi 9 2018-3-26 14:13
4
0
Alfik 再次感谢您的翻译!&nbsp; 如何找到原始的可执行文件?&nbsp;
文中给了Hybrid-Analysis和VirusTotal的链接,这里就不方便发了
游客
登录 | 注册 方可回帖
返回