1. 花指令简介

花指令(junk code),也叫脏指令。反汇编器采用的线性扫描和递归下降,为了干扰反汇编器正确识别函数结构、跳转逻辑,加固方就会在正常指令中插入一些没有实际功能,但是又能干扰到反汇编引擎识别的指令,这些指令就被叫做花指令。

2. 花指令类型

2.1 虚假控制流 + DCQ

虚假控制流:通过在代码逻辑中插入无法到达的分支跳转(如无条件跳转或恒假条件分支),破坏控制流图的连续性,导致逆向分析时出现冗余路径。

DCQ伪指令DCQ属于ARM数据定义伪指令,用于分配并初始化双字(8字节)内存空间。其数据内容可伪装成机器指令片段干扰反汇编器解析流。

  1. 代码段嵌入数据块:

    1. 反汇编器可能将dcq定义的8字节数据错误解析为两条4字节ARM指令,引发后续指令错位。

_main:
    b real_start          ; 跳过混淆区
    dcq 0xdeadbeefcafebabe ; 伪装成指令的无效数据
real_start:
    mov x0, #0x1234       ; 实际功能代码
    ldr x1, =0x5678
  1. 嵌套跳转结构:

    1. 通过dcq填充高相似度指令片段,结合恒假条件跳转构造不可达路径

entry:
    cbz x0, fake_branch   ; 恒假条件跳转(x0≠0)
    dcq 0x1f2003d51f2003d5 ; 填充合法指令片段(实际为NOP)
    b actual_code
fake_branch:
    .byte 0x00,0x00      ; 破坏指令对齐
actual_code:
    add x2, x3, x4       ; 真实逻辑

2.2 虚假控制流 + B + 栈不平衡

虚假控制流:通过插入恒假条件判断(如cmp x0, x0搭配b.ne),引导反汇编器错误解析不可达分支

B指令:ARM64的b指令可实现4字节跳转,结合栈操作指令形成干扰模式

fake_branch:
    sub sp, sp, #0x20  ; 伪造栈修改
    b real_code        ; 实际未执行

栈不平衡陷阱:在不可达路径中插入非对称栈操作(如sub sp无配对恢复指令),制造虚假栈帧结构。

  1. 单层栈干扰结构:静态分析工具可能将.byte数据解析为nop指令,导致后续指令错位

_start:
    cmp xzr, xzr          ; 恒真条件
    b.eq real_code        ; 必然跳转
    sub sp, sp, #0x30    ; 不可达路径栈干扰
    .byte 0x1F, 0x20     ; 破坏指令对齐
real_code:
    stp x29, x30, [sp]   ; 真实栈操作
  1. 嵌套栈操作干扰:通过不可达路径中的sub spldr组合,触发逆向工具误判栈偏移量

entry:
    mov x0, #0x1
    cbz x0, fake_block    ; 恒假跳转
    add sp, sp, #0x10    ; 实际执行路径
    b end
