首页
论坛
专栏
课程

[翻译]手把手静态分析FinSpy VM:第三部分第一阶段,反混淆FINSPY VM字节码程序

2018-3-29 19:13 1552

[翻译]手把手静态分析FinSpy VM:第三部分第一阶段,反混淆FINSPY VM字节码程序

2018-3-29 19:13
1552

原文:http://www.msreverseengineering.com/blog/2018/2/21/wsbjxrs1jjw7qi4trk9t3qy6hr7dye

注意:您可以在此处找到第三部分四个阶段的介绍。

1.介绍

在本系列的第一部分,我们分析了FinSpy VM的x86实现的混淆,并编写了一个工具对其进行反混淆以便于分析。在本系列的第二部分,我们分析了VM指令集,编写了适用于该样本的反汇编程序,并获得了VM字节码。现在我们剩下的工作是去虚拟化:我们希望生成虚拟化之前的原始x86程序。

这个任务写起来相当长,所以我们将分为四个阶段,与我对FinSpy去虚拟化时所做的工作顺序相同(不要把冗长理解为困难)。我们的第一阶段将查看FinSpy VM字节码程序,发现涉及第二组指令的混淆并通过模式替换将其删除。

2.去虚拟化FINSPY:开始观察和方法

在这一过程中,我以反汇编形式首次列出了VM字节码程序。我开始检查VM字节码程序并找出简化字节码的方法,最终全面清除了第二组指令。

你可能希望看看最初的VM字节码反汇编结果,我们将在整个第一阶段逐步完善它。在这个过程中通过发现并应用简化措施我们将逐步获得新的VM字节码反汇编结果。我们将提供所有这些结果的链接。

2.1回顾FINSPY 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的指令集由三组指令组成:

  • 第一组:有条件和无条件跳转(即JMP和x86的所有16个条件跳转,如JZ,JNS和JP)。
  • 第二组:访问FinSpy VM的单个专用寄存器的VM指令。
  • 第三组:在其中包含原始x86指令的VM指令。

2.2关于去虚拟化的初步思考

在分析了VM的指令集并仔细阅读VM字节码程序之后,似乎第三组VM指令——那些带有x86机器代码块的指令很容易转换回x86。第三组指令中有三条VM指令:原始X86,直接调用和间接调用(实际上,后两个VM指令最终呈现出比预期更复杂的情况,直接调用是VM去虚拟化中最困难的)。

第一组有条件和无条件的跳转指令似乎也很容易转换回x86,因为它们的实现几乎与x86条件跳转在内部的实现方式相同。由于条件跳转使用相对位移来确定采取跳转的地址,唯一的挑战是确定每个去虚拟化指令的相对位置。因此,一旦我们确切知道跳转指令离目标有多远,我们可以简单地计算位移(的确,在实践中这很容易)。

第二组访问FinSpy VM的单个专用寄存器的VM指令集是通配符。虽然通过分析FinSpy VM我知道这些指令的原始功能,但在分析之前,我不知道如何使用这些指令,也不知道如何对它们进行去虚拟化。为了便于参考,下面从第二部分更详细的论述中总结出这些指令:

mov scratch, 0                      [Operand: none]
mov scratch, imm32                  [Operand: imm32]
shl scratch, imm32                  [Operand: imm32]
add scratch, imm32                  [Operand: imm32]
mov scratch, savedReg32             [Operand: index of savedReg32]
add scratch, savedReg32             [Operand: index of savedReg32]
mov savedReg32, scratch             [Operand: index of savedReg32]
mov dword ptr [scratch], savedReg32 [Operand: index of savedReg32]
mov scratch, dword ptr [scratch]    [Operand: none]
mov dword ptr [scratch], imm32      [Operand: imm32]
mov dword ptr [imm32], scratch      [Operand: imm32]
push scratch                        [Operand: none]
鉴于其它两组指令似乎是直截了当的,以及第二组指令中的迄今未知的性质,分析第二组指令的使用模式并制定一个策略将它们转换回x86机器代码似乎是去虚拟化我们的FinSpy VM程序的一个很好的首要任务(事实证明,这在实践中非常简单直观,没有真正的复杂性可言)。

3.第一步:调整输出以打印X86反汇编

在仔细阅读我的FinSpy VM反汇编程序的输出后,我认为知道现在被表示为原始机器代码字节的x86指令的反汇编是有意义的。例如,VM字节码反汇编中的前几条指令是:

0x000000: MOV SCRATCH, 0
0x000018: ADD SCRATCH, EBP
0x000030: ADD SCRATCH, 0x000008
0x000048: MOV SCRATCH, DWORD PTR [SCRATCH]
0x000060: MOV EAX, SCRATCH
0x000078: X86 [199, 0, 215, 1, 0, 0] ; <- x86 machine code
0x000090: X86 [184, 0, 240, 65, 0]   ; <- would prefer x86 assembly
0x0000a8: X86 [201]
0x0000c0: X86 [194, 4, 0]
明白后面四条x86指令正在做什么对于弄清楚VM程序中如何使用第二组指令(涉及SCRATCH寄存器,上面列表中的前五条指令)很有帮助。

对于这项任务,我很幸运已经用Python编写了x86反汇编程序和汇编程序库(作为我的基于SMT的程序分析培训课程课程样本的一部分,我以前已经向公众发布了此代码)。对于具有嵌入式x86机器代码的原始X86和间接调用这两个第三组VM指令,我修改了我的FinSpy VM反汇编程序以在打印时调用x86反汇编程序的功能。完善这些Python类的__str__()方法是微不足道的,例如:

        d = X86Decoder(self.Remainder[0:self.DataLen])
        i2container = d.Decode(0)
这样做后,VM字节码反汇编结果看起来要好点。一个例子如下所示,完整的反汇编结果可以在这里找到。

0x000000: MOV SCRATCH, 0
0x000018: ADD SCRATCH, EBP
0x000030: ADD SCRATCH, 0x000008
0x000048: MOV SCRATCH, DWORD PTR [SCRATCH]
0x000060: MOV EAX, SCRATCH
0x000078: X86 mov dword ptr [eax], 1D7h ; <- new: x86 assembly, not machine code
0x000090: X86 mov eax, 41F000h
0x0000a8: X86 leave
0x0000c0: X86 ret 4h
快速浏览之后,x86汇编语言看起来像使用MSVC编译的普通C代码,尽管一些函数使用PUSHA和POPA等通常不是由普通编译器生成的指令,因此可能在虚拟化之前使用汇编语言编写。

4.第二步:对于SCRATCH寄存器访问使用基于模式的简化

有了更清晰的FinSpy VM字节码反汇编结果,我试图找出第二组专用寄存器指令的用途。找到这些指令很容易,它们都使用SCRATCH寄存器,在VM字节码反汇编中标记为SCRATCH,因此您只需搜索SCRATCH即可查看这些指令的位置(或者直接查看反汇编结果,第二组指令在我们样本的VM指令中占很大一部分)。

查看VM字节码程序的开始,我注意到一些将SCRATCH寄存器值设置为x86寄存器值的不同的模式 。例如重复上一节中示例的前两行,我们看到:

0x000000: MOV SCRATCH, 0
0x000018: ADD SCRATCH, EBP

这两条指令将SCRATCH寄存器设置为EBP。但是在其它地方,用于实现相同目标的VM指令有不同的模式。这是第二种模式:

0x03c270: MOV SCRATCH, 0
0x03c288: MOV SCRATCH, EDX

在FinSpy VM字节码反汇编结果的其它地方,我们看到更自然实现这一目标的单条指令,例如:

0x001b48: MOV SCRATCH, ECX

我错误地认为这是一种混淆形式:好像有几种方法将SCRATCH寄存器值设置为x86寄存器值,并且FinSpy在它们之间任意选择以在VM字节码程序中引入一些随机性。这种随机性可能会使模式识别更加费力。

我认为如果将两条指令合并为一条指令可以节省一些工作量(后来我意识到这不是一种混淆形式——FinSpy VM开发者只是懒)。

具体来说,我想编写一个搜索和替换规则,该规则将把以下VM指令:

MOV SCRATCH, 0      | MOV SCRATCH, 0      
ADD SCRATCH, REG32  | MOV SCRATCH, REG32

替换成:

MOV SCRATCH, REG32

使用Python的类型自省功能很容易完成模式识别和替换。由于我的第二部分的Python FinSpy VM反汇编器对这些单独的VM字节码指令类型使用了Python类型,因此我可以简单地调用isinstance函数在VM字节码反汇编结果中查找这些模式的实例。代码可以在函数FirstSimplify中找到(在代码中搜索该名称)。

例如,下面是识别MOV SCRATCH,0的代码。它包含两种不同的可能:将0移到SCRATCH的专用VM指令,以及当一个DWORD值为零时将其移到SCRATCH的VM指令。

def IsMovScratch0(insn):
    if isinstance(insn,MovScratch0):
        return True
    if isinstance(insn,MovScratchImm32):
        return insn.Imm32 == 0
    return False

识别和替换的完整代码如下所示。请花些时间来阅读它,因为本节其余部分中的所有模式匹配和替换都有类似的实现,为简洁起见,不会详细讨论。

# If the first VM instruction is "MOV SCRATCH, 0"...
if IsMovScratch0(i1):

    # Check to see if the second instruction is "MOV SCRATCH, REG32". If 
    # this function returns None, it wasn't. If it returns something other
    # than None, then it returns the register number.
    mr = IsMovScratchReg(i2)
                
    # Otherwise, check to see if the instruction was "ADD SCRACH, REG32",
    # and get the register number if it was (or None if it wasn't).
    if mr is None:
        mr = IsAddScratchReg(i2)
                
    # Did one of the two patterns match?
    if mr is not None:
                    
        # Yes: make a new VM instruction, namely "MOV SCRATCH, REG32" to 
        # replace the two instruction sequence we just matched. Use the same
        # file offset position from the first instruction in the sequence.
        newInsn = MovScratchDisp8([0]*INSN_DESC_SIZE, i1.Pos)
                    
        # Save the register number into the new instruction.
        newInsn.Disp8 = mr
                    
        # Use the same VM instruction key as for the first instruction.
        newInsn.Key = i1.Key
                    
        # Add the new VM instruction to the output list.
        newInsnArr.append(newInsn)

如果两种模式中的一种匹配,上面的代码会生成新的VM指令来替换现有的两种模式。由于每个VM指令都与一个位置和key唯一关联,我决定从匹配的模式中的第一条VM指令中复制这些属性,并直接删除第二条VM指令(我担心我可能会遇到其它VM指令会通过其key或EIP引用现在删除的第二条VM指令的情况,事实上,我必须在第三部分第四阶段对此进行说明)。

新的VM字节码反汇编结果可以在这里找到。

4.1MOV SCRATCH, REG32/PUSH SCRATCH模式

接下来,我继续检查VM字节码反汇编结果并查找涉及SCRATCH寄存器的更多模式。在找到前一个模式不久,我找到了下一个模式:

0x000120: MOV SCRATCH, ECX
0x000138: PUSH SCRATCH

这两条VM指令显然取代了x86指令push ecx。与上一步一样,我决定将其编入一种模式简化。每当我们看到下面的VM指令时:

MOV SCRATCH, REG32
PUSH SCRATCH

就用代表push reg32的单个VM指令替换它。这一步做起来与前一步几乎完全相同。我们使用Python的isinstance()函数来查找此VM指令模式。代码可以在函数 SecondSimplify中找到(在代码中搜索该名称)。为便于参考,这是我们如何识别MOV SCRATCH,REG32指令的:

def IsMovScratchReg(insn):
    if isinstance(insn,MovScratchDisp32):
        return insn.Disp32
    if isinstance(insn,MovScratchDisp8):
        return insn.Disp8
    return None

我们生成一个包含用于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函数(在代码中搜索该名称)如下所示:

def MakeRawX86(Pos, Key, x86Insn):
    # Create a FinSpy VM "Raw X86" instruction with dummy
    # content at the correct position (specified by Pos)
    newInsn = RawX86StraightLine([0]*INSN_DESC_SIZE, Pos)

    # Set the key to be the key from the first of the two
    # instructions of the two-instruction matched sequence
    newInsn.Key = Key

    # Encode the x86 instruction into machine code, store
    # the bytes in the FinSpy VM instruction
    newInsn.Remainder = EncodeInstruction(x86Insn)
    
    # Cache the length of the x86 instruction's machine code
    newInsn.DataLen = len(newInsn.Remainder)

    # Cache the textual disassembly for that instruction
    newInsn.X86 = str(x86Insn)
    
    # Return the FinSpy VM instruction just constructed
    return newInsn

上述函数返回的FinSpy VM原始X86指令对象用来替换VM指令MOV SCRATCH,REG32/PUSH SCRATCH。如代码所示,它的两个唯一标识特性(VM指令位置和VM指令key)是从该模式两条VM指令中的第一个复制而来的。

新的VM字节码反汇编结果可以在这里找到。

4.2MOV SCRATCH,REG32/MOV REG32,SCRATCH模式

下面又有一个类似的VM指令模式:

0x000228: MOV SCRATCH, ESP
0x000240: MOV ESI, SCRATCH

显然,这两条指令用来虚拟化x86指令mov esi,esp。更一般地,当我们看到如下形式的两个相邻的VM指令:

MOV SCRATCH, REG32_1
MOV REG32_2, SCRATCH

我们可以用x86指令mov reg32_2,reg32_1替换它。和前面的例子一样,在使用Python isinstance()定位此模式的实例之后,我们生成一个代表x86指令mov esi,esp的Python对象,并调用MakeRawX86函数(前一节详述)生成一个新的原始x86 VM指令来替换组成该模式实例的两条VM指令。代码可以在函数ThirdSimplify中找到(在代码中搜索该名称)。

新的VM字节码反汇编结果可以在这里找到。

4.3MOV SCRATCH,0/ADD SCRATCH,IMM32模式

继续在VM字节码程序中查找模式,我看到以下内容:

0x0037b0: MOV SCRATCH, 0
0x0037c8: ADD SCRATCH, 0x420344

这两条指令显然可以用单条指令MOV SCRATCH,0x420344替代。代码可以在函数FourthSimplify中找到(在代码中搜索该名称)。同样,编写这种模式识别和替换代码很简单,但与前面描述的模式替换不同,替换后的代码是第二组指令而不是第三组指令,因为输出涉及SCRATCH寄存器。

新的VM字节码反汇编结果可以在这里找到。

4.4MOV SCRATCH,IMM32/PUSH SCRATCH模式

对VM字节码反汇编结果的更多观察使我找到了以下指令:

0x006900: MOV SCRATCH, 0x000003
0x006918: PUSH SCRATCH

和前面的例子一样,我们可以用x86指令push 3替换这两条指令。代码可以在函数FifthSimplify中找到(在代码中搜索该名称)。

新的VM字节码反汇编结果可以在这里找到。

4.5内存地址模式

在完成上述替换之后,我们在剩下的第二组指令中看到更少的变化,并且它们的目的很快变得明显。每个剩余的使用SCRATCH寄存器的VM字节码指令簇由两个连续的部分组成:1)一系列FinSpy VM指令将内存地址加载到SCRATCH寄存器中,我将其称为内存地址模式;接下来是2)一系列FinSpy VM指令,利用刚刚生成的内存地址,用于从地址读取或写入数据,我将其称为内存访问模式。我们将在本节和随后的一节中讨论这两种模式。

为了展示一个有关内存地址模式和内存访问模式的例子,我们看看所有先前替换执行完毕后VM字节码程序的开头:

0x000000: MOV SCRATCH, EBP                 ; Memory !address!
0x000030: ADD SCRATCH, 0x000008            ; Memory !address!
0x000048: MOV SCRATCH, DWORD PTR [SCRATCH] ; Memory !access!
0x000060: MOV EAX, SCRATCH                 ; Memory !access!

前两个VM指令将SCRATCH寄存器设置为EBP+0x8。这两条指令构成内存地址模式。

后两个VM指令从SCRATCH寄存器中包含的内存地址读取DWORD,并将结果存储到EAX中。这两条指令构成内存访问模式。

显然,这四条VM指令可以被单条x86指令mov eax,[ebp+8]取代——这看起来非常像我们期望在函数开头附近看到的东西(刚才显示的四条VM指令是字节码程序中的第一条VM指令,位于x86函数的开始处)。

对于一个更复杂的例子:

0x03c270: MOV SCRATCH, EDX      ; Memory !address!
0x03c2a0: SHL SCRATCH, 0x000003 ; Memory !address!
0x03c2b8: ADD SCRATCH, EAX      ; Memory !address!
0x03c2d0: ADD SCRATCH, 0x000010 ; Memory !address!
0x03c2e8: MOV EAX, SCRATCH      ; Memory !access!

让我们追溯最后写入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内存表达式包含一个或多个添加在一起的以下元素:

  • 一个32位的基址寄存器(base register) 
  • 一个32位的比例因子寄存器(scale register),可选地乘以2,4或8
  • 32位位移(DWORD立即数)

通过查看VM字节码程序,所有内存地址序列具有相同的布局。

  • 如果正在使用索引寄存器(index register),则序列的第一个VM指令将SCRATCH寄存器设置为基址寄存器的值。 
  • 如果索引寄存器乘了比例因子,则下一条VM指令为SHL SCRATCH,IMM32,其中IMM32为1,2或3(分别将索引寄存器乘以2,4或8)。
  • 如果使用基址寄存器,则序列中的下一条指令将加上基址寄存器。
  • 如果使用32位位移,则下一条VM指令是ADD SCRATCH,IMM32。

