花指令
花指令的原理(代码与数据混合)
花指令实质就是一串垃圾指令,它与程序本身的功能无关,并不影响程序本身的逻辑
在软件保护中,花指令被作为一种手段来增加静态分析的难度,花指令也可以被用在病毒或木马上,通过加入花指令改变程序的特征码,躲避杀软的扫描,从而达到免杀的目的
花指令是对抗反汇编的有效手段之一。目的是干扰 IDA 和 OD 等软件对程序的静态分析,使这些软件无法正常反汇编出原始代码
花指令分为两类:可执行的花指令、不可执行的花指令
常用的两类反汇编算法
- 线性扫描算法 —— 逐行反汇编(无法将数据和内容进行区分)
- 将遇到的每一条指令都解析成汇编指令。没有对反汇编的内容进行判断,因而无法正确区分代码和数据,一些数据也会被当成代码来解码
- 例如:简单的花指令
0xE8
是跳转指令,可以对线性扫描算法进行干扰,但是递归扫描算法可以正常分析
- 递归行进算法 —— 按照代码可能的执行顺序进行反汇编
- 按照代码可能的执行顺序来反汇编程序。对每条可能的路径进行扫描,当解码出分支指令后,反汇编工具就将这个分支指令的地址记录下来,并分别反汇编各个分支中的指令,可以避免将代码中的数据作为指令来解码
- 例如:两个
jz
、jnz
跳转,一个指向无效数据,一个指向正常数据来干扰递归扫描算法
IDA 中的花指令
可执行的花指令
能够正常运行但又不改变原始程序逻辑性的一组无用指令,它们运行完后不会改变原来程序的堆栈、寄存器,但能起到干扰静态分析的作用
这类花指令有如下特点:
- 可以正常运行
- 不改变任何寄存器的值
- 反汇编器可以正确反汇编该指令
一般分两种:
- 改变堆栈操作
- 利用
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 功能失效
不可执行的花指令
花指令虽然被插入到了正常代码的中间,但是并不意味着它一定会得到执行。
这类不可执行的花指令通常形式为:在代码中出现了类似数据的代码,或者 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 这条跳转指令必定会被执行,后面插入的 0x11
,0x22
,0x33
就会被跳过,程序正常输出: Hello World!
但是在 IDA 中,IDA 已经无法正确解析这段代码:
- 示例 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 eax
和dec eax
抵消影响。这种共用 opcode 的方法比较麻烦