最近在分析某手 Android 应用的一个 so 库时,发现里面大量函数使用了 BR Xn 这种寄存器间接跳转,而 Xn 的值不是简单赋值,而是通过一段专门构造的算术指令现算出来的。IDA 静态分析走到这种跳转就直接断了,F5 也出不了完整的伪代码。这篇笔记记录一下从发现特征、写脚本批量识别、到最终让 Hex-Rays 正常反编译的完整过程。
拖进 IDA 之后,挑一个典型样本看:
关键的五条指令:
手推一下:ADR 拿到的是 label 的地址,减去 imm、加上 off,最终 BR 要跳的地址其实就是:
带进上面的例子:label = 0x30038,imm = 8,offset = 0x28,算出来 real_target = 0x30038 - 8 + 0x28 = 0x30058,正好是后面那段被数据块隔开的 loc_30058。也就是说,这段常量本身没什么实际意义,纯粹是用来在静态分析阶段藏住真实跳转目标的障眼法。
人工一个个算显然不现实,写了个 IDAPython 脚本全局扫描,按上面五条指令的寄存器数据流关系做匹配:ADR 写的寄存器要能在 SUBS 里被读到,SUBS 写的寄存器要能在 MOV 里被读到,以此类推,最后在 ADDS 之后一定范围内找到 BR,算出 real_target,直接把地址标记在注释里。
跑起来之后,所有匹配到的位置都会打印出类似:
同时会在 ADR 处和 BR 处打上注释,ADR 那行背景标成淡黄色方便肉眼定位,BR 那行注明真实跳转地址。
标记归标记,想让 Hex-Rays 正常出伪代码,还得先把 BR Xn 换成 B real_target,再让 IDA 把整个函数结构建对。
第一步不难,BR 换成 B 直接改字节码:根据 AArch64 的 B 指令编码(imm26 是相对当前地址的偏移,单位 4 字节,范围 ±128MB),算出 imm26 = (real_target - br_ea) / 4,拼出 0x14000000 | (imm26 & 0x3FFFFFF) 写回去就行。
问题出在第二步。把 BR patch 成 B 之后,选中整个函数范围按 P 建函数,IDA 直接报:
原因是函数体中间被混淆器插了一段纯数据(就是前面 ADR 引用的那个常量池 qword_30038),MakeFunction 只认连续的代码字节区间,中间夹着数据就直接拒绝,不会跳过。
IDA 的函数模型本身支持"一个函数由不连续的几段代码组成":一个 owner chunk(主块)加若干个 tail chunk(尾块)。编译器优化(尾部合并、switch 分支散落各处)和这种人为插入数据的混淆手法都会用到这套机制,只是平时写"正常"代码基本碰不上。
解法就是不再指望 MakeFunction 一次性建完整个函数,而是分两步:
[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!