几乎所有的 iOS 越狱环境检测的文章都会提到了以下方法:

1、判断特征文件

/Applications/Cydia.app

/Library/MobileSubstrate

/usr/sbin/sshd

/usr/bin/ssh

...

2、判断特征环境变量

char *insertlib = getenv("DYLD_INSERT_LIBRARIES");

char *safemode = getenv("_MSSafeMode");

...

如果存在以上特征文件或环境变量,则说明该 iOS 系统为越狱状态;否则为非越狱状态。这个论断被众多文章传播,以至于很多人都将其用于在生产环境检查 iOS 越狱。但这两个检测方法真的完全靠谱吗?

特征文件检查

首先,我们需要统一下「越狱设备」的定义。从狭义上讲,「越狱设备」应当指当前时刻系统处于越狱状态。很多越狱方式都是手机重启后越狱状态失效,此时就不应该判定为越狱状态。

问题的关键在于,即使手机从越狱状态恢复为正常,很多情况下,类似 /Library/MobileSubstrate 这样的特征文件会一直存在。如果单纯的只是判定特征文件,那么就会将这种在历史上曾经越狱过而当前系统并不处于越狱状态的设备判定为「越狱」。

我们的判定系统,也曾经单纯地将命中特征文件视为「越狱」,后来逐渐将其视为一个弱特征。因为通过数据分析和用户反馈,我们发现产品的客户群体中有不少人出于各种各样的原因曾经将手机越狱,而我们又不愿意失去这些无害而真实的用户。

特征文件检查只能证明此设备曾经越狱过,并不能代表当前状态。这个问题更应该从实际业务需求上考虑要不要将其作为强特征处理。

特征环境变量

这个问题说来我也觉得很疑惑。好几年前,我在验证越狱检测方法时,验证过注入发生时能获取到正确的注入动态库:

char *insertlib = getenv("DYLD_INSERT_LIBRARIES");

但至少在一年前,我在分析问题时发现即使发生注入,DYLD_INSERT_LIBRARIES 环境变量的值依然为空。

这个问题实际上变成了:无论 iOS App 是有外部注入动态库,获取 DYLD_INSERT_LIBRARIES 环境变量的值总是为空。我尝试了在 iOS 12、iOS 14 系统上使用两种不同的越狱方法获得的测试结果都是如此。

我们可以不使用越狱手机做个简单的测试。在 Xcode 项目中添加 DYLD_PRINT_ENV 的 Environment Variables,值为 YES,同时打开 Main Thread Checker。运行 App 后,可以看到 Xcode 终端打印有:

DYLD_LIBRARY_PATH=/usr/lib/system/introspection

DYLD_INSERT_LIBRARIES=/Developer/usr/lib/libBacktraceRecording.dylib:/Developer/usr/lib/libMainThreadChecker.dylib:/Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib

在 App 中获取环境变量:

char *LIBRARY_PATH = getenv("DYLD_LIBRARY_PATH");

char *INSERT_LIBRARIES = getenv("DYLD_INSERT_LIBRARIES");

DYLD_LIBRARY_PATH 获取值正常,DYLD_INSERT_LIBRARIES的值依然为空。

从掌握的信息来看,getenv 函数调用并没有被 HOOK。尝试在 dyld 源码中也并没有找到到底是哪里的问题。这个现象更像是 dyld 调用 main 函数前,环境变量已经被截断了,因为如果直接从

int main(int argc, char * argv[])

main 函数的 argv 参数来观察,依然看不到 DYLD_INSERT_LIBRARIES 的影子。

现象就是这个现象,并没有再深入下去,如果有哪位大佬清楚这个问题的根因欢迎留言赐教。

DYLD_INSERT_LIBRARIES 有值可以确认发生了注入,但没有值不能证明没有发生注入,更不能说明是否越狱。