所有这些元素都是可选的,但至少有一个必须存在。例如,如果内存地址是原始32位值(例如,dword ptr [401234h]),则只有ADD SCRATCH,IMM32 VM指令存在。如果内存表达式只包含一个寄存器(例如,dword ptr [eax]),则只有MOV SCRATCH,REG32 VM指令存在。使用多个元素的内存表达式将通过结合当前元素的VM指令来虚拟化。

4.6内存访问模式

刚刚描述的内存地址模式在SCRATCH寄存器中创建内存表达式后,接下来的一个或两个VM指令构成内存访问模式,并通过访问SCRATCH寄存器来指定如何使用内存地址。通过查看VM字节码程序,我发现有四种不同的VM指令序列用于内存访问模式。

4.6.1内存访问情况1:内存读取,存储到寄存器

第一种情况是从内存地址中读取,并将结果存储在一个寄存器中:

; ... memory address pattern before this ...
0x000048: MOV SCRATCH, DWORD PTR [SCRATCH]
0x000060: MOV EAX, SCRATCH

这对应于mov eax,dword ptr [memExpression],其中memExpression的细节由内存地址模式决定。

4.6.2内存访问情况2:内存读取,压入结果

第二种情况是从内存地址中读取,然后将结果压入栈:

; ... memory address pattern before this ...
0x004950: MOV SCRATCH, DWORD PTR [SCRATCH]
0x004968: PUSH SCRATCH

这对应于push dword ptr [memExpression],其中memExpression的细节由内存地址模式决定。

4.6.3内存访问情况3:内存写入来自寄存器的值

第三种情况将寄存器存储到由内存地址模式计算的内存位置中:

; ... memory address pattern before this ...
0x000ae0: MOV DWORD PTR [SCRATCH], EAX

这对应于mov dword ptr [memExpression],eax,其中memExpression的细节由内存地址模式决定。

4.6.4内存访问情况4:将地址存储到寄存器中

第四种情况不解引用内存位置,而是将结果保存到寄存器中:

; ... memory address pattern before this ...
0x000e28: MOV EAX, SCRATCH

这对应于lea eax,dword ptr [memExpression],其中memExpression的细节由内存地址模式决定。

4.7综合到一起:取消虚拟化内存引用

如上所述完全分析了对SCRATCH寄存器的其余引用之后,我编写了代码来检测内存地址模式和以上内存访问模式,分析它们,并将它们转换回x86机器代码。简化代码中的SixthSimplify函数识别内存地址模式,检查以上指令以确定内存访问模式,并组合这两条信息以确定内存地址模式和内存访问模式取代了哪个x86指令。接下来,该代码重建x86指令——作为FinSpy VM原始X86指令——最终用它代替包含内存地址模式和内存访问模式的VM指令。与以前一样,替换后的原始x86 FinSpy VM指令使用第一条VM指令中的key和位置属性作为替换后的指令的唯一属性。

首先,SixthSimplify调用函数DecodeAddressSequence。该函数确定FinSpy VM指令中的给定位置是否开始内存地址模式。如果不是,则该函数返回None。否则DecodeAddressSequence将从内存地址模式(基址寄存器,索引寄存器和可选比例因子以及32位位移)中提取详细信息,并使用它创建Mem32对象(我的Python x86库中表示32位内存表达式使用的类),然后它返回包括内存地址模式的VM指令的长度和Mem32对象。代码如下所示。 

# Given:
# * A list of FinSpy VM instructions in insnArr
# * An index within that list in idx
# 
# Determine if insnArr[idx] contains a memory address sequence.
# If not, return None. If so, create an x86 memory expression
# operand using my x86 library, and return it along with the
# number of instructions in the address sequence.

