• 我们在哪一颗星上见过 ,以至如此相互思念 ;我们在哪一颗星上相互思念过,以至如此相互深爱
  • 我们在哪一颗星上分别 ,以至如此相互辉映 ;我们在哪一颗星上入睡 ,以至如此唤醒黎明
  • 认识世界 克服困难 洞悉所有 贴近生活 寻找珍爱 感受彼此

恶意代码样本分析:FinFisher

恶意代码样本分析 云涯 4年前 (2020-06-28) 1818次浏览

一.  基础定义

1. Spaghetti codes

面条式代码(spaghetti code)是非结构化和难以维护的源代码的贬义词组,广泛地解释。 意大利面条代码可能由多种因素引起,例如易变的项目要求,缺乏编程风格规则以及能力或经验不足。

代码过度使用GOTO语句而不是结构化编程结构,从而导致错综复杂和不可维护的程序,通常称为意大利面条代码。 这样的代码有一个复杂和纠结的控制结构,导致程序流程在概念上就像一碗意大利面,扭曲和纠结。在美国国家标准局1980年出版的一本出版物中,用“意大利面条”这个词来描述那些“分散和分散的文件”的较旧的节目。 意大利面代码也可以描述一种反向模式,在这种模式下,面向对象是以程序风格编写的,比如创建方法过于冗长和混乱的类,或者抛弃面向对象的概念,比如多态。这种形式的意大利面代码的存在可能会显着降低系统的可理解性。

2. 花指令

花指令是,由设计者特别构思,希望使反汇编的时候出错,让破解者无法清楚正确地反汇编程序的内容,迷失方向。经典的是,目标位置是另一条指令的中间,这样在反汇编的时候便会出现混乱。花指令有可能利用各种指令:jmp, call, ret的一些堆栈技巧,位置运算,等等。

 

二. 分析前情况汇总

1. 解决面条式代码与花指令

在分析FinFisher时,第一个需要解决的混淆问题是清除垃圾指令和“意大利面条代码”,这是一种旨在混淆反汇编程序的技术。意大利面条代码通过添加连续的代码跳转,使得程序流难以阅读,因此得名。下面是FinFisher意大利面条式代码的一个示例。

images

接下来通过下面代码进行分析

images

FinFisher使用了一种常见的反反汇编技术——通过将一个无条件跳转替换为两个互补的条件跳转来隐藏执行流。这些有条件的跳转都针对相同的位置,因此无论执行哪个跳转,都会产生相同的有效代码执行流。

条件跳转之后是垃圾字节。这是为了误导反汇编程序,反汇编程序通常不会识别它们是死代码,而是继续反汇编垃圾代码。

这个恶意软件的特殊之处在于它使用这种技术的方式。在我们分析的大多数其他恶意软件中,它只被使用了几次。然而,FinFisher在每条指令之后都使用这个技巧。

这种保护在欺骗反汇编程序时非常有效——代码的许多部分不能正确地反汇编。当然,在IDA Pro中不可能使用图形模式。我们的首要任务将是摆脱这种反反汇编保护。代码显然不是手动混淆的,而是使用自动化工具,我们可以观察到所有跳转对中的模式。

有两种不同类型的跳转对—32位偏移的近跳转和8位偏移的短跳转

两个条件近跳转(以dword作为跳转偏移量)的操作码都以0x0F字节开始;而第二个字节等于0x8*。在这两个跳转指令中只相差1位。这是因为x86操作码因为互补跳跃在数值上是连续的。例如,这种混淆方案总是将JE与JNE(0x0F 0x84 vs 0x0F 0x85操作码)配对,将JP与JNP(0x0F 0x8A vs 0x0F 0x8B操作码)配对,依此类推。

然后在这些操作码后面跟着一个32位的参数,指定到跳转目标的偏移量。由于两个指令的大小都是6字节,因此两个后续跳转中的偏移量相差正好6。(指令6字节的意思是OF 8E A1 FA FFFF是六个字节; fffffaa1与fffffa9b相差6字节,也就是保证jle与jg跳转到同一个地址)

下面的代码可以用来检测两个连续的跳转代码

def is_jump_near_pair(addr):
    jcc1 = Byte(addr+1)
    jcc2 = Byte(addr+7)
    # do they start like near conditional jumps?
    if Byte(addr) != 0x0F || Byte(addr+6) != 0x0F:
        return False
    # are there really 2 consequent near conditional jumps?
    if (jcc1 & 0xF0 != 0x80) || (jcc2 & 0xF0 != 0x80):
        return False
    # are the conditional jumps complementary?
    if abs(jcc1-jcc2) != 1:
        return False
    # do those 2 conditional jumps point to the same destination?
    dst1 = Dword(addr+2)
    dst2 = Dword(addr+8)
    if dst1-dst2 != 6:
        return False
    return True

短跳跃的解也是基于这种思想,只是常数不同。

images

短条件跳转的操作码等于0x7?,后跟一个字节–跳转偏移量。所以,当我们想要检测两个连续的、有条件的近跳时,我们必须寻找操作码:0x7?;偏移量;0x7 ? ± 1;偏移 -2。第一个操作码后面跟着一个字节,这两个字节在两个后续跳转中相差2(这也是两个指令的大小)。

