花指令详解 参考文章:
逆向分析基础 — 花指令实现及清除_jmp花指令逆向-CSDN博客
花指令是企图隐藏掉不想被逆向工程的代码块(或其它功能)的一种方法,在真实代码中插入一些垃圾代码的同时还保证原有程序的正确执行,而程序无法很好地反编译, 难以理解程序内容,达到混淆视听的效果。
简单的说就是在代码中混入一些垃圾数据阻碍你静态分析
常用的两类反汇编算法:
线性扫描算法:逐行反汇编(无法将数据和内容进行区分)
递归行进算法:按照代码可能的执行顺序进行反汇编程序。
最简单的花指令:
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: }
假设原始机器码:
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
多层跳转本质上是一样的
jnx和jx条件跳转 这种比较常见,OD能识别但是IDA会被混淆
1 2 3 4 5 6 _asm{ jz label1 jnz label1 db junkcode label1: }
把 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
都替换成等长度的 NOP
(0x90
)序列
这种方法有可能会破坏栈平衡
演示手动绕过方法
我们对4012E7 u取消定义
nop掉然后改jz那个语句为无条件跳转语句
永真条件跳转 通过设置永真或者永假的,导致程序一定会执行,由于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: }
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
(它从未被执行)
接着jnx和jx的例子,下面还有个call &ret类似的花指令
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 (回到正常流程)
恢复,这题为SUSCTF的一道题目
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 idcdef 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" )