21、移动端反外挂(iOS/Android):越狱/root检测、注入检测、模拟器识别

移动端反外挂,说白了就是一场猫鼠游戏。我做了这么多年安全,最深的体会就是:没有绝对的安全,只有不断升级的对抗。今天咱们聊聊iOS和Android上的三个核心战场——越狱/root检测、注入检测、模拟器识别。

嗯,先别急着看代码。你得先理解一个道理:外挂作者也是人,他们也在研究你的检测逻辑。所以我们的策略是——多层防御,动态变化

一、越狱/root检测:你手机越狱了吗?

越狱和root,说白了就是获取了系统的最高权限。外挂有了这个权限,就能干很多坏事——修改游戏内存、注入代码、绕过签名校验。所以第一步,就是判断设备是否被“开刀”了。

1.1 iOS越狱检测

iOS越狱检测,我习惯分三步走:文件检测、沙盒检测、动态检测。

文件检测:越狱设备通常会留下一些标志性文件。比如:

// 常见越狱文件路径
NSArray *jailbreakPaths = @[
    @"/Applications/Cydia.app",
    @"/Library/MobileSubstrate/MobileSubstrate.dylib",
    @"/bin/bash",
    @"/usr/sbin/sshd",
    @"/etc/apt"
];

for (NSString *path in jailbreakPaths) {
    if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
        // 检测到越狱
        return YES;
    }
}

我在项目中遇到过一个问题:有些用户只是越狱了,但没装Cydia。所以光靠文件检测不够,还得配合沙盒检测。

沙盒检测:越狱设备能访问系统沙盒外的文件。试试能不能在/private目录下创建文件:

// 尝试写入系统目录
NSError *error;
NSString *testPath = @"/private/jailbreak_test.txt";
[@"test" writeToFile:testPath atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (!error) {
    // 能写入系统目录,说明越狱了
    return YES;
}

动态检测:调用fork()system()函数。越狱设备上这些函数通常不会被沙盒限制。

注意:苹果审核时可能会检测你的越狱检测代码。我建议把检测逻辑分散到多个地方,别集中在一个函数里。否则容易被“一刀切”绕过。

1.2 Android root检测

Android的root检测,思路类似,但更复杂。因为Android碎片化严重,不同厂商的root方式不一样。

我常用的检测方法:

  • 检测su文件/system/bin/su/system/xbin/su/sbin/su
  • 检测root管理应用:SuperSU、Magisk Manager等
  • 检测系统属性ro.debuggablero.secure
  • 检测挂载点/system分区是否可写
// Android root检测示例
public boolean isRooted() {
    // 检测su文件
    String[] paths = {
        "/system/bin/su",
        "/system/xbin/su",
        "/sbin/su",
        "/system/sd/xbin/su"
    };
    for (String path : paths) {
        if (new File(path).exists()) {
            return true;
        }
    }
    
    // 检测Magisk
    try {
        Process process = Runtime.getRuntime().exec("magisk -v");
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(process.getInputStream()));
        String line = reader.readLine();
        if (line != null && !line.isEmpty()) {
            return true;
        }
    } catch (IOException e) {
        // 没有Magisk
    }
    
    return false;
}

我曾经遇到一个坑:有些手机厂商自己就预置了su文件,但用户并没有root。所以单纯检测su文件会误报。我的解决办法是——组合检测,多个条件同时满足才判定为root。

二、注入检测:谁在往游戏里塞东西?

注入,是外挂最常用的手段。iOS上常见的是CycriptSubstrate注入;Android上则是XposedFridaLSPosed

2.1 iOS注入检测

iOS的注入检测,我主要看两点:动态库加载函数Hook

动态库检测:遍历当前进程加载的所有动态库,看看有没有可疑的:

// 检测可疑动态库
uint32_t count = _dyld_image_count();
for (uint32_t i = 0; i < count; i++) {
    const char *name = _dyld_get_image_name(i);
    NSString *libName = [NSString stringWithUTF8String:name];
    
    // 黑名单
    if ([libName containsString:@"Substrate"] ||
        [libName containsString:@"Cycript"] ||
        [libName containsString:@"frida"]) {
        // 发现注入
        return YES;
    }
}

但外挂作者也不傻。他们会把动态库改名,或者用dlopen手动加载。所以光靠名字检测不够。

函数Hook检测:检查关键函数是否被Hook。比如objc_msgSendmallocfree等。方法是对比函数地址是否在预期范围内:

// 检查objc_msgSend是否被Hook
IMP original = class_getMethodImplementation(objc_getMetaClass("NSObject"), 
                                               @selector(alloc));
// 获取libobjc.A.dylib中的原始地址
Dl_info info;
dladdr((void*)original, &info);
// 如果地址不在libobjc范围内,说明被Hook了
我的经验:函数Hook检测要放在游戏启动的早期,越早越好。因为外挂通常会在启动时注入,你晚一步检测,它就已经把检测函数也给Hook了。

2.2 Android注入检测

Android的注入检测,核心是检测Xposed框架Frida

Xposed检测

  • 检测/data/data/de.robv.android.xposed.installer/目录
  • 检测XposedBridge.jar是否在ClassLoader中
  • 检测android.app.ActivityThreadcurrentActivityThread方法是否被Hook
// Xposed检测
public boolean isXposedInstalled() {
    try {
        // 尝试加载Xposed类
        ClassLoader classLoader = getClassLoader();
        classLoader.loadClass("de.robv.android.xposed.XposedBridge");
        return true;
    } catch (ClassNotFoundException e) {
        return false;
    }
}

Frida检测:Frida的检测比较麻烦,因为它可以运行在多种模式下。我常用的方法:

  • 检测/data/local/tmp/frida-server进程
  • 检测frida相关端口(27042默认端口)
  • 检测linenoisegum-js-loop等Frida特征字符串

说实话,Frida的检测很难做到100%。我见过最狠的做法是——在native层定时扫描/proc/self/maps,看看有没有可疑的内存映射。

三、模拟器识别:你用的是真机还是模拟器?

模拟器识别,这个我太有发言权了。很多外挂作者喜欢在模拟器上跑脚本,因为方便调试。所以识别模拟器,能挡住一大批低端外挂。

3.1 常见模拟器特征

特征类型 检测方法 说明
硬件特征 CPU型号、GPU渲染器 模拟器通常使用特定的虚拟硬件
系统属性 ro.product.model、ro.build.fingerprint 模拟器有固定的属性值
传感器 光线传感器、加速度计 模拟器通常没有真实传感器
网络特征 IP地址、MAC地址 模拟器网络环境与真机不同
文件系统 /system/build.prop、/proc/cpuinfo 模拟器文件内容有特定模式

3.2 Android模拟器检测代码

// 模拟器检测
public boolean isEmulator() {
    // 检测硬件
    String cpuInfo = readFile("/proc/cpuinfo");
    if (cpuInfo.contains("intel") || cpuInfo.contains("amd")) {
        // 真机ARM,模拟器通常是x86
        return true;
    }
    
    // 检测系统属性
    String model = Build.MODEL;
    if (model.contains("sdk") || model.contains("emulator")) {
        return true;
    }
    
    // 检测传感器
    SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
    if (sensorManager.getSensorList(Sensor.TYPE_ALL).size() < 5) {
        // 真机通常有10+个传感器
        return true;
    }
    
    // 检测电话功能
    TelephonyManager tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
    if (tm.getDeviceId() == null || tm.getDeviceId().equals("000000000000000")) {
        return true;
    }
    
    return false;
}

3.3 iOS模拟器检测

iOS模拟器检测相对简单。因为iOS模拟器运行在x86架构上,而真机是ARM架构:

// iOS模拟器检测
#if TARGET_IPHONE_SIMULATOR
    // 模拟器环境
    return YES;
#else
    // 真机环境
    return NO;
#endif

但要注意,这个宏只在编译时有效。如果外挂作者直接修改了编译配置,那就没用了。所以我建议在运行时也做检测:

// 运行时检测
#include <sys/utsname.h>

struct utsname systemInfo;
uname(&systemInfo);
NSString *machine = [NSString stringWithCString:systemInfo.machine 
                                       encoding:NSUTF8StringEncoding];
// 模拟器的machine值通常是x86_64或i386
if ([machine isEqualToString:@"x86_64"] || [machine isEqualToString:@"i386"]) {
    return YES;
}

四、知识体系总览

说了这么多,我画了一张图帮你理清思路。移动端反外挂的三个核心方向,以及它们之间的关联:

移动端反外挂核心架构 越狱/root检测 注入检测 模拟器识别 iOS越狱检测 Android root检测 iOS注入检测 Android注入检测 iOS模拟器 Android模拟器 核心策略:多层防御 + 动态变化 文件检测 → 运行时检测 → 行为分析 → 云端联动 单一检测点容易被绕过,组合使用才能提高门槛 检测逻辑要分散,更新要频繁,让外挂作者疲于应对

五、实战建议

说了这么多理论,最后给点实在的。我这些年踩过的坑,总结成几条:

1. 检测代码要分散

别把所有检测逻辑写在一个函数里。外挂作者一旦Hook了那个函数,你的检测就全废了。我习惯把检测点分散到游戏的不同模块,甚至不同线程里。

2. 检测结果要上报

客户端检测到异常,别直接弹窗说“检测到外挂”。那样等于告诉外挂作者“你的方法有效”。我建议静默上报到服务器,让服务器做最终判定。

3. 定期更新检测库

外挂技术在进步,你的检测库也得跟着更新。我建议每个月至少更新一次检测特征库,包括新的越狱文件路径、新的模拟器特征等。

避坑指南:我曾经在某个项目里,把所有检测逻辑都写在了Application的onCreate里。结果外挂作者直接Hook了Application的构造函数,让整个检测流程都没执行。后来我改成在多个Activity的onCreate里分散检测,才解决了这个问题。

嗯,移动端反外挂这块,说白了就是一场持续的攻防战。没有一劳永逸的方案,只有不断迭代的策略。你想想看,外挂作者也是要吃饭的,他们也在研究你的检测逻辑。所以我们的目标不是100%防住,而是提高门槛,让外挂作者觉得“搞这个游戏不划算”。

好了,这一章就聊到这儿。记住:检测是基础,响应才是关键。下一章咱们聊聊检测到外挂之后,该怎么处理。

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