例如,这段代码可以用来检测两个条件短跳转。

def is_jcc8(b):
    return b&0xF0 == 0x70
def is_jump_short_pair(addr):
    jcc1 = Byte(addr)
    jcc2 = Byte(addr+2)
    if not is_jcc8(jcc1) || not
        is_jcc8(jcc2):
        return False
    if abs(jcc2–jcc1) != 1:
        return False
    dst1 = Byte(addr+1)
    dst2 = Byte(addr+3)
    if dst1 – dst2 != 2:
        return False
    return True

在检测到其中一个条件跳转对之后,我们通过将第一个条件跳转补丁为无条件(对近跳转对使用0xE9操作码和并使用NOP指令修补其余的字节(0x90)。

def patch_jcc32(addr):
    PatchByte(addr, 0x90)
    PatchByte(addr+1, 0xE9)
    PatchWord(addr+6, 0x9090)
    PatchDword(addr+8,0x90909090)
def patch_jcc8(addr):
    PatchByte(addr, 0xEB)
    PatchWord(addr+2, 0x9090)

除了这两种情况之外,在某些地方,跳跃对可能由短跳跃和近跳跃组成,而不是由同一类别的两个跳跃组成。但是,这只在FinFisher示例中的少数情况下发生,可以手动修复。

随着这些补丁的制作,IDA Pro开始“理解”新代码,并准备(或至少几乎准备好)创建一个图。在这种情况下,我们还需要做一个改进:附加tails,即分配节点目的地相同。

带有跳转指令的节点的位置图。为此,我们可以使用IDA Python函数append_func_tail。

克服反反汇编技巧的最后一步是固定函数定义。跳转后的指令仍然可能是push ebp,在这种情况下IDA Pro(不正确)将其视为函数的开始并创建一个新的函数定义。在这种情况下,我们必须删除函数定义,创建正确的定义并再次追加尾部。

以下是完整函数,针对短跳跃与近跳跃进行修改的代码:

def patch_jcc32(addr):#addr是一个字节
    PatchByte(addr, 0x90)
    PatchByte(addr+1, 0xE9)
    PatchWord(addr+6, 0x9090)
    PatchDword(addr+8,0x90909090)
def is_jump_near_pair(addr):
    jcc1 = Byte(addr+1)
    jcc2 = Byte(addr+7)
    if Byte(addr) != 0x0F or Byte(addr+6) != 0x0F:
        return False
    if (jcc1 & 0xF0 != 0x80) or (jcc2 & 0xF0 != 0x80):
        return False
    if abs(jcc1-jcc2) != 1:
        return False
    dst1 = Dword(addr+2)
    dst2 = Dword(addr+8)
    if dst1-dst2 != 6:
        print 'err'
        return False
    return True

def patch_jcc8(addr):
    PatchByte(addr, 0xEB)
    PatchWord(addr+2, 0x9090)

def is_jcc8(b):
    return b&0xF0 == 0x70
def is_jump_short_pair(addr):
    jcc1 = Byte(addr)
    jcc2 = Byte(addr+2)
    if not is_jcc8(jcc1) or not is_jcc8(jcc2):
        return False
    if abs(jcc2 - jcc1) != 1:
        return False
    dst1 = Byte(addr+1)
    dst2 = Byte(addr+3)
    if dst1 - dst2 != 2:
       return False
    return True

import idc
start = 0x0117C773
end = 0x0117D241

addr = start
while addr <= end:
    near = is_jump_near_pair(addr)
    short = is_jump_short_pair(addr)
    print hex(addr),near,short
    if near == True:
        patch_jcc32(addr)
        print 'near_yes'
    if short == True:
        patch_jcc8(addr)
        print 'short_yes'
    addr = NextHead(addr,end)

2. 反反混淆技巧

images

某些代码因为混淆,不能使用“空格”来将其图形化,解决方法是在0117c773处点击“p”来创建新的函数。

但是段代码被混淆,因为一些垃圾字节导致无法创建函数,所以就可以使用idapython将这块给修改成nop

首先按u,将这段数据的格式去掉,还原为“未定义”字节,接下来使用代码将其修改为nop

imagesimages也会遇到如下图,这种错误调用。0117d7dc处调用的是117cbf1+2,也即是117cbf3。但是ida给解析成117cbf1,所以第二张图引用就标红了,解决方法是:

在117cbf1处按“u”,去除格式。然后在117cbf3处按“c”,将接下来的指令解析成汇编。

imagesimages

 

images3. 分析程序保护机制

删除垃圾指令显示了一个可读的代码块。这段代码首先分配两个内存块:一个全局1MB缓冲区和每个线程一个64 KB的缓冲区。大的第一个缓冲区用作多个并发线程的索引。从可移植可执行文件本身提取大块数据,并使用自定义的异或算法解密两次。我们确定该数据块包含一个操作码指令数组,准备由FinFisher a实现的定制虚拟机程序(从这里通常被称为“VM”)解释。以下是FinFisher多层保护机制。

images


云涯历险记 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:恶意代码样本分析:FinFisher
喜欢 (0)