前言
在逆向工程师的职业生涯之中,有一个常见的问题一直在困扰着他们,就是有时会耗费大量的时间去阅读并研究花指令(Junk Code)。所谓花指令,并不是程序真正需要执行的指令,而是一段在不影响程序正常运行前提下迷惑反编译器的汇编指令。借助花指令,可以增加反编译过程的难度。有时,工程师发现可执行代码出现在他们没有预料到的地方,从而误以为发现了一个漏洞,或发现了一个最新的恶意软件样本,然而事实上,他们遇到的很可能就是花指令。
在这篇文章中,我们将讨论如何识别花指令,并了解它与实际代码之间的区别。我们将重点关注x86系统中的反汇编过程,但在许多其他的结构中也会存在类似的问题,大家可以参考。
1. 问题描述
通常,在反编译花指令过程中,我们所犯的第一个错误就是:因为它可以被反编译成有效的指令,所以就将其假设为实际的代码。我们知道,x86的指令集是紧密排列的,其中有许多都是以单字节编码。一眼望去,不管我们反编译任何数据,都能得到看起来有效的x86代码。
举例来说,我生成了一个16KB的随机数据文件,并将其反编译。这样一来,就产生了6455个有效的32位x86指令,以及239字节无法识别的数据。这就意味着,超过98%的随机数据可以被反编译成有效指令,剩下的则无法被解析为有效指令。为了进一步说明,我们一起来看看这个随机数据最开始部分的反汇编,其中包含了一些指令,以及1字节无法识别的数据。
如上所示,其中的第一列是数据偏移量,本文中所有的反汇编指令都以此来表示。第二列是组成该条指令的字节,而第三列则是这些字节相应的反汇编表示。除去标红的0x16之外,反汇编后所得到的全部指令都是有效的。尽管偏移0x16的内容看起来像是指令,但“db”仅仅是用于声明一个字节的汇编符号,反汇编程序并不会将其识别为指令的一部分。正如前面所说,x86的指令集非常密集,因此每个字节都有可能是一条指令的开始部分。在这种情况下,0xF6有可能是指令的有效开始,但由于与后面的0x4E组合之后没有形成有效的操作数,因此0xF6就被视为了无效。在16字节的随机数据中,274个无法识别的字节共包含27种不同的值。而在这27种不同的值之中,唯一一个在英文字符串范围内的,是字母“b”(0x62)。
在这里,我们重点关注了更为主流的32位反汇编过程,但同样的情况也会发生在16位和64位英特尔汇编指令之中。当上述随机数据被反编译成16位代码时,其中的96%将被解析为有效指令。而假如反编译成64位,有效指令占比则为95%。
你可能会说,有可能随机数据中会连续出现较多的0,这就意味着在这个区域中没有代码。然而,高级的反编译器能够智能识别出大量0的出现,判断出它们并非代码,但这部分仍会被反汇编成有效的x86指令,如下所示:
更进一步,如果我们使用英语文本生成随机数据,就会更具有迷惑性。我尝试使用一个包含随机英文字符(lorem ipsum,即“乱数假文”)的60KB的文件,并对其进行了反编译,得到了23170条指令,其中没有任何无法识别的数据。因此,我们现在生成的随机数据,100%都可以反编译成有效指令。下面的片段展示的是“乱数假文”前三个单词(Lorem ipsum dolor)的反编译结果:
很显然,面对这样的花指令,我们就要花费更多的精力去从中辨别出真正的代码。
2. 解决方案
目前来说,我们应对这一问题的最好武器恐怕是人的大脑,但我们还是应该想办法,借助一些启发式算法,利用脚本更好地过滤掉这些花指令。由此也可以看出,即使是经验丰富的逆向工程师,也要不断学习了解这类的代码片段,提升发现花指令的能力。
2.1 特权指令
在x86处理器中,会通过四个Ring级别来进行访问控制的保护,听起来就像指环王一样。其中,有两个Ring基本上不使用。内核模式的执行发生在Ring0,而用户模式发生在Ring3。某些指令只能在Ring0中执行。有许多特殊的特权指令,也恰好是单字节的操作码,并且经常出现在反编译的花指令之中。让我们再来看看之前的“乱数假文”,但在这次,我将重点标记其中的特权指令:
如果我们发现,某段代码并不是作为操作系统引导加载程序、内核或者设备驱动程序运行的,那么一旦看到这些特权指令,就应该意识到该反汇编实际上并不是有效的代码。标红的这些,都是从硬件端口读写数据的输入/输出指令。这些指令必须在设备驱动程序中使用,如果在Ring3用户模式中执行,会产生异常。即使我们现在反编译的是内核代码,这些指令出现的频率也比正常情况下高出很多。下面是一些常见的Ring0指令列表,经常会作为花指令使用:
l IN (INS, INSB, INSW, INSD)
l OUT (OUTS, OUTSB, OUTSW, OUTSD)
l IRET
l IRETD
l ARPL
l ICEBP / INT 1
l CLI
l STI
l HLT
2.2 不常见的指令
在用户模式下,有一些合法但却并不常见的Ring3指令,由于这些指令都是从编译代码而来,并不是直接人工编写的汇编语言,因此就显得格外奇怪。我们可以将这类指令分为三小类,分别为:过于便捷的指令、不常见的数学运算和远指针指令。接下来让我们仔细研究一下。
(1) 过于便捷的指令
l ENTER
l LEAVE
l LOOP (LOOPE/LOOPZ, LOOPNE/LOOPNZ)
l PUSHA
l POPA
其中,ENTER和LEAVE指令经常被汇编语言的编写者用于函数的开始和结束,但它们并不实用,也不能与PUSH、MOV、SUB这些指令一起完成。因此,当前的编译器更倾向于避免使用ENTER和LEAVE,绝大多数的程序员也都不会去使用它们。这些指令在操作码中大约占比1%,经常被用作花指令来使用。
LOOP指令(包括带有条件的LOOPZ、LOOPNZ指令)提供了一种非常直观有效的方式来编写汇编语言中的循环。但编译器一般不会使用这些指令,它们通常会创建自己的循环,并使用JMP(无条件跳转指令)以及其他条件跳转指令。
PUSHA和POPA指令的作用是将所有的16位通用寄存器压入堆栈或取出堆栈。对于编写者来说,这两个指令就像宏一样方便。由于它们还可以存储或恢复堆栈指针本身,因此它们非常复杂。所以编码器并不会在函数的一开始存储它们,再在函数的结尾恢复它们。在编译的代码中不会存在这些指令,但由于它们也同样占据了1%左右的操作码范围,因此也经常被用作花指令。
(2) 不常见的数学运算
浮点指令
l F*
l WAIT/FWAIT
浮点指令通常是以字母“F”开始。尽管有一小部分程序会使用浮点数学运算,但大部分程序都不会使用。浮点指令在操作码范围中占有较大比例,所以在花指令中特别常见。在这里,代码设计的知识往往能够在逆向工程的尝试中派上用场。如果我们对一个3D图形程序进行逆向,就会发现其中存在大量的浮点指令,这是正常的。但如果我们对恶意软件进行逆向,浮点数学运算就不太可能会出现在其中。最后需要特别说明的一点是,Shellcode经常会使用一些浮点指令,用来得到指向其自身的指针。
l SAHF
l LAHF
SAHF的作用是将寄存器AH的值传送到标志寄存器PSW,LAHF则是反过来将PSW的值保存到AH。由于这一操作只能通过编程时明确的语句来实现,并不会从高级语言中翻译而来,所以编译器通常情况下不会输出这些指令。其实,即使是人工编写的汇编代码,也很少使用这两个指令。并且它们还是单操作码范围内的单字节指令,同样常常作为花指令。
ASCII调整指令
l AAA
l AAS
l AAM
l AAD
这一系列的“AA”指令,作用是在汇编语言中以十进制的形式来处理数据。同样,由于这些指令在早期比较流行,现在已经不被广泛使用,所以理论上不应该经常出现。它们同样也是单字节指令。
l SBB
SBB指令与SUB相似,只不过它将进位标志(Carry Flag)添加到源操作数之中。该指令在合法的代码中,特别是在对大于机器字长的数字进行运算时也能见到。然而,SBB指令有9个以上的操作码,占比3.5%。尽管并不是单字节指令,但由于其拥有多种形式和众多操作码,所以会被用作花指令。
l XLAT
XLAT是汇编语言查表指令。由于它并不能直接翻译成某个特定的高级语言结构,所以编译器一般不会使用该指令。它同样也是一个单字节指令,因此它是花指令的概率,要比人工使用该指令的概率更大一些。
l CLC
l STC
l CLD
l STD
这些指令会清除/设置进位标志及目的标志(Destination Flag)。这些指令可能会在流操作的附近出现,我们通常会在REP前缀的地方看到它们。同样,它们都是单字节指令,经常用作花指令。
(3) 远指针指令
l LDS
l LSS
l LES
l LFS
l LGS
在英特尔的架构中,远指针(Far Pointer)不会存在于16位之中。但是,设置远指针的指令,仍然会占用两个单字节的操作码,还占用了双字节操作码中的3个值。因此,这些指令也会作为花指令出现。
2.3 频繁出现的指令前缀
x86中的指令可以带有前缀,我们将其称为指令前缀。指令前缀的作用是修改后面指令的行为,最常见的一种就是改变操作数的大小。例如,我们正在以32位的模式执行指令,但希望能使用16位寄存器或操作数执行计算,那么就可以在计算指令中添加一个前缀,以告知CPU,这是一个16位的指令,并不是32位。
这样的指令前缀有很多,但非常不巧的是,其中的一大部分都在ASCII的字母范围之内。这也就意味着,如果我们反汇编了一个ASCII的文本(比如之前的“乱数假文”),那就会出现特别多的指令前缀。
如果我们在对一个32位的代码进行反汇编,却发现它大量使用了16位的寄存器(比如使用了AX、BX、CX、DX、SP、BP,而不是EAX、EBX、ECX、EDX、ESP、EBP),此时就要意识到,现在在看的很有可能就是花指令。
反汇编器会在指令助记符(Instruction Mnemonic)前添加特定的符号来表示其他前缀。如果我们在代码中能看到下面这些关键词,很有可能会是花指令:
l LOCK
l BOUND
l WAIT
段选择器
l FS
l GS
l SS
l ES
在16位模式下,会使用段寄存器(CS、DS、FS、GS、SS、ES)进行寻址的存储。程序的代码通常是基于CS“代码段”寄存器来实现引用的,而程序处理的数据是从DS“数据段”寄存器引用的。ES、FS、GS是额外的数据段寄存器,用于32位代码。段选择器(Segment Selector)的前缀字节,可以添加在指令之前,从而强制使其基于特定的段来引用内存,而不是基于其默认的段。由于上述这些都占用了单字节操作码空间,因此也会在花指令中频繁出现。在我的随机数据中,有一个反编译后的指令,是GS寄存器的段选择符前缀使其不指向地址存储器:
相比于普通代码来说,花指令会更加频繁地使用这些段寄存器,并且编译器不会产生输出。我们再看看另外一个例子:
该指令会从堆栈中弹出SS“堆栈段”寄存器。这是一个完全有效的指令,然而,由于这是反编译的32位代码,段寄存器并不会像16位那样进行改变。同样,如果只有上面几行代码,会出现另一个奇怪的指令:
32位的体系结构中,支持更多段寄存器的寻址。这条指令的作用是将一些数据移动到第七个段寄存器中,我的反编译器将其命名为“segr7”。
3. 总结
由于花指令的存在,最好的情况是浪费分析人员的时间和精力,而最坏的情况恐怕是会误导我们的分析过程,让我们去分析错误的数据。通过本文,我们学会了识别常见的反汇编花指令,并对其频繁出现的原因做以详细地分析。
希望通过本文的阅读,可以让你在以后的工作中轻松识别出花指令,从而节约工作时间,确保分析的准确性。
发表评论
您还未登录,请先登录。
登录