原文:http://www.msreverseengineering.com/blog/2018/1/23/a-walk-through-tutorial-with-code-on-statically-unpacking-the-finspy-vm-part-one-x86-deobfuscation
通常当我发布有关分析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张关于现代反混淆的幻灯片,并且具有可以运行的包含文档的代码。)
我在了解到一个带有VM的新FinSpy样本公开 的时候做的第一件事当然是获得样本。VirusTotal给了SHA256 ,我从Hybrid-Analysis中获得了相应的样本 。
下一步是将样本加载到IDA中。导航栏立即提示我该二进制文件被混淆了:
text节的前半部分大多是灰色和红色,分别表示数据和非功能代码。
text节的后半部分在导航栏中为灰色,表示数据转换为数组。
普通的二进制文件的text节几乎全部都是蓝色,表示函数中的代码。
IDA的自动分析功能确定该二进制文件是由Microsoft Visual C编译器编译的。我从确定WinMain函数开始。通常IDA会为我做这件事,但该位置的代码被混淆了,所以IDA没有将其命名或将其变为函数。我通过检查Visual C运行库中的___tmainCRTStartup函数并找到它调用用户编写代码的位置来找到WinMain。前几个指令类似于一个正常的函数开头,接下来立即就是混淆了。
对于第一个问题,注意输入这个序列的寄存器的值是未知的。毕竟我们在WinMain()中使用__cdecl调用约定,这意味着调用者不会在寄存器中传递参数。因此,#2处计算的值是不可预知的,并且可能会在不同的执行过程中发生变化。此外,#4处计算的值完全没有意义——堆栈指针的值将在运行时发生变化(并且对EDI的修改会覆盖#1-#3中计算的值)。
对于第二个问题,我浏览了混淆的代码,并注意到没有写入操作只有读取操作,所有这些都与刚刚描述的乱七八糟的指令交织在一起。最后,edi的原始值在标记为Restore registers末尾附近的位置从堆栈中弹出。所以我相当确信这一连串的指令什么都不做,对程序的状态没有任何有意义的改变。
之后是一个短的序列:
接下来我们看到指令db 5 dup(0CCh),后面跟着mov edi,edi。逆向工程师会将这些序列识别为Microsoft Visual C编译器对热补丁支持的实现。我认为在混淆之前,原始的二进制序列包含一个函数,它开始于第一个序列的地址并在db 5 dup(0CCh)序列之前结束。也就是说,我认为混淆器反汇编这个函数中的所有代码,用垃圾指令代替它们,在最后放置一个跳转到其它位置的分支,然后在下一个函数中做同样的事情。
这是一个好兆头,我们正在处理基于虚拟化的混淆器:它看起来像是把用普通编译器编译的二进制文件传递给覆盖原始指令的组件(而不是如同普通的壳一样仅仅就地加密它们)。
再次回顾上一节中的第二个汇编代码序列:
由于(通过假设)来自这个函数的所有代码被替换为乱码,没有太多有意义的东西可供分析。我唯一的选择是检查位于最后一行的JZ指令跳转的目标:位于loc_401950处的代码。我在这个位置注意到的第一件事是loc_401950被引用了125次,几乎所有的引用形式为jz loc_401950,其中一些形式为jmp loc_401950。根据我分析的一些基于VM的混淆器的经验,这个位置符合被称为入口点的VM模式——虚拟CPU开始执行的部分。通常这个位置会在执行任何必要的设置之前将寄存器和标志保存到栈中,并最终开始执行VM指令。VM入口点通常需要一个指针或其它标识符来指向VM将要执行的字节码,也许就是上面序列中标号为#1的指令的值?让我们检查另一个到该位置的引用来验证:
到目前为止,我们已经确定loc_401950是VM入口点,它由二进制文件中的125个分支位置定位,每个分支在跳转之前都会压入一个不同的非指针DWORD。我们开始分析该代码:
我们立即看到一个明显而且众所周知的混淆形式。如果小于条件为真,则第一行跳转到loc_401C27,如果不小于条件为真,则第二行跳转到loc_401C27。也就是说,无论标志寄存器中的内容是什么,这两条指令都会将控制流转移到loc_401C27——我们不妨将这两条指令替换为jmp loc_401C27,因为效果是相同的。
继续在loc_401C27进行分析,我们看到了另一个基本思想相同的例子:
这里我们有一个跳转到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不应该为这个难题负责——这些问题在普通的基于冯诺伊曼的程序执行模型下是不可判定的)。 结果导致许多合法的指令在这个过程生成的垃圾指令中丢失了,按照通常的静态分析的方式去除混淆需要花费大量的时间来手动地取消定义那些无用的指令,并重新定义合法的指令。
如前所述取消定义和重新定义指令将浪费时间,所以我们不这样做。说起IDA处理器模块,一旦明确了这种模式在每个合法的非控制流指令之间重复出现,我就有了编写IDA处理器模块扩展来自动消除混淆的想法。每当反汇编器遇到指令时,IDA处理器模块扩展使我们能够调用自己的函数。如果我们能够认识到我们反汇编的指令是一个条件分支,并且确定下面的指令包含与第一条分支目标相同条件相反的分支,我们可以将第一个分支替换为无条件分支,并且将第二个分支替换为NOP。
因此,第一个任务是提出一种方法来识别这种混淆的实例。看起来最简单的方法是使用字节模式识别来完成此操作。在反汇编指令之前执行的我的回调函数中,我可以检查原始字节以确定是否正在处理条件分支,如果是,则确定条件是什么以及分支目标。然后,我可以应用相同的逻辑来确定以下指令是否为条件分支并确定其条件和目标。如果条件相反并且目标相同,我们就找到了一个混淆的实例并可以使其无效。
在实践中,这比听起来更简单!回想一下上面的第一个例子,为便于阅读,在这里重复:
这两条指令中的每条指令都是六个字节。它们都以字节0F(x86两字节转义操作码)开始,之后是80到8F范围内的一个字节,然后是一个DWORD将指令的结尾到分支目标的距离编码。作为x86指令编码的一个偶然巧合,相反的条件分支采用相邻字节进行编码。即82代表JB的长形式,83代表JNB的长形式。当且仅当它们的第二操作码字节最低位(即0x82^0x83==0x01)不同时,两个长分支具有相反的条件。还要注意,第二个操作码字节之后的DWORD恰好相差6——1个长条件分支指令的长度。
这就是对于长条件分支全部需要知道的,短条件分支如上面的第二个示例所示,为便于阅读,在这里重复:
几乎相同的知识适用于这些序列。两条指令的第一个字节都在0x70到0x7F的范围内,相反的条件具有不同的最低位,第二个字节相差正好是2——1个短条件分支指令的长度。
我从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的逻辑,结果比我上次的处理器模块扩展更简单。以下是用于识别和反混淆短条件分支混淆的逻辑:
现在我将处理器模块扩展复制到%IDA%\ plugins并重新加载样本。它已经工作了!VM入口点已被替换为:
尽管导航栏仍然大部分是红色很不好看,但我立即注意到text节中间有一个很大的函数:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2019-2-1 18:39
被admin编辑
,原因: