1.函数简单分析
项目地址:https://github.com/Aar0n3906/Anti-BR-Obf
对libtprt.so中的JNI_Onload函数进行去混淆

1.png (94.99 KB, 下载次数: 1)
下载附件
2025-9-4 17:17 上传
可以发现在函数后方使用了BR X9作为间接跳转,IDA无法分析控制流了,因为在此处X9为寄存器,在未执行时不知道寄存器的值为多少,所以静态看我们无法了解程序往哪走

2.png (33.8 KB, 下载次数: 0)
下载附件
2025-9-4 17:18 上传
F5反编译后可以看到jni->GetEnv函数后,执行BR X9后就无法看到其余逻辑了

3.png (56.89 KB, 下载次数: 0)
下载附件
2025-9-4 17:18 上传
在JNI_Onload下方还能看到许多对寄存器操作的汇编代码,猜测下方的汇编也为JNI_Onload执行的一部分
2.Unidbg环境的搭建
在这段混淆中我们使用模拟执行对函数进行去混淆
2.1创建项目
直接在项目的unidbg-android/src/test/java目录下建立我们的模拟执行类:AntiOllvm
[ol]
emulator = AndroidEmulatorBuilder.for64Bit().build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM();
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/xxx/xxx.so"),true);
module = dm.getModule();
vm.setJni(this);
vm.setVerbose(true);
dm.callJNI_OnLoad(emulator);
cNative = vm.resolveClass("com/xxx/xxx")
[/ol]
加载动态库==>
public AntiOllvm() {
// 创建模拟器
emulator = AndroidEmulatorBuilder
.for64Bit()
.addBackendFactory(new Unicorn2Factory(true))
.setProcessName("com.example.antiollvm")
.build();
Memory memory = emulator.getMemory();
// 安卓SDK版本
memory.setLibraryResolver(new AndroidResolver(23));
// 创建虚拟机
vm = emulator.createDalvikVM();
vm.setVerbose(true);
// libtprt.so的依赖库
vm.loadLibrary(new File("D:/unidbg/unidbg-android/src/main/resources/android/sdk23/lib64/libc.so"),false);
vm.loadLibrary(new File("D:/unidbg/unidbg-android/src/main/resources/android/sdk23/lib64/libm.so"),false);
vm.loadLibrary(new File("D:/unidbg/unidbg-android/src/main/resources/android/sdk23/lib64/libdl.so"),false);
vm.loadLibrary(new File("D:/unidbg/unidbg-android/src/main/resources/android/sdk23/lib64/libstdcpp.so"),false);
dm = vm.loadLibrary(new File("D:/unidbg/unidbg-android/src/test/resources/AntiOllvm/libtprt.so"), false);
module = dm.getModule();
}
加载后需要先执行jni_onload,而DalvikModule(dm)这个类已经实现了callJNI_OnLoad方法,我们直接调用即可
public void callJniOnload(){
dm.callJNI_OnLoad(emulator);
}
public static void main(String[] args) {
AntiOllvm AO = new AntiOllvm();
AO.callJniOnload();
}

4.png (42.65 KB, 下载次数: 0)
下载附件
2025-9-4 17:18 上传
可以看到在0x87670处进行了RegisterNative,注册的函数名为:initialize,地址在0x86e34
到这一步我们成功完成了使用Unidbg对安卓动态库的运行,并且正常运行了动态库的Jni_Onload函数
2.2基本的指令hook
我们使用hook将每一步运行过的指令都打印出来
public void logIns()
{
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
Capstone capstone = new Capstone(Capstone.CS_ARCH_ARM64,Capstone.CS_MODE_ARM);
byte[] bytes = emulator.getBackend().mem_read(address, 4);
Instruction[] disasm = capstone.disasm(bytes, 0);
System.out.printf("%x:%s %s\n",address-module.base ,disasm[0].getMnemonic(),disasm[0].getOpStr());
}
@Override
public void onAttach(UnHook unHook) {
}
@Override
public void detach() {
}
}, module.base+start, module.base+end, null);
}

5.png (186.32 KB, 下载次数: 0)
下载附件
2025-9-4 17:18 上传
我们可以看到br x9往后执行的指令就是汇编代码中BR之后的指令
这段代码在unidbg中的作用是为指定模块的代码段添加指令级动态跟踪钩子,其效果是实时反汇编并打印该模块每条执行指令的详细信息。
核心功能解析
[ol]
钩子注册
emulator.getBackend().hook_add_new(new CodeHook() { ... }, module.base, module.base+module.size, null);
指令反汇编
Capstone capstone = new Capstone(Capstone.CS_ARCH_ARM64, Capstone.CS_MODE_ARM);
byte[] bytes = emulator.getBackend().mem_read(address, 4);
Instruction[] disasm = capstone.disasm(bytes, 0);
输出格式
System.out.printf("%x:%s %s\n", address - module.base, disasm[0].getMnemonic(), disasm[0].getOpStr());
[/ol]
3.去除间接跳转
CMP W8,W25
CSEL X9,X21,X25,CC
LDR X9,[X24,X9]
ADD X9,X9,X27
BR X9

6.png (66.83 KB, 下载次数: 1)
下载附件
2025-9-4 17:18 上传
[ol]
CMP W8, W25
比较32位寄存器W8和W25的值,设置条件标志位。若W8
CSEL X9, X21, X26, CC
根据CC条件(即W8
LDR X9, [X24, X9]
以X24为基址,加上X9中的偏移量,从内存中加载一个64位地址到X9。这通常用于访问跳转表(如函数指针表)。
ADD X9, X9, X27
将X27的值加到X9中,进一步调整目标地址。X27可能存储固定偏移或基址,用于定位最终跳转位置。
BR X9
无条件跳转到X9指向的地址,执行对应代码。
[/ol]
X27的值由MOV和MOVK分别赋值8位和16位的值,固定为 ==> 0x84FA7910

7.png (45.3 KB, 下载次数: 1)
下载附件
2025-9-4 17:18 上传
X24的值是一个数组

8.png (6.8 KB, 下载次数: 0)
下载附件
2025-9-4 17:18 上传
数组里面分别存了很多指令的地址,用于后续跳转使用
整体逻辑就是每次根据比较结果在数组中选择一个offset,然后用offset + base,得到真实的跳转地址

9.png (76.33 KB, 下载次数: 0)
下载附件
2025-9-4 17:18 上传
CMP W8, W25中的W8和W25的数值也是写死的

10.png (3.05 KB, 下载次数: 0)
下载附件
2025-9-4 17:18 上传

11.png (3.5 KB, 下载次数: 0)
下载附件
2025-9-4 17:18 上传
W8:0x3202B1A5

12.png (22.69 KB, 下载次数: 1)
下载附件
2025-9-4 17:18 上传
W25:0x58F48322
CMP W8,W25
CSEL X9,X21,X25,CC
LDR X9,[X24,X9]
ADD X9,X9,X27
BR X9
以上方代码为例
当CC条件满足时,X21的值赋给X9作为一个offset,在LDR X9,[X24,X9]中使用X24的数组+偏移
根据CSEL的CC条件有两个分支如下:
True Addr: (*(X24+X21) + X27)
False Addr: (*(X24+X25) + X27)
那么我们可以根据CMP的结果使用BCC / BLO和B对True Addr和False Addr进行跳转
替换后的汇编如下
CMP W8,W25
B.cond True Addr
LDR X9,[X24,X9]
ADD X9,X9,X27
B False addr
这样的间接跳转都变为了直接跳转,ida内就可以继续分析了,并且地址也没有变化,因为寄存器的值已知,我们只是其他将他计算出来再跳转而已。
3.1目标
代码的核心目标是自动化修复一种特定的代码混淆技术。这种混淆使用 ARM64 的 csel (条件选择) 指令和 br (间接跳转) 指令来隐藏真实的跳转目标。
原始混淆代码:
cmp w0, w1 ; 比较,设置条件标志 (e.g., EQ, NE)
; ... 可能有其他指令 ...
csel x9, x20, x21, cond ; 如果条件eq为真, x9 = x20, 否则 x9 = x21 (x20/x21存有地址或地址的基址)
; ... 可能有其他指令, 可能会修改 x9 (e.g., ldr x9, [x24, x9]) ...
br x9 ; 跳转到 x9 中的地址
修复后代码:
cmp w0, w1 ; 保留比较
; ... 保留其他指令 ...
b.cond ; Patch 1: 在原 csel 位置替换为条件跳转 (如果cond为真,跳到b1)
; ... 保留其他指令 ...
b ; Patch 2: 在原 br 位置替换为无条件跳转 (对应cond为假,跳到b2)
为了安全准确地找到 (T) 和 (F),代码采用了双模拟器的方法。
3.2整体逻辑
[ol]
阶段 1: 发现与收集混淆特征 (使用主模拟器 emulator)
阶段 2: 分支模拟与 Patch 生成 (使用临时模拟器 tmpEmulator)
阶段 3: 应用 Patch
将 patches 列表中的code写入文件缓冲区的对应位置。
将修改后的数据写入新的 .so-patch文件。
[/ol]
3.3变量解释
tmpEmulator, MainEmulator: 临时模拟器及其相关组件。用于安全地执行分支模拟。为什么需要两个? 避免在主模拟器运行时进行分支模拟可能导致的状态污染(寄存器、内存、Hook 状态被意外修改)。在写这段代码的时候尝试使用一个emulator,但很容易在patch后往下走的分支造成非法内存访问,所以我选择使用两个emu分别进行特征收集和patch执行,这样代码的健壮性会高很多。
insStack: Deque[I]。存储最近执行的指令及其执行前的寄存器状态。为什么需要? 当遇到 br 时,需要回溯查找之前的 csel,并且需要知道 csel 执行前的状态才能正确模拟。
private final Deque[I] insStack = new ArrayDeque(128);
cselInfoMap: Map。存储遇到的 csel 指令的详细信息,以其相对地址作为 Key,方便快速查找。
private final Map cselInfoMap = new HashMap();
[ol]
DeOllvmBr_TwoEmus():
setupMainEmulatorHooks():
private void setupMainEmulatorHooks() {
if (this.mainHook != null) {
this.mainHook.unhook();
this.mainHook = null;
}
System.out.println(" [Hook管理] 正在添加主模拟器 Hook...");
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
// 主模拟器的 Hook 逻辑
long relativeAddr = address - module.base;
if (relativeAddr >= START_ADDR && relativeAddr
processInstruction():
private void processInstruction(long absAddress, int size, Backend backend) {
try {
long relativeAddr = absAddress - module.base;
if (patchedAddresses.contains(relativeAddr)) {
return;
}
List currentRegisters = saveRegisters(backend); // 保存主模拟器当前状态
byte[] code = backend.mem_read(absAddress, size);
Instruction[] insns = capstone.disasm(code, absAddress, 1);
if (insns == null || insns.length == 0) return;
Instruction ins = insns[0];
InstructionContext context = new InstructionContext(relativeAddr, ins, currentRegisters);
insStack.push(context);
if (insStack.size() > 100) insStack.pollLast();
System.out.printf("[MainEmu 执行] 0x%x (Rel: 0x%x): %s %s%n",
ins.getAddress(), relativeAddr, ins.getMnemonic(), ins.getOpStr());
if ("csel".equalsIgnoreCase(ins.getMnemonic())) {
handleConditionalSelect(context);
} else if ("br".equalsIgnoreCase(ins.getMnemonic())) {
// --- 不再调用模拟,而是检查并创建任务 ---
handleBranchInstruction(context);
}
} catch (Exception e) {
System.err.printf("处理主模拟器指令错误 @ 0x%x: %s%n", absAddress, e.getMessage());
e.printStackTrace();
}
}
handleConditionalSelect():
private void handleConditionalSelect(InstructionContext currentContext) {
Instruction ins = currentContext.instruction;
long relativeAddr = currentContext.relativeAddr;
String opStr = ins.getOpStr();
String[] ops = opStr.split(",\\s*");
if (ops.length registersBeforeCsel = currentContext.registers; // CSEL 执行前的状态
try {
long trueSourceValue = getRegisterValue(trueReg, registersBeforeCsel);
long falseSourceValue = getRegisterValue(falseReg, registersBeforeCsel);
CselInfo info = new CselInfo(relativeAddr, destReg, condition, trueReg, falseReg, trueSourceValue, falseSourceValue);
cselInfoMap.put(relativeAddr, info);
System.out.printf("[MainEmu CSEL 发现] @0x%x: %s = %s ? %s(0x%x) : %s(0x%x). Cond: %s%n",
relativeAddr, destReg, condition, trueReg, trueSourceValue, falseReg, falseSourceValue, condition);
} catch (IllegalArgumentException e) {
System.err.printf("[MainEmu CSEL 错误] @0x%x: %s%n", relativeAddr, e.getMessage());
}
}
handleBranchInstruction():
解析 br 指令,获取目标寄存器名。
回溯 insStack: 查找最近执行的指令。
检查历史指令是否是 cselInfoMap 中记录的 csel。
如果找到 csel,并且其目标寄存器与 br 使用的寄存器匹配:
private void handleBranchInstruction(InstructionContext brContext) {
Instruction brIns = brContext.instruction;
long brRelativeAddr = brContext.relativeAddr;
String brReg = brIns.getOpStr().trim();
System.out.printf("[MainEmu BR 发现] @0x%x: br %s. 查找匹配 CSEL...%n", brRelativeAddr, brReg);
int searchDepth = 0;
int maxSearchDepth = 30;
Iterator[I] it = insStack.iterator();
if (it.hasNext()) it.next(); // Skip self
while (it.hasNext() && searchDepth registersBeforeCsel = cselContext.registers;
// 创建模拟任务
SimulationTask task = new SimulationTask(
cselInfo,
brRelativeAddr,
registersBeforeCsel,
module.base + cselInfo.cselAddress, // cselAbsAddr
module.base + brRelativeAddr // brAbsAddr
);
simulationTasks.add(task);
System.out.printf(" [MainEmu 任务已添加] CSEL 0x%x -> BR 0x%x%n", cselInfo.cselAddress, brRelativeAddr);
// 可选:从 Map 中移除,防止一个 CSEL 被多个 BR 错误匹配
// cselInfoMap.remove(prevRelativeAddr);
return;
}
}
searchDepth++;
}
// System.err.printf("[MainEmu BR 警告] @0x%x: 未找到 %s 的匹配 CSEL%n", brRelativeAddr, brReg);
}
performSimulationsOnTmpEmu():
private void performSimulationsOnTmpEmu(SimulationTask task) {
System.out.printf("%n[TmpEmu] ===> 开始模拟任务: CSEL 0x%x -> BR 0x%x ===>%n",
task.cselInfo.cselAddress, task.brRelativeAddr);
Backend tmpBackend = tmpEmulator.getBackend();
// --- 模拟真分支 ---
System.out.println(" [TmpEmu] --- 模拟真分支 (True) ---");
long b1 = performSingleSimulation(tmpBackend, task, true);
System.out.printf(" [TmpEmu] --- 真分支结果 b1 = 0x%x ---%n", b1);
// --- 模拟假分支 ---
System.out.println(" [TmpEmu] --- 模拟假分支 (False) ---");
long b2 = performSingleSimulation(tmpBackend, task, false);
System.out.printf(" [TmpEmu] --- 假分支结果 b2 = 0x%x ---%n", b2);
// --- 处理结果 ---
if (b1 != -1 && b2 != -1) { // 检查模拟是否成功
if (b1 != b2) {
System.out.printf(" [TmpEmu 成功] 发现不同跳转目标: 真=0x%x, 假=0x%x. 生成 Patch.%n", b1, b2);
// 注意:generatePatch 需要绝对地址 b1, b2
generatePatch(task.cselInfo, task.brRelativeAddr, b1, b2);
} else {
System.out.printf(" [TmpEmu 注意] 真假分支目标相同 (0x%x). 无需 Patch 或为其他模式.%n", b1);
}
} else {
System.err.printf(" [TmpEmu 失败] 模拟未能确定跳转目标 (b1=0x%x, b2=0x%x).%n", b1, b2);
}
System.out.printf("[TmpEmu] BR 0x%x
performSingleSimulation():
generatePatch():
private void generatePatch(CselInfo cselInfo, long brRelativeAddr, long trueTargetAbsAddress, long falseTargetAbsAddress) {
long cselRelativeAddr = cselInfo.cselAddress;
// 检查地址是否已被 Patch
if (patchedAddresses.contains(cselRelativeAddr) || patchedAddresses.contains(brRelativeAddr)) {
System.out.printf(" [Patch 跳过] 地址 0x%x 或 0x%x 已标记 Patch.%n", cselRelativeAddr, brRelativeAddr);
return;
}
if (cselRelativeAddr == brRelativeAddr || Math.abs(cselRelativeAddr - brRelativeAddr) BR 0x%x: %s%n", cselRelativeAddr, brRelativeAddr, e.getMessage());
e.printStackTrace();
}
}
辅助方法:
[/ol]
执行前后对比

13.png (112.57 KB, 下载次数: 1)
下载附件
2025-9-4 17:18 上传
最后的最后,求各位大佬Star本项目