数据, 术→技巧, 研发

iOS是否越狱判断方法

钱魏Way · · 3,126 次浏览
!文章内容如有错误或排版问题,请提交反馈,非常感谢!

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

参考链接:

发表回复

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