ios反越狱检测与检测剖析

一、 越狱检测与反检测及越狱社区未来发展

随着ios系统本身的逐步完善以及智能手机的发展步入成熟阶段, 普通用户对于越狱需求的逐步降低, 整个越狱社区略显疲态, 形成一个恶性循环, 这无论对于我们喜欢搞机爱好者, 还是喜欢研究的开发者来说, 都不是一个好消息.

其中特别是一些主流APP,如银行类, 游戏类, 抖音等使用技术手段进行越狱检测以区别对待不同用户群体的做法, 利用自身的市场垄断地位, 逼迫用户二选一, 给越狱社区的发展蒙上了一层阴影.

所以基于此,我想邀请大家建立一套持久的, 深层次的, 不可检测的反越狱检测模块. 以促进越狱社区的发展.

重点我们可以参考android系统的magisk hide, 当然由于ios本身的越狱和android的root还是有较大差异, android的root目前主要以厂商解锁bootloader后刷入自定义的boot镜像, 在启动阶段进行patch, 只需要获取到用户态的root身份即可. 而ios的越狱目前主流则以通过用户态的APP利用内核漏洞获取到内核权限patch内核代码实现. 并且root过程会产生和越狱行为相关的文件到磁盘. 所以综合来说, ios的越狱状态相比android会对系统造成更大的变化, 也更加容易被检测.

目前主流的越狱检测方式如下(水平有限莫喷):

1:基于文件的检测, 上面提到, 越狱过程中会产生一系列和越狱相关的文件到磁盘, 以及越狱后安装的各种框架, 插件, APP等.

虽然普通APP在沙盒中无法读写这些文件, 但是可以获取到这些文件的状态, 例如: 是否存在, 文件大小, 是否为符号链接等. 由此可以比较准确的判断手机是否被越狱过(可能无法区分当前越狱是否激活).

2:基于系统相关API的行为变更, 上面提到, 越狱需要利用内核漏洞patch内核相关代码, 以及可能会对系统其它相关服务进程进行patch修改, 那么必然会导致一些系统API的行为产生可预期的变化.

例如: fork, system等, (以及越狱程序一般会patch代码签名相关的组件, 也许可以检查这一点)

3:基于检测相关越狱框架和app的方式, 比如检测cydia的 url scheme是否可用, 检测当前进程是否有加载mobile substrate基板模块,越狱相关进程等.

基于此,我们便有了相应的通用处理方案, 先说第3点, 对于检测越狱相关app等, 可以采取随机改名移位, patch系统相关服务组件隐藏app等方式, 相对来说比较好处理, 而对于基板检测, 目前有substrate,substitute,libhooker等,我不确定是否默认会自动注入到所有APP进程, 可以通过改进过滤等方式, 针对需要隐藏越狱的APP不注入基板模块即可解决.

而对于第2种检测方式, 则可以通过patch内核和相关系统服务组件进程的方式, 针对需要隐藏越狱的APP, 执行原有代码流程逻辑, 即可规避该种检测方式.

对于第1种检测方式, 个人认为则是最难以处理的, ios系统越狱后, 不同的越狱程序, 安装了不同的越狱APP, 框架, 插件等, 对磁盘文件的改动, 都是不一样的. 我们既不能直接针对需要隐藏越狱的APP阻止访问磁盘系统, 也比较难以用通用性的方式隐藏所有和越狱相关的磁盘文件.

其中目前有一款开源的隐藏越狱插件kernbypass,

开源地址(https://github.com/akusio/KernBypass-Public) ,我大概看了一下原理和流程, 其注入了一个tweak到所有进程, 检查当前APP是否在需要隐藏越狱的名单中, 则暂停当前APP进程, 并通知其deamon程序通过内核端口tfp0修改内核中该APP进程的相关数据, 实现chroot当前APP进程的根目录到/private/var/MobileSoftwareUpdate/mnt1, 然后再恢复APP进程继续执行.

这款插件目前在我的iphone手机中测试对建行APP并不能有效, 而且其注入到每个进程的tweak dylib也极容易被针对性检测, 以及/private/var/MobileSoftwareUpdate/mnt1 在我的ios系统中查看到该目录为空, 是否意味着目标APP无法访问到任何根目录中的文件, 这也及其容易被针对性检测.

让我们来回想android的magisk hide是如何做的, magisk由于patch了boot启动文件, 在系统启动的早期阶段, 其利用linux的namespace mount构建了一个虚拟文件系统附加到系统原生的文件系统之上.

但是遗憾的是,目前来说ios的越狱都不是在系统的早期阶段, 而且ios的XUN内核也不支持namespace mount功能.

好在天无绝人之路,从ios10.3开始, 苹果采用了APFS磁盘文件系统格式, APFS有一个快照功能, 可以将一个磁盘分区的状态固定在某一个时间点, 并且支持单独以只读方式挂载磁盘快照(本身ios的rootfs就是以这种方式挂载的system分区). 所以我们也可以利用这一点, 针对特定APP来实现类似于systemless的反越狱检测方式.

当然一个人的力量是有限的,整个越狱社区的发展离不开众多开发者, 用户以及第三方的支持, 只有我们开发者使用技术的力量, 协助第三方壮大整个越狱生态, 才能让更多的用户选择越狱, 不排斥越狱. 才能避免被部分APP以强迫性行为倒逼我们用户进行二选一, 进而促进整个越狱社区的良性循环.

有兴趣的小伙伴可以加入我的越狱研究小群(1017751661)

=========================================================================================================

iOS的越狱检测和反越狱检测原理剖析

为什么要检测越狱?因为越狱后会大幅降低安全性。对于一些金融类的APP或者游戏类的,因为监管原因、资金安全问题,甚至防止使用越狱分析等,需要进行检测。不过其实越狱与反越狱就像矛与盾一样,都没有完美的方案。用一些反越狱插件可以防99%的越狱检测方式,本质上因为越狱后可以hook已知的所有检测越狱的方法,包括我下面的几种常用的。对于具体的反越狱插件可以用一些特定的方案来辅助检测。

具体代码清参考我的github

二、 越狱检测方案

1. 检测动态库

1.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;
}

1.2 判断是否注入了动态库

利用_dyld_get_image_name来获取动态库的名字,并查看是否有相关的动态库,这个相对来说最为准确,因为这个系统库运行的更早,且很多越狱的也需要依赖这个库的正常运行,所以更难被绕过

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;
}

2. 判断是否有越狱相关文件或权限

2.1 判断是否能打开越狱软件

利用URL Scheme来查看是否能够代开比如cydia这些越狱软件

    //Check cydia URL hook canOpenURL 来绕过
    if([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"cydia://package/com.avl.com"]])
    {
        return YES;
    }

    if([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"cydia://package/com.example.package"]])
    {
        return YES;
    }

2.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;
        }
    }

2.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;
    }

3. 利用系统命令来判断

3.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;
        }
    }

3.2 是否能够fork一个子进程

未越狱的设备是无法fork子进程的,可以通过这个检测,还有其他类似的命令:方法posix_spawn,kill,popen等

//Check process forking
    // hook fork
    int pid = fork();
    if(!pid)
    {
        exit(1);
    }
    if(pid >= 0)
    {
        return YES;
    }

4. 查看是否有异常类和异常的动态库

4.1 检测是否有异常类

//     查看是否有注入异常的类,比如HBPreferences 是越狱常用的类,这里无法绕过,只要多找一些特征类就可以,注意,很多反越狱插件会混淆,所以可能要通过查关键方法来识别
    NSArray *checksClass = [[NSArray alloc] initWithObjects:@"HBPreferences",nil];
    for(NSString *className in checksClass)
    {
      if (NSClassFromString(className) != NULL) {
        return YES;
      }
    }

4.2 检测是否有异常的动态库

这个和1.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;
  }
  
  
}

5. 检测是否在调试

5.1 查看是否有环境变量DYLD_INSERT_LIBRARIES

#pragma mark 通过环境变量DYLD_INSERT_LIBRARIES检测是否越狱
BOOL dyldEnvironmentVariables ()
{
    if(TARGET_IPHONE_SIMULATOR)return NO;
    return !(NULL == getenv("DYLD_INSERT_LIBRARIES"));
}

5.2 判断当前进程是否为调试模式

使用sysctl方法来获取当前进程的相关信息,从而确实是否在进行pTraced调试,具体参考sysctl,方法来自于官方https://developer.apple.com/library/archive/qa/qa1361/_index.html, 这里插一句,之前可以通过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 );
}

6. 阻止 DYLD_INSERT_LIBRARIES 生效 (iOS 10以下才有效)

反越狱插件基本上都是通过 DYLD_INSERT_LIBRARIES 来做注入的,这个是官方提供的一套修改动态库的方案,看一下相关说明:

DYLD_INSERT_LIBRARIES
    This is a colon separated list of dynamic libraries to load before the ones specified in the
    program. This lets you test new modules of existing dynamic shared libraries that are used in
    flat-namespace images by loading a temporary dynamic shared library with just the new modules.
    Note that this has no effect on images built a two-level namespace images  using a dynamic
    shared library unless DYLD_FORCE_FLAT_NAMESPACE is also used.

查看下dylib的源码,pruneEnvironmentVariables方法里,会移出DYLD开头的环境变量。

//
// For security, setuid programs ignore DYLD_* environment variables.
// Additionally, the DYLD_* enviroment variables are removed
// from the environment, so that any child processes don't see them.
//
static void pruneEnvironmentVariables(const char* envp[], const char*** applep)
{
    // delete all DYLD_* and LD_LIBRARY_PATH environment variables
  ***
    if ( removedCount != 0 ) {
        dyld::log("dyld: DYLD_ environment variables being ignored because ");
        switch (sRestrictedReason) {
            case restrictedNot:
                break;
            case restrictedBySetGUid:
                dyld::log("main executable (%s) is setuid or setgid\n", sExecPath);
                break;
            case restrictedBySegment:
                dyld::log("main executable (%s) has __RESTRICT/__restrict section\n", sExecPath);
                break;
            case restrictedByEntitlements:
                dyld::log("main executable (%s) is code signed with entitlements\n", sExecPath);
                break;
        }
    }
    
  ***
}

检测是否需要拒绝的代码在这里processRestricted,其中geteuid是禁止使用的,所以可以用hasRestrictedSegment方法可以处理。

static bool processRestricted(const macho_header* mainExecutableMH)
{
    // all processes with setuid or setgid bit set are restricted
    if ( issetugid() ) {
        sRestrictedReason = restrictedBySetGUid;
        return true;
    }
        
    const uid_t euid = geteuid();
    if ( (euid != 0) && hasRestrictedSegment(mainExecutableMH) ) {
        // existence of __RESTRICT/__restrict section make process restricted
        sRestrictedReason = restrictedBySegment;
        return true;
    }
    
#if __MAC_OS_X_VERSION_MIN_REQUIRED    
    // ask kernel if code signature of program makes it restricted
    uint32_t flags;
    if ( syscall(SYS_csops /* 169 */,
                0 /* asking about myself */,
                CS_OPS_STATUS,
                &flags,
                sizeof(flags)) != -1) {
        if (flags & CS_RESTRICT) {
            sRestrictedReason = restrictedByEntitlements;
            return true;
        }
    }
#endif
    return false;
}

检索hasRestrictedSegment,可以指定如果段里面有 __RESTRICT,并且节里面有 __restrict,就返回true

static bool hasRestrictedSegment(const macho_header* mh)
{
    const uint32_t cmd_count = mh->ncmds;
    const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(macho_header));
    const struct load_command* cmd = cmds;
    for (uint32_t i = 0; i < cmd_count; ++i) {
        switch (cmd->cmd) {
            case LC_SEGMENT_COMMAND:
            {
                const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
                
                //dyld::log("seg name: %s\n", seg->segname);
                if (strcmp(seg->segname, "__RESTRICT") == 0) {
                    const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
                    const struct macho_section* const sectionsEnd = §ionsStart[seg->nsects];
                    for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
                        if (strcmp(sect->sectname, "__restrict") == 0) 
                            return true;
                    }
                }
            }
            break;
        }
        cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
    }
        
    return false;
}

所以只需要在xcode中Other Linker Flags 加入 -Wl,-sectcreate,__RESTRICT,__restrict,/dev/null。创建一个空的section来解决(也可以放一个文件,利用getsectdata API获取),不过注意的是,千万不要在非发布环境的地方开启这个,因为会影响到比如Instruments等依赖动态注入来debug的工具,而且苹果在iOS10以后放弃了检测。具体可以参考:Blocking Code Injection on iOS and OS X。

注意: 新的dylib实现中,这个检测逻辑只有MAC可用,iPhone已经不可用了。

if __MAC_OS_X_VERSION_MIN_REQUIRED

三、防越狱检测的方案和对策

越狱注入的基本原理是:MobileSubstrate会将 SpringBoard 的 [FBApplicationInfo environmentVariables] 函式做 hook ,将环境变量DYLD_INSERT_LIBRARIES设定新增需要载入的动态库,但是应用的二进制包无需做任何变化,dyld会在载入应用的时候因为DYLD_INSERT_LIBRARIES去插入具体的库。 具体可以参考:https://itw01.com/84SJREC.html

2.1 shadow的防检测方法

有一些越狱的插件可以做到防越狱检测,这里以shadow为例,来解释下原理,知己知彼。shadow现在是开源的,可以在这里找到: https://github.com/jjolano/shadow/tree/old(用old分支)。

首先需要利用Tweak软件,大家可以参考这里iOS 越狱的Tweak开发,最主要的是使用cydia Substrate 软件来做动态库注入。具体使用可以参考这里cydiasubstrate,如果有时间会分析下具体的实现,这是个跨平台的开发方案,也支持Android平台。相关源码可以参考substrate.

简单来说就是设置环境变量,用DYLD_INSERT_LIBRARIES来加载subsrate的注入代码:
https://github.com/jevinskie/substrate/blob/97fa4bae349b867ae789bb756f6c45c311d16e7d/Environment.hpp#L25-L26。并利用__attribute__((constructor)),关键字hook的相关代码在mian函数执行前执行。

从这里分析可以知道,本质是通过DYLD_INSERT_LIBRARIES,详细的原理可以学习下这篇博文:macOS/OSX中的DYLDINSERTLIBRARIES DYLIB注入技术详解。所以可以利用上面提到的6\. 阻止DYLD_INSERT_LIBRARIES生效来防止。

然后看下源码,shadow主要逻辑为:

  1. shadow会维护一个列表,检索哪些文件是越狱需要保护的文件
  2. hook相关的类,如果要检索这些文件,就影藏,返回修改后的结果。

最主要hook以下的方法

  1. hook c的类,主要是各种判断文件权限和执行命令的方法,比如:
*   access
*   getenv
*   fopen
*   freopen
*   stat
*   dlopen
  1. hook_NSFileManager | NSFileHandle | NSDirectoryEnumerator | hook_NSFileVersion | NSBundle

  2. hook_NSURL

  3. hook_UIApplication

  4. hook_NSBundle

  5. hook_CoreFoundation

  6. hook UIImage

  7. hook NSMutableArray | NSArray | NSMutableDictionary | NSDictionary | NSString

  8. hook 第三方库检测方法

  9. 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框架和动态库的名称,shadow并不能所有都hook,他会把应用app的image加载后就停止了,防止检测的执行后注入的动态库被查找到:

    *   %hookf(const char * _Nonnull *, objc_copyImageNames, unsigned int *outCount) {
        const char * _Nonnull *ret = %orig;

        ```
        if(ret && outCount) {
            NSLog(@"copyImageNames: %d", *outCount);

            const char *exec_name = _dyld_get_image_name(0);
            unsigned int i;

            for(i = 0; i < *outCount; i++) {
                if(strcmp(ret[i], exec_name) == 0) {
                    // Stop after app executable.
                    *outCount = (i + 1);
                    break;
                }
            }
        }

        return ret;

        ```

        }

*   objc_copyClassNamesForImage 获取动态库里面对应的所有class名称

hook_dladdr dladdr可以用来获取方法或image对应的信息,比如所属的动态库的名称,这里hook如果是忽略的文件,则返回0,所以如果返回0,要再判断下是否数据真的是空的。

```
static int (*orig_dladdr)(const void *addr, Dl_info *info);
static int hook_dladdr(const void *addr, Dl_info *info) {
    int ret = orig_dladdr(addr, info);

    if(!passthrough && ret) {
        NSString *path = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:info->dli_fname length:strlen(info->dli_fname)];

        if([_shadow isImageRestricted:path]) {
            return 0;
        }
    }

    return ret;
}

```

2.3 如何防止shadow等插件绕过

几个策略:

  1. 检测这些插件的关键指纹,比如检测只有他们有的类, 参考4\. 查看是否有异常类和异常的动态库的实现
  2. 阻止DYLD_INSERT_LIBRARIES生效, 参考 6\. 阻止DYLD_INSERT_LIBRARIES生效 。(这个可以通过修改macho,重新打包来绕过)
  3. 生产发布前,使用objc_copyImageNames方法记录使用的所有动态库,做成白名单,在运行过程中,再运行objc_copyImageNames去查看当前的动态库是否一致

四、 参考

  1. Jailbreak Detection Methods
  2. system 函数被废除的替代方法
  3. https://github.com/theos/theos
  4. wiki: Cydia_Substrate
  5. LD_PRELOAD, DYLD_INSERT_LIBRARIES 和 Cydia Substrate
  6. Blocking Code Injection on iOS and OS X
  7. 动态库加载源码:dyld
  8. Simple code injection using DYLD_INSERT_LIBRARIES
  9. macOS/OSX中的DYLDINSERTLIBRARIES DYLIB注入技术详解。
  10. A security review of 1,300 AppStore applications
  11. Tweak原理&防护
  12. Apple open source

你可能感兴趣的:(ios反越狱检测与检测剖析)