“越狱”在评估有 Appstore 时就已经存在,当时很多人越狱的目的是为了安装收费的应用或游戏。随着 Appstore 应用的丰富及免费 APP 的增多,已经很少有用户为了牺牲手机的安全性来的进行越狱了。另外一方面,越狱的设备可以随意安装任何软件或脚本,也给黑产带来了方便之门。

iOS越狱判断方法
有时我们的应用希望知道安装的设备是否已经越狱了,以下是整理的一些判断方法:
检测动态库
1)stat 是否是系统的库,并利用 stat 来检测一些特定的文件权限
stat 命令时 OS 系统中用来判断文件信息的,但是对于私有的路径调用命令返回的是 -1,如果越狱后,因为权限变化,可以通过stat返回私有目录下的文件信息。
BOOL isStatNotSystemLib(){
if(TARGET_IPHONE_SIMULATOR) return NO;
int ret;
Dl_info dylib_info;
int (*func_stat)(const char *, struct stat *) = stat;
if((ret = dladdr(func_stat, &dylib_info))){
NSString *fName = [NSString stringWithUTF8String:dylib_info.dli_fname];
if(![fName isEqualToString:@"/usr/lib/system/libsystem_kernel.dylib"]){
return YES;
}
}
char *JbPaths[] = {"/Applications/Cydia.app",
"/usr/sbin/sshd",
"/bin/bash",
"/etc/apt",
"/Library/MobileSubstrate",
"/User/Applications/"};
for(int i = 0; i< sizeof(JbPaths)/sizeof(char *); i++){
struct stat stat_info;
if(0 == stat(JbPaths[i], &stat_info)){
return YES;
}
}
return NO;
}
2)判断是否注入了动态库
BOOL isInjectedWithDynamicLibrary()
{
int i = 0;
char *substrate = "/Library/MobileSubstrate/MobileSubstrate.dylib";
while(true){
//hook_dyld_get_image_name 方法可以绕过
const char *name = _dyld_get_image_name(i++);
if(name == NULL){
break;
}
if(name != NULL){
if(strcmp(name, substrate) == 0){
return YES;
}
}
}
return NO;
}
判断是否有越狱相关文件或权限
1)判断是否能打开越狱软件
大部分越狱设备会自动 cydia,利用 URLScheme 来查看是否能够打开比如 cydia 这些越狱软件。
-(BOOL)isJailBreak
{
if([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"cydia://"]]){
NSLog(@"The device is jailbroken!");
return YES;
}
NSLog(@"The device is NOT jailbroken!");
return NO;
}
2)判断是否可以访问一些越狱的文件
越狱后会产生额外的文件,通过判断是否存在这些文件来判断是否越狱了,可以用 fopen 和 FileManager 两个不同的方法去获取。
BOOL fileExist(NSString *path)
{
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDirectory = NO;
if ([fileManager fileExistsAtPath:path isDirectory:&isDirectory]) {
return YES;
}
return NO;
}
BOOL directoryExist(NSString *path)
{
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDirectory = YES;
if ([fileManager fileExistsAtPath:path isDirectory:&isDirectory]) {
return YES;
}
return NO;
}
BOOL canOpen(NSString *path)
{
FILE *file = fopen([path UTF8String], "r");
if (file == nil) {
return fileExist(path) || directoryExist(path);
}
fclose(file);
return YES;
}
NSArray *checks = [[NSArray alloc] initWithObjects:@"/Application/Cydia.app",
@"/Library/MobileSubstrate/MobileSubstrate.dylib",
@"/bin/bash",
@"/usr/sbin/sshd",
@"/etc/apt",
@"/usr/bin/ssh",
@"/private/var/lib/apt",
@"/private/var/lib/cydia",
@"/private/var/tmp/cydia.log",
@"/Applications/WinterBoard.app",
@"/var/lib/cydia",
@"/private/etc/dpkg/origins/debian",
@"/bin.sh",
@"/private/etc/apt",
@"/etc/ssh/sshd_config",
@"/private/etc/ssh/sshd_config",
@"/Applications/SBSetttings.app",
@"/private/var/mobileLibrary/SBSettingsThemes/",
@"/private/var/stash",
@"/usr/libexec/sftp-server",
@"/usr/libexec/cydia/",
@"/usr/sbin/frida-server",
@"/usr/bin/cycript",
@"/usr/local/bin/cycript",
@"/usr/lib/libcycript.dylib",
@"/System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist",
@"/System/Library/LaunchDaemons/com.ikey.bbot.plist",
@"/Applications/FakeCarrier.app",
@"/Library/MobileSubstrate/DynamicLibraries/Veency.plist",
@"/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist",
@"/usr/libexec/ssh-keysign",
@"/usr/libexec/sftp-server",
@"/Applications/blackra1n.app",
@"/Applications/IntelliScreen.app",
@"/Applications/Snoop-itConfig.app"
@"/var/lib/dpkg/info",nil];
//Check installed app
for (NSString *check in checks)
{
if (canOpen(check))
{
return YES;
}
}
3)查看是否有权限写入私有目录
通过检测是否可以写入私有目录来判断,是否越狱了
NSString *path = @"/private/avl.txt";
NSFileManager *fileManager = [NSFileManager defaultManager];
@try {
NSError *error;
NSString *test = @"AVL was here";
[test writeToFile:path atomically:NO encoding:NSStringEncodingConversionAllowLossy error:&error];
[fileManager removeItemAtPath:path error:nil];
if (error == nil)
{
return YES;
}
return NO;
} @catch (NSException *exception) {
return NO;
}
利用系统命令来判断
1)通过lstat命令来判断系统的一些目录是否存在还是变成了链接
越狱后会变动一些文件,这些文件目录会迁移到其他区域,但是原来的文件位置必须有效,所以会创建符号链接,链接到原来的路径,我们可以检测这些符号链接是否存在,存在说明就越狱了。
//symlink verification
struct stat sym;
//hook lstat可以绕过
if (lstat("/Applications", &sym) || lstat("/var/stash/Library/Ringtones", &sym) ||
lstat("/var/stash/Library/Wallpaper", &sym) ||
lstat("/var/stash/usr/include", &sym) ||
lstat("/var/stash/usr/libexec", &sym) ||
lstat("/var/stash/usr/share", &sym) ||
lstat("/var/stash/usr/arm-apple-darwin9", &sym))
{
if (sym.st_mode & S_IFLNK)
{
return YES;
}
}
2)是否能够fork一个子进程
一些越狱工具会移除沙盒的限制,使程序可以不受限制的运行,这里要说的是关于fork函数的限制。fork函数可以允许你的程序生成一个新的进程,如果沙盒被破坏或者程序在沙盒外运行,那么fork函数就会成功执行,如果沙盒没有被篡改则fork函数执行失败。这里我们通过fork()的返回值判断子进程是否成功,程序代码如下:
#!c #include#include static inline int sandbox_integrity_compromised(void) __attribute__((always_inline)); int sandbox_integrity_compromised(void){ int result = fork(); if (!result) exit(0); if (result >= 0) return 1; return 0; } int main(int argc, char *argv[]){ if (sandbox_integrity_compromised()) { printf("Device is JailBroken\n"); }else{ printf("Device is not JailBroken\n"); } return 0; }
查看是否有异常类和异常的动态库
1)检测是否有异常类
查看是否有注入异常的类,比如HBPreferences是越狱常用的类,这里无法绕过,只要多找一些特征类就可以,注意,很多反越狱插件会混淆,所以可能要通过查关键方法来识别。
NSArray *checksClass = [[NSArray alloc] initWithObjects:@"HBPreferences", nil];
for (NSString *className in checksClass)
{
if (NSClassFromString(className) != NULL) {
return YES;
}
}
2)检测是否有异常的动态库
这个和检测注入动态库的区别是,一般反越狱插件会hook_dyld_get_image_name这个方法,把越狱使用的一些动态库给影藏掉(比如返回其他动态库名称,或者返回正常的),导致匹配不到,可以利用image加载时的回调来从MachOHeader中去动态库信息,需要注意的是使用dladdr检测库信息的时候,也可能被强制返回错误,需要进一步做一下判断,具体看下面代码。
+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_dyld_register_func_for_add_image(_check_image);
});
}
//监听image加载,从这里判断动态库是否加载,因为其他的检测动态库的方案会被hook
static void _check_image(const struct mach_header *header,
intptr_t slide){
//hook Image load
if (SCHECK_USER){
//检测后就不在检测
return;
}
//检测的lib
NSSet *dylibSet = [NSSet setWithObjects:
@"/usr/lib/CepheiUI.framework/CepheiUI",
@"/usr/lib/libsubstitute.dylib"
@"/usr/lib/substitute-inserter.dylib",
@"/usr/lib/substitute-loader.dylib",
nil];
Dl_info info;
//0表示加载失败了,这里大概率是被hook导致的
if (dladdr(header, &info) == 0){
char *dlerro = dlerror();
//获取失败了但是返回了dli_fname,说明被人hook了,目前看的方案都是直接返回0来绕过的
if (dlerro == NULL && info.dli_fname != NULL){
NSString *libName = [NSString stringWithUTF8String:info.dli_fname];
//判断有没有在动态列表里面
if ([dylibSet containsObject:libName]){
SCHECK_USER = YES;
}
}
return;
}
}
检测是否在调试
1)查看是否有环境变量DYLD_INSERT_LIBRARIES
#pragma mark 通过环境变量DYLD_INSERT_LIBRARIES检测是否越狱
BOOL dyldEnvironmentVariables()
{
if (TARGET_IPHONE_SIMULATOR) return NO;
return !(NULL == getenv("DYLD_INSERT_LIBRARIES"));
}
2)判断当前进程是否为调试模式
使用sysctl方法来获取当前进程的相关信息,从而确实是否在进行pTraced调试,具体参考sysctl。
BOOL isDebugged()
{
int junk;
int mib[4];
struct kinfo_proc info;
size_t size;
info.kp_proc.p_flag = 0;
mib[0] = CTL_KERN;
mib[1] = KERN_PROC;
mib[2] = KERN_PROC_PID;
mib[3] = getpid();
size = sizeof(info);
junk = sysctl(mib, sizeof(mib)/sizeof(*mib), &info, &size, NULL, 0);
assert(junk == 0);
return ((info.kp_proc.p_flag & P_TRACED) != 0);
}
反越狱检测及应对方案
有一些越狱的插件可以做到防越狱检测,这里以shadow为例,来解释下原理,知己知彼。
shadow反越狱主要逻辑为:
- 维护一个列表,检索哪些文件是越狱需要保护的文件
- hook相关的类,如果要检索这些文件,就影藏,返回修改后的结果。
最主要hook以下的方法:
- hook c的类,主要是各种判断文件权限和执行命令的方法,比如:access、getenv、fopen、freopen、stat、dlopen
- hook _NSFileManager|NSFileHandle|NSDirectoryEnumerator|hook _NSFileVersion|NSBundle
- hook _NSURL
- hook _UIApplication
- hook _NSBundle
- hook _CoreFoundation
- hook UIImage
- hook NSMutableArray|NSArray|NSMutableDictionary|NSDictionary|NSString
- hook 第三方库检测方法,比如AppsFlyerUtils、WXOMTAEnv
- hook hook_debugging
- sysctl主要用来检测是否当前进程挂载了P_TRACED
- getppid返回当前的pid
- _ptrace
- hook _dyld_image。hook image动态加载的方法
- _dyld_image_count获取image的数量
- _dyld_get_image_name获取动态库的名字
- hook _dyld_dlsym。hook用来检测是否可以加载动态库。功能和dlopen一样
- hook 系统一些私有方法:vfork|fork|hook _popen(打开管道)
- hook runtime
- objc_copyImageNames hook获取所有加载的Objective-C框架和动态库的名称
- objc_copyClassNamesForImage获取动态库里面对应的所有class名称
- hook _dladdr dladdr可以用来获取方法或image对应的信息,比如所属的动态库的名称,这里hook如果是忽略的文件,则返回0,所以如果返回0,要再判断下是否数据真的是空的。
如何绕过反检测:
- 检测这些插件的关键指纹,比如检测只有他们有的类。比如,查看是否有异常类和异常的动态库的实现
- 阻止DYLD_INSERT_LIBRARIES生效(这个可以通过修改macho,重新打包来绕过)
- 生产发布前,使用objc_copyImageNames方法记录使用的所有动态库,做成白名单,在运行过程中,再运行objc_copyImageNames去查看当前的动态库是否一致
- 采用汇编指令(SVC)代替函数调用,来绕过Hook。
示例:
#include <stdlib.h>
uint32_t test(){
char* filename = "/var/lib/dpkg/status";
volatile uint32_t result = -1;
volatile uint64_t nzcv = 0;
#if __arm64__
asm volatile(
"mov x0, %[file]\n"
"mov x1, 0\n"
"mov x16, #5\n"
"svc #42\n"
"mov %w[res], w0\n"
"mrs %[c], nzcv"
: [res] "=r" (result), [c] "=r" (nzcv)
: [file] "r" (filename)
: "x0", "x1", "x16", "memory", "cc"
);
#endif
uint32_t cc = (nzcv >> 29) & 1;
return result | (cc<< 31);
}
参考链接:



