1. 花指令简介
花指令(junk code),也叫脏指令。反汇编器采用的线性扫描和递归下降,为了干扰反汇编器正确识别函数结构、跳转逻辑,加固方就会在正常指令中插入一些没有实际功能,但是又能干扰到反汇编引擎识别的指令,这些指令就被叫做花指令。
2. 花指令类型
2.1 虚假控制流 + DCQ
虚假控制流:通过在代码逻辑中插入无法到达的分支跳转(如无条件跳转或恒假条件分支),破坏控制流图的连续性,导致逆向分析时出现冗余路径。
DCQ伪指令:DCQ
属于ARM数据定义伪指令,用于分配并初始化双字(8字节)内存空间。其数据内容可伪装成机器指令片段干扰反汇编器解析流。
代码段嵌入数据块:
反汇编器可能将
dcq
定义的8字节数据错误解析为两条4字节ARM指令,引发后续指令错位。
_main:
b real_start ; 跳过混淆区
dcq 0xdeadbeefcafebabe ; 伪装成指令的无效数据
real_start:
mov x0, #0x1234 ; 实际功能代码
ldr x1, =0x5678
嵌套跳转结构:
通过
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无配对恢复指令),制造虚假栈帧结构。
单层栈干扰结构:静态分析工具可能将
.byte
数据解析为nop
指令,导致后续指令错位
_start:
cmp xzr, xzr ; 恒真条件
b.eq real_code ; 必然跳转
sub sp, sp, #0x30 ; 不可达路径栈干扰
.byte 0x1F, 0x20 ; 破坏指令对齐
real_code:
stp x29, x30, [sp] ; 真实栈操作
嵌套栈操作干扰:通过不可达路径中的
sub sp
与ldr
组合,触发逆向工具误判栈偏移量
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跳转
核心机制:
寄存器特性:
X30
寄存器存储函数返回地址,RET
指令默认使用X30
值作为跳转目标。通过动态修改X30
寄存器值,可构造非连续性控制流。跳转控制:利用
blr x30
或ret
指令实现间接跳转,结合栈操作干扰逆向工具对函数边界的识别。
实现方式:
直接寄存器操纵:逆向工具可能将
ret
解析为函数结束标记,导致后续代码被误判为独立函数
fake_ret:
adr x30, real_code // 预置有效地址
mov x2, #0x1234 // 干扰指令
ret // 跳转至real_code
real_code:
str x0, [sp, #-0x10]! // 真实栈操作
动态地址计算:通过算术运算隐藏目标地址,增加静态分析难度
dynamic_jump:
sub x30, x30, #0x30 // 偏移地址计算
add x30, x30, #0x40
ret // 实际跳转地址=原x30+0x10
2.3 其他实现
这里只做补充,基本都依赖于上面几种的组合
数据动态解密
.macro dynamic_dcq
.quad 0x1B4A3D5 ^ 0xFFFFFFFF ; 加密数据
.endm
decrypt_block:
ldr x0, [pc, #-8] ; 动态加载加密数据
eor x0, x0, #0xFFFFFFFF
br x0 ; 跳转到解密后地址
dynamic_dcq ; 嵌入混淆数据
多层嵌套
layer1:
tst x0, #0x1 ; 动态寄存器检测
b.eq layer2 ; 真实跳转
dcq 0xA8D12345A8D12345 ; 干扰数据块
layer2:
cbz x1, layer3 ; 不可达条件
.inst 0xDEADCODE ; 非法指令
layer3:
ret ; 真实返回
3. 案例
这里主要是花指令去除,涉及到虚假控制流会放到ollvm相关文章中去讲解。
主要操作流程:
通过ida 观察,识别出花指令类型和特征,尝试手动去除
根据手动去除的流程,编写ida python脚本自动去除
3.1 花指令识别与手动去除
这里有一个样本,JNI_OnLoad
,可以看到它只有一条JUMPOUT
指令。这条指令出现是因为:当反汇编引擎无法解析基于寄存器的间接跳转目标(如BR X30
或BX 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
去执行
现在在0x4631c
按P
即可让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
控制流双重污染
// 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;
// ...
}
}
}
指令级别混合混淆
substitute_inst:
add x0, x1, x2 // OLLVM未替换的原始指令
.dcq 0xA8D12345A8D12345 // 数据污染
sub x0, x1, x2, lsl #2 // OLLVM生成的等价替换指令
ret
学习文章: