“越狱”在评估有 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); }
参考链接: