一、花指令的核心原理

花指令(Junk Code / Obfuscation)的本质是:
不改变程序原本的真实运行逻辑,但通过插入无效字节、改变指令流或制造指令错位,使 IDA、Ghidra 等反编译工具的静态分析工作陷入瘫痪或解析出错。

二、反编译器的扫描机制与盲区

反编译工具通常有两种扫描模式,而花指令正是针对这两种模式设计的:

image-20261018104131720

1. 线性扫描 (Linear Sweep)

原理:从代码段起始位置开始,逐字节或逐指令往下扫描,遇到分支指令(如 call、jmp)不递归进入分支,而是继续扫描下一条物理地址。

盲区:如果在跳转指令下面接一些垃圾数据,反汇编器会把垃圾数据也当成代码去解析,导致后续指令全部错位。

2. 递归下降 (Recursive Descent)

原理:从程序入口点(Entry Point)开始,顺着控制流进行扫描。遇到 call 会进入函数,遇到 jmp 会跟着跳转。

盲区:遇到条件跳转(JCC)时,反编译器会认为两个分支都有可能走,因此两个分支都会去反汇编。如果在其中一个分支里塞满垃圾数据(或者指令错位),就会导致反编译崩溃。

三、常见花指令的构造手法

1. 干扰指令长度 (指令错位)

反编译工具在解析时,通常会读取一个 currentLen(当前指令长度),然后 当前地址 += currentLen 来解析下一条指令。

  • JMP 错位:比如 jmp 的字节码是 0xE9,后面跟 4 字节相对偏移(目标地址 - 当前地址 - 5)。
  • image-20261018105617577
  • 构造方式:如视频演示图,正常的跳转本应跳到目标位置。但如果利用 0xE9 等操作码的字节重叠,让工具错误地识别了下一条指令的起点,就会导致后续反汇编顺序全乱。

2. 永真/永假条件跳转

为了对付递归下降,出题人会构造必定成立或不成立的条件跳转。

  • 构造方式:比如前面写一个 xor eax, eax(清零,使 ZF=1),紧接着写一个 je label(必定跳转)。然后在 je 下方塞入垃圾数据。
  • 特征识别:由于构造永真/永假式可能会影响寄存器状态,出题人通常会在前面加 push 保存寄存器,跳转后再 pop 还原(栈平衡)。这也是识别花指令的重要特征。

3. 间接跳转 (动态地址计算)

利用 call 压栈返回地址的特性,动态计算跳转目标。

image-20261018111125108

  • 构造方式:如视频演示图,call $+5 将下一行地址压栈,pop rax 取出地址,经过 add 偏移计算后,直接 jmp rax 跳出垃圾指令区。
  • 手工去除法:把这一整块数据 Dump 出 Hex,用等长的 0x90(NOP)在 010 Editor 等工具中替换覆盖。

4. 指令复用 (跳入指令中间)

  • 构造方式:跳转到某一条多字节指令的中间部分,将后半段作为新指令的起始(重叠指令)。经过巧妙构造,可以从原本无意义的字节中解析出一条新的跳转指令跳出。这会让线性扫描彻底失效。

四、实战案例分析(原样保留)

比如这道 [HDCTF2019]Maze

image-20261018172522835

这里可以看到1027,把eax的值push到栈里了,也是花指令的开始。

然后抑或,比较作永真永假式,最后跳转,而且是跳转到了地址+1,也就是102E的58开始的位置(被精心设计过,后面的可以构成另一个语句)。

所以我们只需要从1027到102E的hex用90 nop替换即可。

再看 moectf2025 的 flower 题目解法依旧,jz,jnz互相冲突,然后call又怼到+1的位置,直接全nop即可。

image-20261018173217974

站内搜索

没有找到内容!