某旅行 App 基于 LR 劫持的 ARM64 控制流混淆逆向分析
0x00 前言
在分析某旅行类 App 的 native 层时遇到这种混淆。so 名就不说了,分析过看到这个混淆模式应该会觉得眼熟——对,思路一脉相承,换了个马甲而已。本文重点在混淆原理和自动化处理,跟目标无关。
0x01 混淆特征识别
在 IDA 中可以看到大量如下模式的调用:
LDR W0, =0x98609D
BL loc_14A8AC
乍看像是普通的函数调用,但 loc_14A8AC 内容很可疑:
loc_14A8AC:
BL sub_14C1BC
只有一条 BL,没有任何逻辑。继续跟进 sub_14C1BC:
sub_14C1BC:
SUB X0, X0, #0x2D
EOR X0, X0, #0x70
LSR X0, X0, #0xD
ADD X0, X0, #1
LDR W0, [X30, X0, SXTX#2]
ADD X30, X30, X0
RET
这里开始出现异常:函数末尾是 RET,但在此之前 X30 已经被修改,也就是说 RET 实际跳回的地址并不是调用方。
0x02 原理分析
完整执行流追踪如下。
第一步,call site 设置 dispatch key 并跳转:
LDR W0, =0x98609D ; W0 = key
BL loc_14A8AC ; X30 = 0x158014(此 LR 之后会被丢弃)
第二步,trampoline 再次 BL,X30 被覆盖:
BL sub_14C1BC ; X30 = 0x14A8B0(跳表基址)
这一步是整个混淆的核心——第二个 BL 把 X30 覆盖成了紧跟其后的地址,而这个地址恰好就是跳表的起始位置。
第三步,dispatcher 用 W0 计算跳表下标:
index = ((0x98609D - 0x2D) ^ 0x70) >> 0xD + 1 = 1220
第四步,查表并劫持返回地址:
LDR W0, [X30, X0, SXTX#2] ; W0 = *(int32*)(X30 + 1220 * 4)
ADD X30, X30, X0 ; X30 = 跳表基址 + 表项值
RET ; 跳到真正目标
RET 执行时 X30 已经指向目标地址,call site 原本的 LR(0x158014)就此丢失,执行流不会返回到 LDR+BL 后面,而是直接跳到跳表计算出的目标。
一句话总结:LDR W0, =key + BL trampoline 这两条指令等价于一条 B <target>,目的是伪装成函数调用,破坏 IDA 的控制流图重建。
进一步分析发现,同一个二进制里存在多个 dispatcher,变换指令序列各不相同,例如:
sub_17E3F0:
EOR X0, X0, #0xC0
SUB X0, X0, #0x26
ADD X0, X0, #1
LDR W0, [X30, X0, SXTX#2]
ADD X30, X30, X0
RET
变换顺序和参数不同,但结构完全一致。这意味着不能硬编码参数,需要对每个 dispatcher 动态提取变换链。
0x03 自动化反混淆
整体思路
找 dispatcher:扫描所有函数,特征是 LDR Wx,[X30,X0,SXTX#2] + ADD X30,X30,X0 + RET 连续出现
模拟变换:从 dispatcher 函数头开始逐条模拟对 X0 的算术/逻辑运算,算出 index
patch:把 LDR W0, =key 替换为 B target,原来的 BL trampoline 替换为 NOP
完整脚本
import struct
import idc
import idaapi
import idautils
import ida_bytes
import ida_segment
DRY_RUN = True # 改 False 才真正 patch
MAX_INSNS = 20
NOP_AARCH64 = 0xD503201F
MASK64 = 0xFFFFFFFFFFFFFFFF
REG_X0 = 129 # IDA 9.x ARM64
REG_X30 = 159
def is_dispatcher(func_addr):
addr = func_addr
for _ in range(MAX_INSNS):
insn = idaapi.insn_t()
size = idaapi.decode_insn(insn, addr)
if not size:
return False
if insn.get_canon_mnem().upper() == "LDR":
if insn.ops[0].reg != REG_X0:
return False
insn2 = idaapi.insn_t()
if not idaapi.decode_insn(insn2, addr + size):
return False
if insn2.get_canon_mnem().upper() != "ADD":
return False
if insn2.ops[0].reg != REG_X30 or insn2.ops[1].reg != REG_X30:
return False
insn3 = idaapi.insn_t()
if not idaapi.decode_insn(insn3, addr + size + insn2.size):
return False
return insn3.get_canon_mnem().upper() == "RET"
addr += size
return False
def find_all_dispatchers():
result = []
for seg_ea in idautils.Segments():
seg = ida_segment.getseg(seg_ea)
if not seg:
continue
if idc.get_segm_attr(seg_ea, idc.SEGATTR_TYPE) in (
ida_segment.SEG_DATA, ida_segment.SEG_BSS,
ida_segment.SEG_XTRN, ida_segment.SEG_NULL):
continue
for func_ea in idautils.Functions(seg.start_ea, seg.end_ea):
i
...(已截断)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-291422.htm
[原创]某旅行 App 基于 LR 劫持的 ARM64 控制流混淆逆向分析
173 浏览
1 回复
tql