原文:http://www.msreverseengineering.com/blog/2018/2/21/wsbjxrs1jjw7qi4trk9t3qy6hr7dye
注意:您可以在此处找到第三部分四个阶段的介绍。
在本系列的第一部分,我们分析了FinSpy VM的x86实现的混淆,并编写了一个工具对其进行反混淆以便于分析。在本系列的第二部分,我们分析了VM指令集,编写了适用于该样本的反汇编程序,并获得了VM字节码。现在我们剩下的工作是去虚拟化:我们希望生成虚拟化之前的原始x86程序。
这个任务写起来相当长,所以我们将分为四个阶段,与我对FinSpy去虚拟化时所做的工作顺序相同(不要把冗长理解为困难)。我们的第一阶段将查看FinSpy VM字节码程序,发现涉及第二组指令的混淆并通过模式替换将其删除。
在这一过程中,我以反汇编形式首次列出了VM字节码程序。我开始检查VM字节码程序并找出简化字节码的方法,最终全面清除了第二组指令。
你可能希望看看最初的VM字节码反汇编结果,我们将在整个第一阶段逐步完善它。在这个过程中通过发现并应用简化措施我们将逐步获得新的VM字节码反汇编结果。我们将提供所有这些结果的链接。
总结第二部分:FinSpy VM使用固定长度的指令编码,每个VM指令由一个长度为0x18字节的结构体表示。
FinSpy VM程序中的每条指令都有两个与其相关的唯一标识特征。首先,我们可以获得VM字节码指令数组中VM指令的原始位置。例如,第一条指令位于0x0。由于每条指令的长度均为0x18字节,因此第二条指令位于0x18,并且通常第N条指令位于字节码数组中0x18*N处。
指令的第二个标识特征是它的key,一个32位值(编码的FinSpy VM指令中的第一个DWORD),可用于定位特定的VM指令。具体来说,在进入VM之前,将控制权转交给FinSpy VM解释器之前执行的x86代码首先将一个DWORD大小的key压入栈。进入VM之后,FinSpy VM初始化代码加载由x86代码压入的DWORD大小的key,搜索VM字节码数组以找到具有该key的VM指令,然后开始解释从该位置开始的VM指令(事实证明,FinSpy VM指令key会导致去虚拟化中的一些复杂性,我们将在后面看到)。
大多数FinSpy VM指令被假定为按顺序执行。也就是说,一条VM指令完成后,指令处理程序的x86代码会将当前的VM EIP加上0x18,以进入下一条VM指令。VM控制流指令可能会有不同的行为,例如,条件跳转指令像它们对应的x86指令一样相对寻址(如果跳转VM指令会向VM EIP添加一个0x18的倍数的位移;如果没有跳转则在当前指令之后的0x18字节处的VM指令继续执行)。FinSpy VM也有一个用于直接调用的指令和一个用于间接调用的指令。这些调用的行为与期望的相同——在被调用的函数返回后,它们将返回地址压入栈以重新进入VM。
FinSpy VM的指令集由三组指令组成:
在分析了VM的指令集并仔细阅读VM字节码程序之后,似乎第三组VM指令——那些带有x86机器代码块的指令很容易转换回x86。第三组指令中有三条VM指令:原始X86,直接调用和间接调用(实际上,后两个VM指令最终呈现出比预期更复杂的情况,直接调用是VM去虚拟化中最困难的)。
第一组有条件和无条件的跳转指令似乎也很容易转换回x86,因为它们的实现几乎与x86条件跳转在内部的实现方式相同。由于条件跳转使用相对位移来确定采取跳转的地址,唯一的挑战是确定每个去虚拟化指令的相对位置。因此,一旦我们确切知道跳转指令离目标有多远,我们可以简单地计算位移(的确,在实践中这很容易)。
第二组访问FinSpy VM的单个专用寄存器的VM指令集是通配符。虽然通过分析FinSpy VM我知道这些指令的原始功能,但在分析之前,我不知道如何使用这些指令,也不知道如何对它们进行去虚拟化。为了便于参考,下面从第二部分更详细的论述中总结出这些指令:
在仔细阅读我的FinSpy VM反汇编程序的输出后,我认为知道现在被表示为原始机器代码字节的x86指令的反汇编是有意义的。例如,VM字节码反汇编中的前几条指令是:
对于这项任务,我很幸运已经用Python编写了x86反汇编程序和汇编程序库(作为我的基于SMT的程序分析培训课程的课程样本的一部分,我以前已经向公众发布了此代码)。对于具有嵌入式x86机器代码的原始X86和间接调用这两个第三组VM指令,我修改了我的FinSpy VM反汇编程序以在打印时调用x86反汇编程序的功能。完善这些Python类的__str__()方法是微不足道的,例如:
有了更清晰的FinSpy VM字节码反汇编结果,我试图找出第二组专用寄存器指令的用途。找到这些指令很容易,它们都使用SCRATCH寄存器,在VM字节码反汇编中标记为SCRATCH,因此您只需搜索SCRATCH即可查看这些指令的位置(或者直接查看反汇编结果,第二组指令在我们样本的VM指令中占很大一部分)。
查看VM字节码程序的开始,我注意到一些将SCRATCH寄存器值设置为x86寄存器值的不同的模式
。例如重复上一节中示例的前两行,我们看到:
这两条指令将SCRATCH寄存器设置为EBP。但是在其它地方,用于实现相同目标的VM指令有不同的模式。这是第二种模式:
在FinSpy VM字节码反汇编结果的其它地方,我们看到更自然实现这一目标的单条指令,例如:
我错误地认为这是一种混淆形式:好像有几种方法将SCRATCH寄存器值设置为x86寄存器值,并且FinSpy在它们之间任意选择以在VM字节码程序中引入一些随机性。这种随机性可能会使模式识别更加费力。
我认为如果将两条指令合并为一条指令可以节省一些工作量(后来我意识到这不是一种混淆形式——FinSpy VM开发者只是懒)。
具体来说,我想编写一个搜索和替换规则,该规则将把以下VM指令:
替换成:
使用Python的类型自省功能很容易完成模式识别和替换。由于我的第二部分的Python FinSpy VM反汇编器对这些单独的VM字节码指令类型使用了Python类型,因此我可以简单地调用isinstance函数在VM字节码反汇编结果中查找这些模式的实例。代码可以在函数FirstSimplify中找到(在代码中搜索该名称)。
例如,下面是识别MOV SCRATCH,0的代码。它包含两种不同的可能:将0移到SCRATCH的专用VM指令,以及当一个DWORD值为零时将其移到SCRATCH的VM指令。
识别和替换的完整代码如下所示。请花些时间来阅读它,因为本节其余部分中的所有模式匹配和替换都有类似的实现,为简洁起见,不会详细讨论。
如果两种模式中的一种匹配,上面的代码会生成新的VM指令来替换现有的两种模式。由于每个VM指令都与一个位置和key唯一关联,我决定从匹配的模式中的第一条VM指令中复制这些属性,并直接删除第二条VM指令(我担心我可能会遇到其它VM指令会通过其key或EIP引用现在删除的第二条VM指令的情况,事实上,我必须在第三部分第四阶段对此进行说明)。
新的VM字节码反汇编结果可以在这里找到。
接下来,我继续检查VM字节码反汇编结果并查找涉及SCRATCH寄存器的更多模式。在找到前一个模式不久,我找到了下一个模式:
这两条VM指令显然取代了x86指令push ecx。与上一步一样,我决定将其编入一种模式简化。每当我们看到下面的VM指令时:
就用代表push reg32的单个VM指令替换它。这一步做起来与前一步几乎完全相同。我们使用Python的isinstance()函数来查找此VM指令模式。代码可以在函数
SecondSimplify中找到(在代码中搜索该名称)。为便于参考,这是我们如何识别MOV SCRATCH,REG32指令的:
我们生成一个包含用于x86 PUSH操作的x86机器代码的FinSpy VM原始x86指令来替换这两条指令。我使用我的x86库为替换后的x86指令push reg32创建Python对象:在本例中,push ecx的Python对象可以用X86.Instruction([],XM.Push,X86.Gd(mr,True))创建。
在生成用于替换的x86指令对象后,我编写了一个函数来生成包含原始机器代码的FinSpy VM指令。简化程序中的MakeRawX86函数(在代码中搜索该名称)如下所示:
上述函数返回的FinSpy VM原始X86指令对象用来替换VM指令MOV SCRATCH,REG32/PUSH SCRATCH。如代码所示,它的两个唯一标识特性(VM指令位置和VM指令key)是从该模式两条VM指令中的第一个复制而来的。
新的VM字节码反汇编结果可以在这里找到。
下面又有一个类似的VM指令模式:
显然,这两条指令用来虚拟化x86指令mov esi,esp。更一般地,当我们看到如下形式的两个相邻的VM指令:
我们可以用x86指令mov reg32_2,reg32_1替换它。和前面的例子一样,在使用Python isinstance()定位此模式的实例之后,我们生成一个代表x86指令mov esi,esp的Python对象,并调用MakeRawX86函数(前一节详述)生成一个新的原始x86 VM指令来替换组成该模式实例的两条VM指令。代码可以在函数ThirdSimplify中找到(在代码中搜索该名称)。
新的VM字节码反汇编结果可以在这里找到。
继续在VM字节码程序中查找模式,我看到以下内容:
这两条指令显然可以用单条指令MOV SCRATCH,0x420344替代。代码可以在函数FourthSimplify中找到(在代码中搜索该名称)。同样,编写这种模式识别和替换代码很简单,但与前面描述的模式替换不同,替换后的代码是第二组指令而不是第三组指令,因为输出涉及SCRATCH寄存器。
新的VM字节码反汇编结果可以在这里找到。
对VM字节码反汇编结果的更多观察使我找到了以下指令:
和前面的例子一样,我们可以用x86指令push 3替换这两条指令。代码可以在函数FifthSimplify中找到(在代码中搜索该名称)。
新的VM字节码反汇编结果可以在这里找到。
在完成上述替换之后,我们在剩下的第二组指令中看到更少的变化,并且它们的目的很快变得明显。每个剩余的使用SCRATCH寄存器的VM字节码指令簇由两个连续的部分组成:1)一系列FinSpy VM指令将内存地址加载到SCRATCH寄存器中,我将其称为内存地址模式;接下来是2)一系列FinSpy VM指令,利用刚刚生成的内存地址,用于从地址读取或写入数据,我将其称为内存访问模式。我们将在本节和随后的一节中讨论这两种模式。
为了展示一个有关内存地址模式和内存访问模式的例子,我们看看所有先前替换执行完毕后VM字节码程序的开头:
前两个VM指令将SCRATCH寄存器设置为EBP+0x8。这两条指令构成内存地址模式。
后两个VM指令从SCRATCH寄存器中包含的内存地址读取DWORD,并将结果存储到EAX中。这两条指令构成内存访问模式。
显然,这四条VM指令可以被单条x86指令mov eax,[ebp+8]取代——这看起来非常像我们期望在函数开头附近看到的东西(刚才显示的四条VM指令是字节码程序中的第一条VM指令,位于x86函数的开始处)。
对于一个更复杂的例子:
让我们追溯最后写入EAX的值。前两个VM指令将EDX左移3,第三条VM指令添加EAX,第四条VM指令添加0x10。因此,写入EAX的表达式是EDX<<3+EAX+0x10。EDX<<3与EDX*8相同,因此我们的表达式是EDX*8+EAX+0x10。内存表达式的格式应该看起来很熟悉——它可以被编码为合法的x86 ModRM/32内存表达式[EAX+EDX*8+0x10]。这前四条VM指令就是我们所说的内存地址模式。第五条也是最后一条VM指令是内存访问模式的例子——我们只是简单地将内存地址本身存储到EAX中,而不是从此内存位置读取或写入此内存位置。
实际上,所有内存地址模式创建的内存地址使用X86 ModRM内存表达式进行编码都是合法的。X86 ModRM内存表达式包含一个或多个添加在一起的以下元素:
通过查看VM字节码程序,所有内存地址序列具有相同的布局。
所有这些元素都是可选的,但至少有一个必须存在。例如,如果内存地址是原始32位值(例如,dword ptr [401234h]),则只有ADD SCRATCH,IMM32 VM指令存在。如果内存表达式只包含一个寄存器(例如,dword ptr [eax]),则只有MOV SCRATCH,REG32 VM指令存在。使用多个元素的内存表达式将通过结合当前元素的VM指令来虚拟化。
刚刚描述的内存地址模式在SCRATCH寄存器中创建内存表达式后,接下来的一个或两个VM指令构成内存访问模式,并通过访问SCRATCH寄存器来指定如何使用内存地址。通过查看VM字节码程序,我发现有四种不同的VM指令序列用于内存访问模式。
4.6.1内存访问情况1:内存读取,存储到寄存器
第一种情况是从内存地址中读取,并将结果存储在一个寄存器中:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2018-3-30 16:10
被houjingyi编辑
,原因: