花指令的原理(代码与数据混合)

花指令实质就是一串垃圾指令,它与程序本身的功能无关,并不影响程序本身的逻辑

在软件保护中,花指令被作为一种手段来增加静态分析的难度,花指令也可以被用在病毒或木马上,通过加入花指令改变程序的特征码,躲避杀软的扫描,从而达到免杀的目的

花指令是对抗反汇编的有效手段之一。目的是干扰 IDA 和 OD 等软件对程序的静态分析,使这些软件无法正常反汇编出原始代码

花指令分为两类:可执行的花指令、不可执行的花指令


常用的两类反汇编算法

  1. 线性扫描算法 —— 逐行反汇编(无法将数据和内容进行区分)
    • 将遇到的每一条指令都解析成汇编指令。没有对反汇编的内容进行判断,因而无法正确区分代码和数据,一些数据也会被当成代码来解码
    • 例如:简单的花指令 0xE8 是跳转指令,可以对线性扫描算法进行干扰,但是递归扫描算法可以正常分析

CTF - Reverse_花指令1.png

  1. 递归行进算法 —— 按照代码可能的执行顺序进行反汇编
    • 按照代码可能的执行顺序来反汇编程序。对每条可能的路径进行扫描,当解码出分支指令后,反汇编工具就将这个分支指令的地址记录下来,并分别反汇编各个分支中的指令,可以避免将代码中的数据作为指令来解码
    • 例如:两个 jzjnz 跳转,一个指向无效数据,一个指向正常数据来干扰递归扫描算法

CTF - Reverse_花指令2.png


IDA 中的花指令

CTF - Reverse_花指令3.png


可执行的花指令

能够正常运行但又不改变原始程序逻辑性的一组无用指令,它们运行完后不会改变原来程序的堆栈、寄存器,但能起到干扰静态分析的作用

这类花指令有如下特点:

  1. 可以正常运行
  2. 不改变任何寄存器的值
  3. 反汇编器可以正确反汇编该指令

一般分两种:

  1. 改变堆栈操作
  2. 利用 call 指令jmp 指令 增加执行流程的复杂度【call 指令的硬指令为 0E8h,E8 表示执行,90 表示跳过
  • 示例:
int main()
{
	_asm {
		push eax;
		add esp, 4;
	}
	printf("Hello World!\n");
}

在 32位 下,push eax 分为两个步骤:

(esp) <-- (esp) - 4     // 修改堆栈指针 ESP (压入时自动减 4)
((esp)) <-- (eax)     // 将指定的操作数送入新的栈顶位置

正常情况下,push 操作需要对应一个 pop 操作来保持堆栈的平衡
在 32位 下,pop eax 分为两个步骤:

(eax) <-- ((esp))     // 将栈顶位置送入指定的操作数
(esp) <-- (esp) + 4     // 修改堆栈指针 ESP (退出时自动加 4)

后面跟着的 add esp, 4 起到了 pop 指令的部分功能,也就是恢复了堆栈的平衡,使得程序能够正常运行

但在 IDA 中却无法正常识别这种操作,所以 IDA 进行解析时会认为该函数堆栈不平衡,从而使 F5 功能失效

CTF - Reverse_花指令4.png


不可执行的花指令

花指令虽然被插入到了正常代码的中间,但是并不意味着它一定会得到执行。

这类不可执行的花指令通常形式为:在代码中出现了类似数据的代码,或者 IDA 反汇编后为 JUMPOUT(xxxxx)

这类花指令一般不属于 CPU 可识别的操作码,那么就需要在上面用跳转跳过这些花指令才能保证程序的正常运行

  • 示例 1:
int main()
{
	_asm {
		xor eax, eax;
		jz s;
		_emit 0x11;   // _emit 指令为:插入字节码
		_emit 0x22;
		_emit 0x33;   // 0x33是 xor 指令的操作码,会导致后面正常的 Push 指令被错误解析
	s:
	}
    printf("Hello World!\n");
}

由于经过 xor eax, eax 后,ZF 标志位被置为 1,那么 jz 这条跳转指令必定会被执行,后面插入的 0x110x220x33 就会被跳过,程序正常输出: Hello World!

但是在 IDA 中,IDA 已经无法正确解析这段代码:

NSSCTF-jump_by_jump2.png

  • 示例 2:
int main()
{
	_asm {
		xor eax, eax;
		jz s;
		add esp, 0x11;
	s:
	}
	printf("Hello World!\n");
}

插入的花指令也可以是改变堆栈平衡的汇编代码,虽然这里的花指令不会被执行,但是 IDA 进行解析时会认为该函数堆栈不平衡,从而使 F5 功能失效


花指令的实现方式

简单jmp

  • 这是最简单的花指令。OD 能被骗过去,但是因为 IDA 采用的是递归扫描法,所以能够正常识别
__asm{
jmp label1
db junkcode
label1:    
}

多层跳转

  • 本质上和简单跳转是一样的,只是加了几层跳转。无法干扰 IDA
start:   //花指令开始
    jmp label1
    DB junkcode
label1:
     jmp label2
     DB junkcode
label2:
    jmp label3
    DB junkcode
label3   

jnx 和 jx 条件跳转

  • 利用 jz 和 jnz 的互补条件跳转指令来代替 jmp。无法干扰吾爱破解版 OllyDBG,但 IDA 不能正常识别
_asm{
    jz label1
    jnz label1
    db junkcode
label1:    
}

永真条件跳转

  • 通过设置永真或永假的条件,导致程序一定会执行。也可以调用某些会返回确定值的函数,来达到构造永真或永假条件。这种方式 IDA 和 OD 都无法正常识别
__asm{
    push ebx
    xor ebx,ebx
    test ebx,ebx
    jnz label1
    jz label2
label1:
    _emit junkcode
label2:
   pop ebx   //需要恢复ebx寄存器    
}

__asm{
	clc
	jnz label1:
	_emit junkcode
label1:
}

call & ret 构造花指令

  • 利用 call 和 ret,在函数中修改返回地址,达到跳过 thunkcode 到正常流程的目的。可以干扰 IDA 的正常识别

call 指令:将下一条指令地址压入栈,再跳转执行
ret 指令:将保存的地址取出,跳转执行

__asm{
    call label1
    _emit junkcode
label1:
    add dword ptr ss:[esp],8   //具体增加多少根据调试来
    ret
    _emit junkcode
}

汇编指令共用 opcode

  • jmp 的指令是 inc eax 的第一个字节,inc eaxdec eax 抵消影响。这种共用 opcode 的方法比较麻烦

CTF - Reverse_花指令5.png

CTF - Reverse_花指令6.png