数据, 术→技巧, 研发

iOS是否越狱判断方法

钱魏Way · · 2,115 次浏览

“越狱”在评估有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);
}

参考链接:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注