“越狱”在评估有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,利用URL Scheme来查看是否能够打开比如cydia这些越狱软件。
- (BOOL)isJailBreak { if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"cydia://"]]) { NSLog(@"The device is jail broken!"); return YES; } NSLog(@"The device is NOT jail broken!"); 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 <stdio.h> #include <stdlib.h> 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加载时的回调来从MachO Header中去动态库信息,需要注意的是使用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); }
参考链接: