0x01 函数分析

函数后方使用了BR X9作为间接跳转,IDA无法分析控制流了,因为在此处X9为寄存器,在未执行时不知道寄存器的值为多少,所以静态看我们无法了解程序往哪走

使用unidbg进行trace

可以看到在执行完0x87114的跳转指令后,直接执行了0x87118,也就是JNI_Onload下方许多对寄存器操作的汇编代码,猜测下方的汇编也为JNI_Onload执行的一部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package com.test.demo;
import capstone.Capstone;
import capstone.api.Instruction;
import com.alibaba.fastjson.util.IOUtils;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.Symbol;
import com.github.unidbg.arm.HookStatus;
import com.github.unidbg.arm.backend.Backend;
import com.github.unidbg.arm.backend.CodeHook;
import com.github.unidbg.arm.backend.UnHook;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.arm.context.Arm32RegisterContext;
import com.github.unidbg.arm.context.Arm64RegisterContext;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.hook.HookCallback;
import com.github.unidbg.hook.HookContext;
import com.github.unidbg.hook.ReplaceCallback;
import com.github.unidbg.hook.hookzz.*;
import com.github.unidbg.hook.xhook.IxHook;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.XHookImpl;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.utils.Inspector;
import com.sun.jna.Pointer;
import keystone.Keystone;
import keystone.KeystoneArchitecture;
import keystone.KeystoneMode;
import unicorn.Arm64Const;
import unicorn.ArmConst;
import unicorn.Unicorn;

import java.io.File;
import java.util.ArrayList;

public class AntiOllvm extends AbstractJni{
private final AndroidEmulator emulator;
private final VM vm;
//private final DvmClass AntiOllvms;
private final Module module;
private final DalvikModule dm;
public AntiOllvm() {
emulator=AndroidEmulatorBuilder.for64Bit().setProcessName("com.test.demo").build();
final Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));

vm=emulator.createDalvikVM();
vm.setJni(this);
vm.setVerbose(true);

vm.loadLibrary(new File("unidbg-android/src/main/resources/android/sdk23/lib64/libc.so"),false);
vm.loadLibrary(new File("unidbg-android/src/main/resources/android/sdk23/lib64/libm.so"),false);
vm.loadLibrary(new File("unidbg-android/src/main/resources/android/sdk23/lib64/libdl.so"),false);
vm.loadLibrary(new File("unidbg-android/src/main/resources/android/sdk23/lib64/libstdcpp.so"),false);

dm=vm.loadLibrary(new File("unidbg-android/src/test/java/com/test/demo/libtprt.so"), false);
module=dm.getModule();
//dm.callJNI_OnLoad(emulator);
//AntiOllvms=vm.resolveClass("com.test.demo.AntiOllvm");
}

public void callJniOnload(){
dm.callJNI_OnLoad(emulator);
}

private void crack(){
emulator.traceCode();

}

public void logIns()
{
//钩子注册:
//在模块的内存范围 [module.base, module.base+module.size) 内注册一个代码执行钩子。
//当模拟器执行到该范围内的任意指令时,会触发 hook() 方法。
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
//指令反汇编
//使用Capstone反汇编引擎,将当前指令地址(address)处的4字节机器码转换为可读的汇编指令。
//ARM64指令特性:固定长度为4字节(Thumb模式为2/4字节混合,但此处明确指定CS_MODE_ARM,表明处理的是ARM模式指令)。
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, module.base+module.size, null);
}

public static void main(String[] args) {
AntiOllvm antiOllvm=new AntiOllvm();
//antiOllvm.crack();
antiOllvm.logIns();
antiOllvm.callJniOnload();

}


}

0x02 去除间接跳转

1
2
3
4
5
6
sub_87118
.text:0000000000087118 000 1F 01 1C 6B CMP W8, W28
.text:000000000008711C 000 09 32 8F 9A CSEL X9, X16, X15, CC
.text:0000000000087120 000 09 6B 69 F8 LDR X9, [X24,X9]
.text:0000000000087124 000 29 01 1B 8B ADD X9, X9, X27
.text:0000000000087128 000 20 01 1F D6 BR X9
  1. CMP W8, W28

    比较w8和w28的值,若w8<w28则设置CC标志位为1,否则为0

  2. CSEL X9, X16, X15

    条件选择,若CC为1(w8<w28),则将x16的值赋值给x9,否则(w8>=w28),将x15的值赋值给x9

  3. LDR X9, [X24,X9]

    以X24为基址,加上X9中的偏移量,从内存中加载一个64位地址到X9。这通常用于访问跳转表(如函数指针表)。

  4. ADD X9, X9, X27

    将X27的值加到X9中,进一步调整目标地址。X27可能存储固定偏移或基址,用于定位最终跳转位置。

  5. BR X9

    无条件跳转到X9指向的地址,执行对应代码

w8的值是写死的 w8:0x3202B1A5

w25的值也是写死的 w25:58F48322

x21:0x30

x26:0xF8

x24为一个数组,存了许多指令,以便后续调用

X27的值由MOV和MOVK分别赋值8位和16位的值,固定为 ==> 0x84FA7910

整体逻辑就是每次根据比较结果在数组中选择一个offset,然后用offset + base,得到真实的跳转地址

1
2
3
4
5
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条件有两个分支如下:

1
2
True Addr: (*(X24+X21) + X27)
False Addr: (*(X24+X25) + X27)

那么我们可以根据CMP的结果使用BCC / BLO和B对True Addr和False Addr进行跳转

替换后的汇编如下

1
2
3
4
5
6
CMP W8,W25
...
B.cond True Addr
LDR X9,[X24,X9]
ADD X9,X9,X27
B False addr
  • 原始混淆代码:

    1
    2
    3
    4
    5
    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 中的地址
  • 修复后代码:

    1
    2
    3
    4
    5
    cmp w0, w1         ; 保留比较
    ; ... 保留其他指令 ...
    b.cond <目标地址1> ; Patch 1: 在原 csel 位置替换为条件跳转 (如果cond为真,跳到b1)
    ; ... 保留其他指令 ...
    b <目标地址2> ; Patch 2: 在原 br 位置替换为无条件跳转 (对应cond为假,跳到b2)