花指令详解

参考文章:

  1. 逆向分析基础 — 花指令实现及清除_jmp花指令逆向-CSDN博客

花指令是企图隐藏掉不想被逆向工程的代码块(或其它功能)的一种方法,在真实代码中插入一些垃圾代码的同时还保证原有程序的正确执行,而程序无法很好地反编译, 难以理解程序内容,达到混淆视听的效果。

简单的说就是在代码中混入一些垃圾数据阻碍你静态分析

常用的两类反汇编算法:

  1. 线性扫描算法:逐行反汇编(无法将数据和内容进行区分)
  2. 递归行进算法:按照代码可能的执行顺序进行反汇编程序。

image-20251008134326892

最简单的花指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//源码
#include<stdio.h>
#include<windows.h>

int main(int argc, char* argv[])
{

int a = MessageBox(NULL,"Hello","main",MB_OK);
int b,c,d,e;
//这部分为花指令部分
__asm{_emit 0xe8}
//花指令结束
b=1;
c=2;
d=3;
e=4;
return 0;
}

简单jmp和多层跳转

IDA 是递归扫描的,能够正常识别正常插入一个E8字节,但是OD就不行了

1
2
3
4
5
__asm {
jmp label1
db junkcode
label1:
}

image-20251008143700384

假设原始机器码:

1
2
3
4
jmp label1
db 0xAA, 0xBB, 0xCC, 0xDD
label1:
mov eax, 1

CPU 执行时:

1
jmp label1 → 跳过那四个字节 → 直接执行 mov eax, 1

但反汇编器可能看到(IDA是递归扫描可以识别):

1
2
3
4
5
jmp short loc_XXXX
stosb
in al, dx
dec ebp
mov eax, 1

多层跳转本质上是一样的

image-20251008143813638

jnx和jx条件跳转

这种比较常见,OD能识别但是IDA会被混淆

1
2
3
4
5
6
_asm{
jz label1
jnz label1
db junkcode
label1:
}

image-20251008143919661

jz(短跳 opcode 0x74 rel8)或 jnz (0x75) 改为 jmp short (0xEB rel8) —— 这样第一条就变为无条件短跳,后面那条变成不可达,可以用 NOP 填充。

示例(假设短跳格式):

1
2
3
4
5
6
7
8
9
10
原始 bytes:
74 05 ; jz +5
75 03 ; jnz +3
AA BB CC ; junkcode (3 bytes)
...label:

Patch:
EB 05 ; jmp +5 (把 0x74 -> 0xEB)
90 90 ; nop nop (把 0x75 0x03 -> 0x90 0x90)
90 90 90 ; 把 junk bytes 全部 nop(视需要)

或全部 NOP把 jz/jnz 和后面的 db 都替换成等长度的 NOP0x90)序列

这种方法有可能会破坏栈平衡

image-20251008152317381

演示手动绕过方法

我们对4012E7 u取消定义

image-20251008152448075

nop掉然后改jz那个语句为无条件跳转语句

image-20251008152652987

永真条件跳转

通过设置永真或者永假的,导致程序一定会执行,由于ida反汇编会优先反汇编接下去的部分(false分支)。也可以调用某些函数会返回确定值,来达到构造永真或永假条件。ida和OD都被骗过去了

这个挺常见的,注意跟上面那个区分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__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:
}

image-20251008144717664

call&ret构造花指令

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

call label1:把 返回地址(即 call 指令后面的下一条指令的地址,也就是 _emit junkcode 的起始地址)压入栈,然后跳转到 label1

CPU 到达 label1 执行 add dword ptr [esp], 8:这条指令直接修改栈上保存的返回地址,把其值加上 8(或其它立即数,取决于多少字节要被跳过)。

  • 结果:当 ret 执行时,从栈中弹出的地址已经被改成了 original_return_addr + 8(也就是跳过那段垃圾字节后的地址)。

ret:跳回到修改后的返回地址,从而跳过紧跟在 call 之后的那段 junkcode(它从未被执行)

image-20251008145335438

接着jnx和jx的例子,下面还有个call &ret类似的花指令

image-20251008153828431

1
2
3
4
5
6
7
8
9
10
11
.text:00401343  push    offset loc_40134F
.text:00401348 jmp far ptr loc_40143F ; 配合前面的 push,走一段非常规流程

.text:0040134F loc_40134F:
.text:0040134F call $+5 ; 把下一条指令地址(=0x401354)压栈
.text:00401354 mov dword ptr [esp+4], 23h ; 把“段选择子”写到栈上(给 retf 用)
.text:0040135C add dword ptr [esp], 0Dh ; 把“返回EIP”加 0x0D,跳过13字节垃圾
.text:00401360 retf ; 远返回:弹出EIP和CS,转到 0x401361:0x23

.text:00401361 db 0EBh, 0AEh ; 0xEB 0xAE = jmp short -0x52 (回到正常流程)

image-20251008154405171

恢复,这题为SUSCTF的一道题目

image-20251008154558702

IDAPython脚本patch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
70 <–> JO(O标志位为1跳转)
71 <–> JNO
72 <–> JB/JNAE/JC
73 <–> JNB/JAE/JNC
74 <–> JZ/JE
75 <–> JNZ/JNE
76 <–> JBE/JNA
77 <–> JNBE/JA
78 <–> JS
79 <–> JNS
7A <–> JP/JPE
7B <–> JNP/JPO
7C <–> JL/JNGE
7D <–> JNL/JGE
7E <–> JLE/JNG
7F <–> JNLE/JG

如jnz和jz这种可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import idc
def clear(start_ea,end_ea):
s_o_h=[0x74,0x05,0x75,0x03,0xe8,0x11,0x00]
while start_ea<end_ea:
if idc.get_bytes(start_ea,7)==bytes(s_o_h):
for i in range(7):
idc.patch_byte(start_ea+i,0x90)
start_ea+=1


start_ea=0x00411DC0
end_ea=0x00411E1E
clear(start_ea,end_ea)
print("ok")