def DecodeAddressSequence(insnArr, idx):
    # Save position of index within insnArr
    oldIdx = idx
    
    # The first VM instruction in the sequence is usually "MOV SCRATCH, REG32".
    r1 = IsMovScratchReg(insnArr[idx])

    # Was it?
    if r1 is not None:

        # Yes, it was, so increment the current index
        idx += 1

        # Is the next VM instruction "SHL REG, [1/2/3]"?
        if isinstance(insnArr[idx],ShlScratchImm32):
            # Yes, copy the scale factor
            scaleFac = insnArr[idx].Imm32
            assert(scaleFac == 1 or scaleFac == 2 or scaleFac == 3)
            # Increment the current index
            idx += 1
        
        # Otherwise, there is no scale factor
        else:
            scaleFac = None

        # Is the next VM instruction "ADD SCRATCH, REG32"?
        r2 = IsAddScratchReg(insnArr[idx])
        if r2 is not None:
            # Yes, increment the current index
            idx += 1

        # Is the next VM instruction "ADD SCRATCH, IMM32"?
        disp32 = IsAddScratchImm32(insnArr[idx])
        if disp32 is not None:
            # Yes, increment the current index
            idx += 1
        
        # Make a memory expression from the parts, and return the length
        # of the memory address sequence
        return (idx-oldIdx, MakeMemExpr(r2, r1, scaleFac, disp32))
    
    # The second possibility is that the memory expression is a raw address.
    imm = IsMovScratchImm32(insnArr[idx])
    # Was it?
    if imm is not None:
        # Yes: make a memory expression from the address, and return the
        # length of the memory address sequence (namely, 1).
        return (1, MakeMemExpr(None,None,None,imm))

    # If we are here, neither of the memory address patterns matched, so
    # signal match failure.
    return None

接下来,一个名为DecodeAccessSequence的类似函数使用模式匹配来确定四种内存访问模式中的一种——mov reg32,[memExpr]/push [memExpr]/mov [memExpr],reg32/lea reg32,[memExpr]。该代码类似于刚才显示的用于解码内存地址模式的代码,所以不再重复。查看源代码以获取完整的详细信息。

最后,在识别出一个内存地址模式后跟一个内存访问模式之后,我们使用我们以前开发的用于重新生成x86机器代码的机制——即函数MakeRawX86——创建一个包含原始x86指令机器代码的FinSpy VM指令。我们用它来替换刚刚识别的内存地址模式和内存访问模式。

在运行了简化代码之后,第二组指令和SCRATCH寄存器从VM字节码反汇编结果中消失了。 新的VM字节码反汇编结果可以在这里找到。

5.第三步:纠正一个小错误

第二组VM指令消失后,第一组和第三组依然存在。然而,在解决这些问题之前,有一条VM指令不适用于其中任何一组。在生成我的初始VM字节码反汇编结果时,我错误地将FinSpy VM无条件跳转指令标识为导致故意崩溃的指令。我把它反汇编为CRASH。CRASH指令出现的频率很高,我的FinSpy VM字节码反汇编结果看起来很令人困惑。

0x005bb0: JNZ VM[0x005c88] (fallthrough VM[0x005bc8])
0x005bc8: X86 xor eax, eax
0x005be0: CRASH
0x005bf8: X86 mov eax, dword ptr [esi+4h]
0x005c70: CRASH
0x005c88: X86 push ebx

我在第二部分中详细讨论了这个问题——无条件跳转的实现与条件跳转有很大不同,并且涉及到动态代码生成,而这种生成仅用于第三组指令。更近一步的分析表明,这条指令的真正目的是作为无条件跳转而不是故意引起崩溃。在知道这些之后,我更新了反汇编器组件,将CRASH指令重命名为JMP,并打印这些新重命名的JMP指令的跳转目标。由此产生的VM字节码反汇编结果更清楚了:

0x005bb0: JNZ VM[0x005c88] (fallthrough VM[0x005bc8])
0x005bc8: X86 xor eax, eax
0x005be0: JMP VM[0x008df0]
0x005bf8: X86 mov eax, dword ptr [esi+4h]
0x005c70: JMP VM[0x008df0]
0x005c88: X86 push ebx

新的VM字节码反汇编结果可以在这里找到。

6.总结

在第一阶段中,我们查看了我们的样本的初始FinSpy VM字节码反汇编结果。我们通过将添加第三组指令中嵌入的x86机器代码的反汇编改进了结果。我们查看了VM字节码程序如何使用第二组指令。六种简单的模式替换足以完全删除第二组指令。之后,我们通过把CRASH指令改成无条件跳转来纠正输出结果。

在接下来的第三部分第二阶段,我们准备好尝试为FinSpy VM字节码程序进行第一次去虚拟化。我们最初的尝试结果不够好。我们在第三部分第三阶段研究这些不足,并在第三部分第四阶段第二次也是最后一次去虚拟化中试图纠正这些问题。



[公告]安全测试和项目外包请将项目需求发到看雪企服平台:https://qifu.kanxue.com

最后于 2018-3-30 16:10 被houjingyi编辑 ,原因:
最新回复 (1)
sudozhange 4 2018-3-29 19:36
2
0
支持!一直想分析这个样本,到对抗反反汇编就继续不下去了
游客
登录 | 注册 方可回帖
返回