查看原文
其他

识别和避免反汇编中遇到的花指令

2018-01-31 skeep 看雪学院

工作中需要花费大量时间和精力去分析花指令,成为困扰逆向工程师以及安全响应人员的一件事。花指令就是一串没有任何实际意义的指令。除了浪费时间之外,我发现有的人也会对他们发现的花指令感到震惊、兴奋。这是因为他们在一个本来不能执行代码的内存中发现了可执行代码,也就相当于发现了一个漏洞利用程序或高级恶意软件样本。

 

在这篇文章中,我将通过介绍花指令产生的原因以及其我将讨论如何识别花指令。 我们将重点关注x86反汇编,在其他的平台上也是类似的情况。



问题的产生


人们在分析汇编代码的过程中,由于反汇编器将花指令反汇编成有效的指令,所以很多人就默认假设所有汇编代码都是实际的代码指令,这就是很多人所犯的第一个错误。由于X86平台的指令是密集编码的,所以很多指令都是一个字节编码的。乍一看,几乎所有数据的反汇编代码都可能产生有效的x86代码。

 

例如,我生成了一个16kb的随机数据文件,并对其进行了反汇编。 这产生了6,455个有效的32位x86指令,只有239个字节无法识别的数据。这意味着反汇编程序无法解析成有效指令的数据。可以看到超过98%的随机数据可以被反汇编成有效的指令。


让我们看看这个随机数据的第一部分反汇编,我们可以看到许多指令和一个字节无法识别的数据。

 

 

这个片段中的第一列数据是数据偏移量。第二列显示组成指令的字节,第三列显示这些字节的反汇编表示。对于除高亮行(偏移量0x16)之外的所有行,反汇编显示一个有效的指令。


偏移量0x16表示可能看起来像一条指令的内容,但符号“db”只是汇编表示法来声明一个字节。反汇编器表示在这个位置是字节0xF6,它不能将其识别为指令的一部分。x86指令集是编码密集,以至于每个字节值都可能是下一个指令的潜在开始。


在这种情况下,0xF6可能是一个指令的有效开始,取决于它后面的字节,但这种情况下的其与后面的字节0x4E并不构成有效的操作数。在随机数据的16kb中,274个无法识别的字节只有27个不同的值。在这27个值中,唯一一个与常用英文字符范围重叠的是字母“b”(0x62)。

 

在这篇文章中,我们将坚持使用32位反汇编,因为它的流畅度更高,但在16位和64位英特尔汇编中也会出现同样的问题。 当先前提供的随机数据被拆分为16位代码时,它产生了96%的有效指令,而拆分为64位则产生了95%的有效指令。

 

你可能会想如何用大面积的零来表示着这段内存空间中没有代码存在,这该多么美妙。高级反汇编可能会智能地识别大量的零代码,并跳过反汇编,但它们还是将大量的零反汇编为有效的x86指令。 这里有几个反汇编零字节来说明这一点:


 

英文文本的问题更为严重。我生成了一个包含随机文本(lorem ipsum)的60kb文件,并对其进行了反汇编,生成了23,170条指令,并且没有无法识别的数据。所以,100%的随机文本被反汇编成有效的指令。 下面的片段显示了标准Lorem ipsum反汇编的前三个单词(“Lorem ipsum dolor”):




所以,我们在看反汇编代码时,不仅要看对应序列能否被反汇编成功,还是要看其是否是有效的指令。



解决方法


尽管编写更好的启发式的脚本用来过滤掉花指令是可行的,但是目前我们最好的武器依旧是是人类的大脑。对于一个经验丰富的逆向工程师来说,在本文中到目前为止所看到的代码片段有很多东西,他们是学习判断你所看到的代码是否是花指令的关键。


👉 特权指令


x86处理器有四个用于保护的“环”,就像“魔戒”一样,但是不那么有趣。其中两个环(Ring1和Ring2)通常根本不使用。 内核模式执行Ring0和Ring3用户模式则是常用的两种特权级别。某些指令只能在Ring0中运行。这些特殊的特权指令也碰巧是单字节操作码,并且经常发生在反汇编花指令中。让我们再来看看这个“Lorem ipsum”反汇编出来的指令,但这次我会突出特权指令的说明:

 



如果你知道你正在查看的代码不是作为操作系统引导加载程序、内核或设备驱动程序运行的,那么当查看到这些特权指令时我们应该意识到这些反汇编不是真正有效的代码。 突出显示的指令都是IN和OUT指令的变体,主要作用是从硬件端口读取和写入数据。这些可能的指令必须在设备驱动程序中使用,如果在Ring3用户模式下执行,将会产生一个异常。即使你试图反汇编内核代码,这些指令发生的频率也会比你反汇编操作系统的任意文件的要高得多。以下是你将经常在反汇编的花指令中看到的一些常见Ring0指令的列表:

  • IN  (INS, INSB, INSW, INSD)

  • OUT (OUTS, OUTSB, OUTSW, OUTSD)

  • IRET

  • IRETD

  • ARPL

  • ICEBP / INT 1

  • CLI

  • STI

  • HLT


👉 罕见的指令


在用户模式代码中有许多Ring3指令是有效的,但是在编译代码中非常少见。 我们可以将这些指令分为三类:便利指令,不太可能的数学指令和远指针指令。我们来看看这些类别。


👉 便利指令

 

  • ENTER

  • LEAVE

  • LOOP (LOOPE/LOOPZ, LOOPNE/LOOPNZ)

  • PUSHA

  • POPA


汇编语言程序员可以使用ENTER和LEAVE指令来获得函数序言和结语,这两个指令可以手动的使用PUSH,MOV和SUB来完成。现代编译器倾向于避免使用ENTER和LEAVE,因此大多数程序员也不会使用它们。因为他们一起占据了操作码范围的近1%,所以它们在花指令代码中的出现是非常普遍的。

 

LOOP指令及其条件同类的LOOPZ和LOOPNZ提供了一种用汇编语言编写循环的非常直观且有用的方法。 编译器通常选择不使用这些,而是使用JMP和条件跳转指令来制作自己的循环。

 

PUSHA和POPA指令提供了将所有寄存器的状态保存到堆栈的机制。这为汇编语言程序员提供了一个可能方便的类宏指令。由于它也存储和恢复堆栈指针本身,这使得一个懒惰的编码器在函数启动时盲目地存储寄存器信息,并在函数结束时恢复它们的潜在用途变得复杂了。你不会在编译代码中找到这些指令,但是它们也占据了可用操作码范围的近1%,所以它们经常出现在花指令中。


👉不太可能的数学指令


  • 浮点指令

    • F*

    • WAIT/FWAIT


浮点指令通常以字母“F”开始。虽然有些程序使用浮点数学,但大部分程序都不使用浮点数学。浮点指令占用操作码范围的很大一部分,所以它们在花指令中的是普遍存在的。这就是你对你正在尝试进行逆向工程的代码设计的知识将派上用场的地方。如果您使用3D图形对程序进行反向工程,则需要查找潜在的大量浮点指令。对于我们在FLARE团队所做的工作,恶意软件分析,浮点数学是非常罕见的。值得注意的例外是shellcode通常使用一些浮点指令作为获取指向自身的指针。


  • SAHF

  • LAHF


SAHF和LAHF指令将AH寄存器的内容复制到标志寄存器EFLAGS中。这是一种编程行为,不会从高级语言转换下来,因此编译器通常不会输出这些指令。如果不是手动编写汇编代码,那么这些指令将会很少出现。由于这些是单操作码范围内的单字节指令,所以它们经常在反汇编花指令中出现。


  • ASCII调整指令

    • AAA

    • AAS

    • AAM

    • AAD


“AA”系列指令涉及以二进制编码的十进制形式处理数据。这是一个比较早的编码方式,基本很难再现代计算中遇到。但是,您将经常在反汇编花指令中遇到这些指令,因为它们是单字节指令。


  • SBB

SBB指令与SUB相似,只是它将进位标志添加到源操作数。这可以在合法的代码中看到,特别是当试图对大于机器字长的数字进行算术运算时(32位代码中的64位数学)。不幸的是,SBB指令不少于九个操作码,占可能范围的3.5%。它们不是单字节指令,但是有很多形式和很多操作码,它们经常在反汇编的的花指令中出现。


  • XLAT


在你的汇编代码中XLAT是一个有趣的指令。它没有直接的翻译到一个单一的高级语言结构,因此,编译器不像我们在FLARE团队那样喜欢它。由于它是一个单字节指令,所以你会发现它比使用汇编语言找到程序员更频繁。


  • CLC

  • STC

  • CLD

  • STD


这些指令清除并设置进位和目标标志。 它们可能在流操作附近的编译器生成的代码中找到(通常会在REP前缀的地方)。尽管如此,由于它们都是单字节指令,它们在花指令中出现的可能性非常高。


远指针指令

  • LDS

  • LSS

  • LES

  • LFS

  • LGS

在英特尔架构中,远指针的使用并不存在于16位时代。但是,设置远指针的指令仍然占用两个单字节操作码和两个字节操作码范围中的三个值。因此,你会看到这些指令经常出现在反汇编花指令中。



指令前缀


x86中的指令可以有前缀。前缀字节通常会修改后续指令的行为。前缀的常用用法是改变操作数的大小。例如,如果您正在以32位模式执行指令,并且希望使用16位寄存器或操作数执行计算,则可以在计算指令中添加一个前缀,以通知CPU它是一个16位指令而不是一个32位的。有许多这样的前缀,不幸的是,其中许多都落在ASCII表的字母范围内。这意味着,当反汇编ASCII文本(如我们的lorem ipsum),指令前缀将是非常普遍的。这些指令前缀会出现在正常指令中,但没有在花指令中出现的频繁。 如果您正在拆解32位代码,并且您看到大量使用的16位寄存器(AX,BX,CX,DX,SP,BP等,而不是EAX,EBX,ECX,EDX,ESP和EBP ),你可能正在看花指令。

 

反汇编器还将代表在指令助记符之前添加了某些符号的其他前缀。如果你在代码中经常看到任何这些关键字,那很可能是花指令代码:

  • LOCK

  • BOUND

  • WAIT


段选择器


  • FS

  • GS

  • SS

  • ES


在16位模式下,使用段寄存器(CS,DS,FS,GS,SS,ES)完成寻址存储器。程序的代码通常是基于CS“代码段”寄存器来引用的,程序处理的数据是从DS“数据段”寄存器引用的 ES,FS和GS是额外的数据段寄存器,合法地用于32位代码,但这是一个更高级的话题。有段选择符前缀字节,可以在指令之前添加段,强制指定它参考内存基于特定的段,而不是它设计使用的默认段。 由于这些都占用了那个宝贵的单字节操作码空间,所以它们会在反汇编的花指令中频繁出现。从我反汇编的随机数据中得到的下列指令显示了一个在没有寻址内存的指令上使用的GS寄存器的段选择符前缀:

 

 

反汇编垃圾也会比普通代码更频繁地使用这些段寄存器,而且编译器不会输出。 让我们来看看反汇编花指令的另一行:

 

 

该指令从堆栈弹出SS“堆栈”寄存器。这是一个完全有效的指示; 然而,这是反汇编的32位代码和段寄存器通常不会像在16位模式中那样改变。在同样的反汇编中,只有上面几行代码是另一个奇怪的代码行:

32位体系结构支持更多段寄存器的寻址,而不是段寄存器的名称。这条指令是将一些数据移动到第7段寄存器中,我的反汇编器命名为“segr7”。



结论


往小了说,反汇编花指令代码可能会浪费你的时间和精力。在最糟糕的情况下,它可以让你正在分析的数据是不正确的。在这篇文章中,我们学会了通过识别和理解不可能的代码来发现反汇编的花指令。我们打破了不寻常的代码分类为最常见的情况,并讨论他们的意义,为什么他们会频繁发生,为什么他们不应该出现。通过学习这些指标,你可以轻松地发现反汇编中的花指令代码,节省你的时间。





本文由看雪翻译小组 skeep 编译,来源fireeye@Nick Harbour

转载请注明来自看雪社区


热门阅读


点击阅读原文/read,

更多干货等着你~



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存