fake_block:
    sub sp, sp, #0x40    ; 栈不平衡操作
    ldr x1, [sp, #0x20]  ; 伪造内存访问
    .inst 0xDEADBEEF     ; 非法指令
end:
    ret

2.3 利用X30(LR)寄存器RET跳转

核心机制:

  1. ‌寄存器特性X30寄存器存储函数返回地址,RET指令默认使用X30值作为跳转目标。通过动态修改X30寄存器值,可构造非连续性控制流。

  2. 跳转控制:利用blr x30ret指令实现间接跳转,结合栈操作干扰逆向工具对函数边界的识别。

实现方式:

  1. 直接寄存器操纵:逆向工具可能将ret解析为函数结束标记,导致后续代码被误判为独立函数

fake_ret:
    adr x30, real_code  // 预置有效地址
    mov x2, #0x1234     // 干扰指令
    ret                 // 跳转至real_code
real_code:
    str x0, [sp, #-0x10]!  // 真实栈操作
  1. 动态地址计算:通过算术运算隐藏目标地址,增加静态分析难度

dynamic_jump:
    sub x30, x30, #0x30  // 偏移地址计算
    add x30, x30, #0x40
    ret                   // 实际跳转地址=原x30+0x10

2.3 其他实现

这里只做补充,基本都依赖于上面几种的组合

  1. 数据动态解密

.macro dynamic_dcq
    .quad 0x1B4A3D5 ^ 0xFFFFFFFF  ; 加密数据
.endm

decrypt_block:
    ldr x0, [pc, #-8]    ; 动态加载加密数据
    eor x0, x0, #0xFFFFFFFF
    br x0                ; 跳转到解密后地址
    dynamic_dcq          ; 嵌入混淆数据
  1. 多层嵌套

layer1:
    tst x0, #0x1        ; 动态寄存器检测
    b.eq layer2         ; 真实跳转
    dcq 0xA8D12345A8D12345  ; 干扰数据块
layer2:
    cbz x1, layer3      ; 不可达条件
    .inst 0xDEADCODE    ; 非法指令
layer3:
    ret                 ; 真实返回

3. 案例

这里主要是花指令去除,涉及到虚假控制流会放到ollvm相关文章中去讲解。

主要操作流程:

  1. 通过ida 观察,识别出花指令类型和特征,尝试手动去除

  2. 根据手动去除的流程,编写ida python脚本自动去除

3.1 花指令识别与手动去除

这里有一个样本,JNI_OnLoad,可以看到它只有一条JUMPOUT指令。这条指令出现是因为:当反汇编引擎无法解析基于寄存器的间接跳转目标(如BR X30BX R0)时,IDA会标记JUMPOUT

切换到反汇编窗口,查看JNI_OnLoad代码具体逻辑,可以看到花指令的逻辑就是:动态计算出X9的地址,之后跳转去执行,后面的DCQ部分就是干扰反汇编器解析的。

接下来我们来分析X9是如何生成的。

第一条指令:STP X0, X1, [SP,#-0x20]!

翻译成伪代码就是:sp -= 0x20;sp[0] = x0;sp[8] = x1

+-------------------+
|       旧SP        | <--- 当前的栈指针(SP)位置(执行前)
+-------------------+
|                   |
|      未使用       |
|      空间         |
|                   |
+-------------------+
|       X1          | <--- SP + 8
+-------------------+
|       X0          | <--- SP
+-------------------+ <--- 新的栈指针(SP)位置(执行后,已减去0x20)

第二条指令:STP X2, X30, [SP,#0x10]

翻译成伪代码就是:[SP + 0x10] = X2;[SP + 0x18] = X2

+-------------------+
|       旧SP        | 
+-------------------+
|                   |
|      未使用       |
|      空间         |
|                   |
+-------------------+
|       X30         | <--- SP + 0x18
+-------------------+
|       X2          | <--- SP + 0x10
+-------------------+
|       X1          | <--- SP + 8
+-------------------+
|       X0          | <--- SP + 0
+-------------------+ <--- 新的栈指针(SP)位置(执行后,已减去0x20)

第三条指令:ADR X1, 0x462EC

翻译成伪代码就是:X1 = 0x462EC

第四条指令:SUBS X1, X1, #0xC

即:X1 -= 0xC,执行后X1 = 0x462E0

第五条指令:MOV X0, X1

即:X0 = X1,执行后X0 = X1 = 0x462E0

第六条指令:ADDS X0, X0, #0x3C ;

即:X0 += 0x30,执行后X0 = 0x4631c

第七条指令:STR X0, [SP,#0x18]

SP[0x18] = X0

+-------------------+
|       旧SP        | <--- 当前的栈指针(SP)位置(执行前)
+-------------------+
|                   |
|      未使用       |
|      空间         |
|                   |
+-------------------+
|  x0 0x4631c       | <--- SP + 0x18
+-------------------+
|       X2          | <--- SP + 0x10
+-------------------+
|       X1          | <--- SP + 8
+-------------------+
|       X0          | <--- SP + 0
+-------------------+ <--- 新的栈指针(SP)位置(执行后,已减去0x20)

第八条指令:LDP X2, X9, [SP,#0x10]

即:X2 = SP[0x10]; X9 = [0x18],执行后X9 = 0x4631c

第九条指令:LDP X0, X1, [SP],#0x20

即:X0 = SP[0],X1 = SP[8];SP +=20

+-------------------+
|        新SP       | <--- 新的栈指针(SP)位置(执行后,已减去0x20)
+-------------------+
|                   |
|      未使用       |
|      空间         |
|                   |
+-------------------+
|  x0 0x4631c       | 
+-------------------+
|       X2          | 
+-------------------+
|       X1          | 
+-------------------+
|       旧SP        | <--- 当前的栈指针(SP)位置(执行前)
+-------------------+ 

第十条指令:BR X9此时X9 = 0x4631c,所以最终跳转到0x4631c去执行

现在在0x4631cP即可让ida识别成函数。

之后再看JNI_OnLoad反汇编,就可以正常识别了。

3.2 编写IDA Python自动去除

根据上面分析跳转地址主要由以下几条指令计算

01 01 00 10		ADR  X1, 0x462EC
21 30 00 F1		SUBS  X1, X1, #0xC
E0 03 01 AA		MOV	  X0, X1
00 F0 00 B1		ADDS  X0, X0, #0x3C

接着我们需要提取特征,以前两行指令作为特征"E0 07 BE A9 E2 7B 01 A9"

写测试代码(ida 9.0+)

import ida_bytes
def binSearch(start,patternStr):
    matches = []

    pattern = ida_bytes.compiled_binpat_vec_t()
    ida_bytes.parse_binpat_str(pattern,0x0,patternStr,16,ida_nalt.BPU_2B)
    while True:
        addr,_  = ida_bytes.bin_search(start, idc.BADADDR,pattern,1)
        if addr == idc.BADADDR:  # bad address
            break
        else:
            matches.append(addr)
            start = addr + 1
    return matches

# 1 搜索匹配
matches = binSearch(0, "E0 07 BE A9 E2 7B 01 A9")
print(matches)
print(len(matches))

行后有匹配到123个

再编写代码动态计算跳转地址

import ida_bytes
def binSearch(start,patternStr):
    matches = []

    pattern = ida_bytes.compiled_binpat_vec_t()
    ida_bytes.parse_binpat_str(pattern,0x0,patternStr,16,ida_nalt.BPU_2B)
    while True:
        addr,_  = ida_bytes.bin_search(start, idc.BADADDR,pattern,1)
        if addr == idc.BADADDR:  # bad address
            break
        else:
            matches.append(addr)
            start = addr + 1
    return matches


def getJumpAddress(addr):
    A = idc.get_operand_value(addr + 8, 1)  # get_operand_value 需要地址识别为代码
    B = idc.get_operand_value(addr + 12, 2)
    C = idc.get_operand_value(addr + 20, 2)
    return A - B + C   # 获取跳转的地址


def makeInsn(addr):
    # 尝试在给定的地址 addr 上创建一条指令
    if idc.create_insn(addr) == 0:
        # 如果在地址 addr 上没有成功创建指令,则删除该地址上的所有项(扩展项)
        idc.del_items(addr, idc.DELIT_EXPAND)
        # 重新尝试在地址 addr 上创建一条指令
        idc.create_insn(addr)
    # 等待自动操作完成
    idc.auto_wait()

# 1 搜索匹配
matches = binSearch(0, "E0 07 BE A9 E2 7B 01 A9")

for addr in matches:
    # 2 计算跳转地址
    makeInsn(addr) # 给定的地址 addr 上创建一条指令
    targetAddr = getJumpAddress(addr)
    print(hex(targetAddr))

最后编写patch代码,去除花指令。

import keystone  # pip install keystone-engine
from keystone import *

import ida_bytes
import idc
import ida_nalt

# https://python.docs.hex-rays.com/
# https://python.docs.hex-rays.com/ida_bytes

def binSearch(start,patternStr):
    matches = []

    pattern = ida_bytes.compiled_binpat_vec_t()
    ida_bytes.parse_binpat_str(pattern,0x0,patternStr,16,ida_nalt.BPU_2B)
    while True:
        addr,_  = ida_bytes.bin_search(start, idc.BADADDR,pattern,1)
        if addr == idc.BADADDR:  # bad address
            break
        else:
            matches.append(addr)
            start = addr + 1
    return matches



def getJumpAddress(addr):
    A = idc.get_operand_value(addr + 8, 1)  # get_operand_value 需要地址识别为代码
    B = idc.get_operand_value(addr + 12, 2)
    C = idc.get_operand_value(addr + 20, 2)
    return A - B + C   # 获取跳转的地址


def makeInsn(addr):
    # 尝试在给定的地址 addr 上创建一条指令
    if idc.create_insn(addr) == 0:
        # 如果在地址 addr 上没有成功创建指令,则删除该地址上的所有项(扩展项)
        idc.del_items(addr, idc.DELIT_EXPAND)
        # 重新尝试在地址 addr 上创建一条指令
        idc.create_insn(addr)
    # 等待自动操作完成
    idc.auto_wait()


def generate(code, addr):
    ks = Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
    # 参数2是地址,很多指令是地址相关的,比如 B 指令,如果地址无关直接传 0 即可,比如 nop。
    encoding, _ = ks.asm(code, addr)
    return encoding


# 1 搜索匹配
matches = binSearch(0, "E0 07 BE A9 E2 7B 01 A9")

for addr in matches:
    # 2 计算跳转地址
    makeInsn(addr) # 给定的地址 addr 上创建一条指令
    targetAddr = getJumpAddress(addr)
    print(hex(targetAddr))

    # 3 patch
    code = f"B {hex(targetAddr)}"
    bCode = generate(code, addr)
    nopCode = generate("nop", 0)

    ida_bytes.patch_bytes(addr, bytes(bCode))
    ida_bytes.patch_bytes(addr + 4, bytes(nopCode) * 9)

执行后结果如下:

之后使用菜单栏中Edit -> Patch prgram -> applay patch to保存即可。

4. 补充说明

后续计划会使用ollvm+花指令做一个demo

  1. 控制流双重污染

// OLLVM处理后的平坦化控制流
void flattened_func() {
    int state = 0;
    while(1) {
        switch(state) {
            case 0:
                __asm__ volatile(
                    "ldr x0, [sp, #0x10]\n\t"
                    ".dcq 0xDEADBEEFCAFEBABE\n\t"  // 插入ARM64花指令
                );
                state = compute_next_state();
                break;
            // ...
        }
    }
}
  1. 指令级别混合混淆

substitute_inst:
    add x0, x1, x2          // OLLVM未替换的原始指令
    .dcq 0xA8D12345A8D12345 // 数据污染
    sub x0, x1, x2, lsl #2  // OLLVM生成的等价替换指令
    ret

学习文章:

  1. Anti-Disassembly on ARM64

  2. https://zhuanlan.zhihu.com/p/640711859

  3. 04-安全攻防之bl和ret指令_bl用完lr寄存器-CSDN博客

  4. 花指令处理(一)

  5. 杨如画花指令教程