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 避坑指南

我曾经遇到过这样一个坑:一个App检测Root的方式是检查/data/local/tmp目录下有没有Magisk的临时文件。我当时只隐藏了suMagiskManager,结果它还是检测到了。后来才发现,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. 知识体系总览

下面这张图是我自己画的,把本章的三个检测维度串起来了。你一看就明白它们之间的关系:

Android反调试检测体系 反调试检测 检测模拟器 检测Root 检测Frida Build特征 传感器 电话状态 WiFi列表 su命令 系统文件 ro.debuggable selinux状态 端口扫描 进程名 D-Bus协议 内存特征 每个检测点都有对应的绕过方法,关键在于找到App用了哪些组合

5. 实战建议

说了这么多,最后给几条实在的建议:

  • 先静态分析:把App的so文件拖进IDA,搜一下"generic"、"su"、"frida"这些关键词。看看它到底检测了什么。
  • 再动态验证:用Frida Hook住关键函数,把返回值打印出来。你就能知道App在哪个环节检测到了你。
  • 最后针对性绕过:根据检测点,一个一个绕。别想着一次性搞定所有检测,那不现实。

核心思路:反调试的本质是“信息不对称”。App不知道你用了什么工具,你也不知道App检测了什么。谁的信息更多,谁就占优势。所以,先收集信息,再动手,永远是最稳妥的做法。

好了,这一章就到这里。反调试是个大话题,我们后面还会继续深入。你先把这三个基础检测吃透,后面的高级玩法才能玩得转。


公众号:蓝海资料掘金营,微信deep3321