安卓逆向面试中经常遇到的几个问题

2026-07-13

ollvm有哪几种混淆方式?

指令替换

将简单的运算指令替换为功能等价但更复杂的表达式,增加逆向难度。

原始代码

1
2
3
4
5
6
7
int calculate(int a, int b) {
int sum = a + b; * // 简单的加法*
int diff = a - b; *// 简单的减法*
int xor = a ^ b; *// 简单的异或*

return sum + diff + xor;
}

OLLVM -sub 混淆后

1
2
3
4
5
6
7
8
9
10
11
12
13
int calculate(int a, int b) {
// 1. 加法替代:a + b 替换为 a - (-b)
int sum = a - (-b);

// 2. 减法替代:a - b 替换为 a + (~b + 1) [利用补码原理:-b = ~b + 1]
int diff = a + (~b + 1);

// 3. 异或替代:a ^ b 替换为 (a | b) - (a & b) [并集减去交集]
int xor_val = (a | b) - (a & b);

// 返回三者之和
return sum + diff + xor_val;
}

💡 **核心思想:**利用数学恒等式,如 a + b ≡ a - (-b)、a ^ b ≡ (a|b) - (a&b)、a - b ≡ a + (~b + 1) 等,让代码逻辑不变但形式变得晦涩。

1、简单的 人工还原

2、复杂 MBA 会考虑借助 Z3 或 angr 等符号执行工具

3、特别麻烦直接unicron 模拟执行

虚假控制流

插入永远不会执行的虚假分支,配合不透明谓词(Opaque Predicates)干扰静态分析。

1
2
3
4
5
6
int check_password(int input) {
if (input == 0x1234) {
return 1; // 密码正确
}
return 0; // 密码错误
}

OLLVM -bcf 混淆后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int check_password(int input) {
// 不透明谓词: (x*x) >= 0 在数学上恒为真(不考虑溢出),但分析器难以直接确定
if ((input * input) >= 0) {
if (input == 0x1234) {
return 1; // 真正控制流:密码正确
}
} else {
// 死代码:这段代码理论上永远不会执行!用于混淆视听
int garbage = 0xDEADBEEF;
garbage = garbage * 3 + 7;
if (garbage == 0x1234) {
fake_function();
}
}

return 0; // 密码错误或未命中
}

**💡 核心思想:*利用不透明谓词——对人类/编译器显而易见为真/假,但对静态分析工具难以判断的条件。例如 (xx) >= 0 对整数恒真,但符号执行可能无法确定。

最简单 直接nop掉无用流程 用unidbg 修改寄存器或者frida

控制流扁平化

将函数的所有基本块放入一个循环+switch结构中,彻底打乱原始控制流。

1
2
3
4
5
6
7
8
9
10
11
12
13
int login(int user, int pass) {
if (user == 0x1111) { // 检查用户名
if (pass == 0x2222) { // 检查密码
return 1; // 登录成功
} else {
log_error("bad pass"); // 密码错误
}
} else {
log_error("bad user"); // 用户名错误
}

return 0; // 登录失败
}

OLLVM -fla 混淆后

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
int login(int user, int pass) {
int state = 0; // 状态变量
int result = 0;

while (1) { // 主循环
switch (state) {
case 0: // 入口块
state = (user == 0x1111) ? 1 : 4;
break;

case 1: // 原: if (user == 0x1111) 为真
state = (pass == 0x2222) ? 2 : 3;
break;

case 2: // 原: 登录成功
result = 1;
state = 5;
break;

case 3: // 原: 密码错误
log_error("bad pass");
state = 5;
break;

case 4: // 原: 用户名错误
log_error("bad user");
state = 5;
break;

case 5: // 出口块
return result;
}
}
}

**💡 核心思想:**把所有基本块变成”平级”的case,通过state变量控制跳转。原本清晰的嵌套if-else变成了状态机,逆向工程师无法直观看出逻辑层次关系。

你插桩得到的执行顺序,变回正常的控制流

字符串加密

  • 对程序中的字符串常量进行加密,运行时动态解密
  • 防止通过字符串搜索快速定位关键逻辑

反调试/反分析技术

  • 虽然OLLVM核心主要以上述三种为主,但一些扩展版本或配合使用的工具还会加入:
    • 反调试检测
    • 反虚拟机检测
    • 代码完整性校验

Unidbg问题

补环境用Unidbg 怎么可以保证分支正确 ,如果出现异常你如何定位是系统函数缺失还是库函数缺失,软中断怎么看?

怎么保证分支正确?

答:对齐流向,缺啥补啥”。先用 traceCode() 打印执行轨迹,对比真机。如果是走错分支,说明环境被风控识别了。最快的方法是直接在 Unidbg 里用 write() 打补丁(Patch),强行把分支的跳转条件(比如 CBZCBNZ 指令)改掉。

什么是 traceCode

emulator.traceCode(baseAddress, baseAddress + size);

dm.callFunction(emulator, “target_encrypt_func”);

traceCode 就是把 SO 文件的内存范围传进去,让 Unidbg 把执行过的汇编指令一行行打在控制台上。

怎么用 write 打补丁(Patch)?

如果检测到 Root,就跳到“退出程序”分支;如果没检测到,就继续。

你想让它不管检测没检测到,都当做没检测到,最简单的办法就是把这行指令变成 NOP

1
2
3
4
5
long targetAddress = dm.base + 0x1234; 
// 2. 准备好 NOP(空指令)的机器码字节数组
byte[] nopInstructions = new byte[]{ 0x1F, 0x20, 0x03, (byte) 0xD5 };
// 3. 核心:直接用 write() 把这 4 个字节写入对应的内存地址,强行覆盖掉原来的检测指令
emulator.getMemory().pointer(targetAddress).write(nopInstructions);

怎么分清系统函数还是库函数缺失?

答: 看报错的“第一现场”。

  • 系统函数(JNI/Syscall)缺失:Unsupported virtual method(Java 层的库没补全)或 Unknown syscall
  • 库函数(SO 依赖)缺失:cannot locate symbol(说明找不到其他 SO 里的 C 函数)。

只有这两种方法吗?

1、底层异常: UnicornException Fetch memory failed Invalid memory read/write

2、库函数缺失: Illegal instruction

3、看Unidbg的 warning警告

软中断(SVC)怎么看?

答: 软中断就是底层系统调用。在 ARM 中,看特定的寄存器就行。ARM32 看 R7,ARM64 看 X8,里面存的就是系统调用号(比如 openread)。Unidbg 里开启 setVerbose(true) 就能在控制台直接看到它们。

Unidbg 如何处理属性访问(Property)?

Android 获取设备属性主要有两种方式。

第一种是 Java 层,通过 android.os.BuildSystemProperties.get() 获取,例如 Build.MODELBuild.BRANDBuild.FINGERPRINT

在 Unidbg 中,这类访问一般通过实现 AbstractJnigetStaticObjectField()getStaticIntField() 等方法返回对应值。

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
SystemPropertyHook systemPropertyHook = new SystemPropertyHook(emulator);

systemPropertyHook.setPropertyProvider(new SystemPropertyProvider() {

@Override
public String getProperty(String key) {

System.out.println("SystemProperty: " + key);

switch (key) {

case "ro.board.platform":
return "sdm845";

case "ro.product.brand":
return "Xiaomi";

case "ro.product.manufacturer":
return "Xiaomi";

case "ro.product.model":
return "MI 8";

case "ro.product.device":
return "dipper";

case "ro.product.name":
return "dipper";

case "ro.build.version.release":
return "10";

case "ro.build.version.sdk":
return "29";

case "ro.build.id":
return "QKQ1.190828.002";

case "ro.build.fingerprint":
return "Xiaomi/dipper/dipper:10/QKQ1.190828.002/V12.0.3.0:user/release-keys";

default:
return null;
}
}
});
评论
分享

评论