70、Android逆向(六):反调试(检测模拟器、检测Root、检测Frida)
反调试,说白了就是App不想让你碰它。你拿个调试器往上怼,它要么闪退,要么给你一堆假数据。我做了这么多年逆向,碰到过最狠的一个App,连我插着充电线都检测——它觉得USB连电脑就是在调试。
这一章我们聊三个最基础的反调试手段:检测模拟器、检测Root、检测Frida。这三板斧几乎每个加固App都会用,你绕不过去。
1. 检测模拟器——你跑在真机上吗?
很多App一发现你在模拟器里跑,直接就罢工了。为什么?因为模拟器环境太“假”了,特征太明显。
1.1 常见的检测点
我整理了一下,模拟器检测主要看这几个地方:
| 检测项 | 说明 | 绕过思路 |
|---|---|---|
| Build.FINGERPRINT | 模拟器通常包含"generic"字样 | Hook掉,返回真机值 |
| Build.MODEL | 常见如"sdk_gphone_x86" | 改成"Pixel 6"之类的 |
| TelephonyManager | 模拟器没有IMEI/IMSI | 用Xposed伪造 |
| WiFi热点列表 | 模拟器通常为空 | 手动添加几个 |
| 传感器数量 | 真机至少有加速度计、陀螺仪 | 用Magisk模块模拟 |
嗯,这里要注意:单个检测点很容易绕过,但App往往组合使用。比如先检查Build.FINGERPRINT,再检查传感器,最后检查有没有SIM卡。你只改一个地方,它照样能发现你。
1.2 代码示例
这是我在一个金融App里看到的检测代码,很典型:
public boolean isEmulator() {
// 检测1:Build特征
String finger = Build.FINGERPRINT;
if (finger.contains("generic") || finger.contains("vbox")) {
return true;
}
// 检测2:传感器
SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE);
if (sm.getSensorList(Sensor.TYPE_ALL).size() < 5) {
return true;
}
// 检测3:电话状态
TelephonyManager tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
if (tm.getDeviceId() == null || tm.getDeviceId().equals("000000000000000")) {
return true;
}
return false;
}
你看,它三个检测串在一起,任何一个命中就直接返回true。我曾经遇到过一个App,它甚至检测光线传感器——模拟器里光线传感器返回的值永远是0,真机至少有个环境光值。
2. 检测Root——你手机越狱了吗?
Root检测比模拟器检测更常见。毕竟很多用户确实在真机上Root了,App得防着这些人。
2.1 检测手段
我见过最粗暴的检测方式,就是直接执行su命令:
try {
Runtime.getRuntime().exec("su");
// 如果能执行到这里,说明有su
return true;
} catch (Exception e) {
return false;
}
但这种方法太容易被Hook了。你只要把exec的返回值改一下,它就检测不到了。
高级一点的App会这么干:
- 检查/system/bin/su是否存在——但Magisk可以隐藏
- 检查/system/app/Superuser.apk——同样可以隐藏
- 检查build.prop中的ro.debuggable——这个比较难绕
- 检查selinux状态——Root后selinux通常被设为permissive
我个人习惯是,先看看App到底检测了哪些文件。用strace跟踪一下,它访问了哪些路径,一目了然。
2.2 避坑指南
/data/local/tmp目录下有没有Magisk的临时文件。我当时只隐藏了su和MagiskManager,结果它还是检测到了。后来才发现,Magisk会在/data/local/tmp里写一个.magisk文件。把这个文件删掉或者隐藏,问题就解决了。
3. 检测Frida——动态插桩的克星
Frida是逆向工程师的瑞士军刀。但正因为太好用了,App厂商也盯上了它。现在稍微有点安全意识的App,都会检测Frida。
3.1 Frida的检测原理
Frida检测主要分两类:
| 检测类型 | 原理 | 绕过方法 |
|---|---|---|
| 端口检测 | Frida默认监听27042端口 | 改端口号 |
| 进程名检测 | 查找"frida-server"进程 | 重命名frida-server |
| D-Bus协议检测 | Frida使用D-Bus通信 | 用frida-gadget替代 |
| 内存特征检测 | 扫描内存中的"frida"字符串 | 编译时修改源码 |
其中最狠的是D-Bus协议检测。App会尝试连接一个D-Bus服务,如果连接成功,说明Frida正在运行。这个检测很难绕,因为它是Frida的底层通信机制。
3.2 代码示例
这是我在一个游戏App里看到的D-Bus检测代码:
private boolean detectFrida() {
try {
// 尝试连接D-Bus
Socket socket = new Socket("127.0.0.1", 27042);
byte[] handshake = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
socket.getOutputStream().write(handshake);
socket.getOutputStream().flush();
// 如果能收到回复,说明有Frida
byte[] response = new byte[16];
int read = socket.getInputStream().read(response);
socket.close();
return read > 0;
} catch (Exception e) {
return false;
}
}
你看,它直接往27042端口发了一个D-Bus握手包。如果Frida在监听,就会回复。这个检测你光改端口号没用,因为App会扫描多个端口。
frida-gadget模式。Gadget是注入到App进程里的,不走端口,也不走D-Bus。App根本检测不到。我最近几个项目都是用Gadget过的,稳得很。
4. 知识体系总览
下面这张图是我自己画的,把本章的三个检测维度串起来了。你一看就明白它们之间的关系:
5. 实战建议
说了这么多,最后给几条实在的建议:
- 先静态分析:把App的so文件拖进IDA,搜一下"generic"、"su"、"frida"这些关键词。看看它到底检测了什么。
- 再动态验证:用Frida Hook住关键函数,把返回值打印出来。你就能知道App在哪个环节检测到了你。
- 最后针对性绕过:根据检测点,一个一个绕。别想着一次性搞定所有检测,那不现实。
核心思路:反调试的本质是“信息不对称”。App不知道你用了什么工具,你也不知道App检测了什么。谁的信息更多,谁就占优势。所以,先收集信息,再动手,永远是最稳妥的做法。
好了,这一章就到这里。反调试是个大话题,我们后面还会继续深入。你先把这三个基础检测吃透,后面的高级玩法才能玩得转。