美文网首页iOS精选yue狱检测和反yue狱检测安全专题
iOS逆向:__restrict防止动态库注入的方案分析

iOS逆向:__restrict防止动态库注入的方案分析

作者: 康小曹 | 来源:发表于2021-03-05 16:46 被阅读0次

    一、基本使用

    1. 怎么用?

    很多第三方安全监测可能会碰到这种检测结果:

    安全检测

    如图,就是建议使用 -Wl,-sectcreate,__RESTRICT,__restrict,/dev/null 这条指令来解决注入风险;

    但是,这个方法真的已经被玩烂了,把 mach-O 文件拉出来,找个编辑器就可以直接改掉,也可以直接在 machOverview 中修改,如下:

    修改

    修改完成后用 machoverview 查看:


    修改之后

    改完之后,重签名即可走后面的破解流程。

    2. 进阶防护

    因为修改很容易,所以需要做防护。

    如果我们采用这种做法,可以确定,我们上架之前肯定会添加这个配置项。如果未被修改,这个配置项一定存在。如果不存在,那肯定是被 hack 了。所以,我们可以检测这个端是否存在来判断代码是否被注入,以此做一些防护。

    那么怎么判断呢?直接使用 dyld 中的 hasRestrictedSegment 方法即可;

    具体代码就省略了......网上很多资料......

    这里有几个重点:

    1. 最好对 hasRestrictedSegment 方法名进行修改,防止 hacker 直接检测并 hook 这个函数;
    2. 最好对 hasRestrictedSegment 方法的返回值进行修改,不要返回一个布尔值。因为这个函数被 hook 之后或者被修改成返回 YES 之后,那么多判断代码都没用了,可以写成返回特定字符串加解密的这种效果;
    3. 检测到被注入时,不要直接 exit(0),因为这样太明显了。安全攻防的核心不在于防护技术,而在于不被对方发现自己所使用的防护技术。说白了,这不是一个开门和堵门的博弈,而是一场猫抓老鼠的游戏。一旦门被暴露,那么门被开掉只是时间的问题,而且通常这个时间会很快;
    4. 基于第三点,微信的做法是,发现被注入之后,什么也不错,上报到服务器,然后直接做封号处理。这种做法简直就是大杀器......

    二、restrict原理分析

    首先,dyld 的加载流程就不赘述了,先来看 _main 函数。

    dyld360.18 源码中, 其 _main 函数中有这样一段代码:

    sProcessIsRestricted = processRestricted(mainExecutableMH, &ignoreEnvironmentVariables, &sProcessRequiresLibraryValidation);
    if ( sProcessIsRestricted ) {
    #if SUPPORT_LC_DYLD_ENVIRONMENT
            checkLoadCommandEnvironmentVariables();
    #endif 
            pruneEnvironmentVariables(envp, &apple);
            // set again because envp and apple may have changed or moved
        setContext(mainExecutableMH, argc, argv, envp, apple);
    }
    

    上述代码的意思是如果 sProcessIsRestricted == true,就执行 pruneEnvironmentVariables 函数,而 pruneEnvironmentVariables 函数:

    //
    // 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)
    {
            xxxx.....
    }
    

    从其描述以及函数的命名 prune 中可以看出,这个函数就是清楚环境变量中的配置,而插入动态库是根据 DYLD_INSERT_LIBRARIES 这条配置来插入的。

    因此,当 sProcessIsRestricted == true 时,这条配置就无效了,且会产生一些其他效果,比如禁止调试等。

    那么何时 sProcessIsRestricted == true ?其主要逻辑在 processRestricted 函数中,看关键代码:

    // all processes with setuid or setgid bit set are restricted
    if ( issetugid() ) {
        sRestrictedReason = restrictedBySetGUid;
        return true;
    }
        
    // <rdar://problem/13158444&13245742> Respect __RESTRICT,__restrict section for root processes
    if ( hasRestrictedSegment(mainExecutableMH) ) {
        // existence of __RESTRICT/__restrict section make process restricted
        sRestrictedReason = restrictedBySegment;
        return true;
    }
    

    两种情况下为 true:

    1. setugid;
    2. hasRestrictedSegment 返回 true;

    看注释也能知道点大概,但是很狗的是 Apple 把 rdar 上相关的案例都删除了,具体原因忘记了,所以我们往深了查,也不是很好查了。

    暂时先不管 setugid 是什么,此时就进入了熟悉的 hasRestrictedSegment,看关键 代码:

    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 = &sectionsStart[seg->nsects];
        for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
            if (strcmp(sect->sectname, "__restrict") == 0) 
                return true;
        }
    }
    

    其实到这里,还是不知道 __RESTRICT 怎么用,但是在后面版本的代码中,包含了 test 代码,意思是在特定模式下启动代码,虽然运行不了,但是根据注释能够大概猜出 __RESTRICT 怎么用:

    __RESTRICT

    这估计就是 -Wl,-sectcreate,__RESTRICT,__restrict,/dev/null 这条指令的由来吧。

    上图是 dyld400+以后版本才有,从这里其实也可以从侧面看出来这个 __restrict 已经不适用于 iOS了,而只适用于 Macos。这也是为什么配置 __restrict 之后,模拟器里面有效,但是真机跑确没有效果的原因,如图模拟器跑的效果就是:

    模拟器上的限制模式

    至于具体的验证步骤看下文。

    三、查看dyld源码版本

    观点:在 iOS10 之后,__restrict 就已经失效;

    思路:
    iOS10 之后肯定是因为 dyld 的代码发生了变化,特别是 __ restrict 相关的代码发生变化,这才会导致 __restrict 配置失效。如此,对比 iOS10 和 iOS9 的代码即可判断此观点是否真实;

    1. 查看 iOS10 中 dyly 源码的版本;
    2. dlyd 代码中并无头绪,直接在头文件中查看所提供的接口;
    3. 源码中直接搜索 version 相关;
    4. 有这个接口,可是返回的是 uint_32,其解析规则又是什么
    5. 去源码中查看这个接口的实现;
    6. 源码中查找是否有其他位置调用这个接口;

    1. otool工具dyly源码版本

    使用 otool -help 查看 otool 的指令有:

    otool

    其中就可以使用 -L 指令来查看 mach-O 文件所使用到的动态库:

    查看使用的动态库

    继续查看 libSystem.B.dyld,因为这个库其实就是打包了很多系统基础库,其中就有 dyld:


    dyld

    由此得到使用的 dlyd 源码版本是 655.1.1;

    但是,这样真的准确吗?otool 是一个 Xcode 提供的专门查看 Mach-O 文件的工具,还可以查看汇编代码,可以当反编译工具用。另外,machOverview 如果看不了的话,可以使用 otool 来看;但是具体的官方文档说明没有找到,所以这里的指令原理需要存疑;

    另外,很明显,这里的动态库显示的都是本地路径,动态库本来就是不会打包进入工程的,所以这里的版本号准确来说是自己机器上对应的动态库的版本。因此,运行在不同版本的真机上就会使用不同版本的 dyld。当运行在模拟器上是,dyld 会将加载流程交给 dyld_sim 来处理,那就更不一样了;

    2. dyld_sim的验证

    关于 dyld_sim 可以来验证一下,首先打个加载时机比较靠前的断点,就比如 libSystem.B.dylib 中的初始化方法会调用 obcj 库中的 _objc_init 方法,就这个断点吧:

    _objc_init断点

    断点之后使用 image list 查看当前所有镜像:


    image list

    其中几个关键点:

    1. 0x0000000108015000 为随机偏移(需要减去 100000000);
    2. 最先加在的镜像是 dyld;
    3. dyld 加载之后判断是模拟器,直接到模拟器相关文件夹中运行 dyld_sim;
    4. 断点中也可以看到具体的执行流程;

    总结一下,这里可以得出一个结论:

    • 模拟器中的主流程由 dyld_sim 来执行;

    我的电脑中有如下几个版本的 模拟器:


    模拟器版本

    所以,当我在不同版本的模拟器上运行时,将会使用这个文件下的 dyld_sim 来加载镜像(动态库);

    3. 查看 dyld_sim/dyld 版本号

    所以,需要知道模拟器中 dyld 具体的源码版本号,可以查看这个 dyld_sim 中使用的是哪个 dyld:

    otool -l filePath | grep -A 3 "LC_SOURCE_VERSION"
    

    注意这里是小写的 l,意思是打印 load command ,后面的指令表示提取对应段信息;

    结果:

    cmd LC_SOURCE_VERSION
    cmdsize 16
    version 433.8
    Load command 9
    

    其实这个指令就是从 load command 中取出数据,mach-O 也可以查看:

    mach-O查看源码版本

    同理可以查看 mac 上的 dyld 源码版本:

    caoxks-MBP:~ caoxk$ otool -l /usr/lib/dyld   | grep -A 3 "LC_SOURCE_VERSION"
    cmd LC_SOURCE_VERSION
    cmdsize 16
    version 655.1.1
    Load command 9
    

    从图上也可以看到,不同版本的模拟器会进入到不同版本的文件夹中找到对应的 dyld_sim 来执行,上图中则是 10.3 版本的模拟器中的 dyld 源码版本,再看个 iOS12 的:

    caoxks-MBP:~ caoxk$ otool -l /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim | grep -A 3 "LC_SOURCE_VERSION"
          cmd LC_SOURCE_VERSION
      cmdsize 16
      version 650.3.4
    Load command 9
    

    注意,这里查看的直接就是可执行的 dyld 文件,不是 libdyld.dylib;

    总结:

    1. 模拟器中,dyld 会将加载过程交给 dyld_sim 来处理;
    2. 使用 otool -l | grep 的方法可以提取 load command 中指定内容;
    3. macoverview 文件也可以直接查看 dyld/dyld_sim 文件的 load command 中的 resource version 字段来查看源码版本;

    4. 源码中查看如何运行时查看 dyld 版本

    上文可以使用 LC_SOURCE_VERSION 来查看版本号,所以先在源码中查找一下这个东西是怎么用的:

    LC_SOURCE_VERSION

    看了前后代码,感觉并看不懂,那么继续搜一下 version 试试,但是实在太多了。看看头文件中有没有暴露接口给我们用吧,结果在 dyld.h 中找到了:

    dyld.h

    那么试一下这个有用么:

    #import <mach-o/dyld.h>
    
    int32_t version1 = NSVersionOfRunTimeLibrary("dyld");
    int32_t version2 = NSVersionOfRunTimeLibrary("libdyld.dylib");
    NSLog(@"%d",version1);
    NSLog(@"%d",version2);
    

    结果使用 dyld、libdyld.dylib 都可以打印出来,可是结果有点蛋疼:

    2021-03-05 15:43:59.419 XKSpeechSynthesis[5633:22680115] 28379136
    2021-03-05 15:43:59.420 XKSpeechSynthesis[5633:22680115] 28379136
    

    两个结果一致,而且 API 中说明,如果没找到则会返回 -1,这就表示确实找到了 dyld 的版本号,但是这个整数是个啥意思呢?看源码吧:

    current_version

    如上图,这个 current_version 是关键,其定义如下:

    struct dylib {
        union lc_str  name;         /* library's path name */
        uint32_t timestamp;         /* library's build time stamp */
        uint32_t current_version;       /* library's current version number */
        uint32_t compatibility_version; /* library's compatibility vers number*/
    };
    

    但是我们需要 char 类型的,这个 int 类型的没卵用啊。这里有个思路,既然 version 能转回 xxx.xx.xx 的格式,那源码里面肯定有用到这个格式,那必定会有对 current_version 转换的代码,所以找找哪些地方用到了 current_version ,在 dyld_shared_cache_util 中找到关键的一条:

    解码源码

    如上图可知,这里应该是一个配置项,如果这个配置项开启了,则会打印动态库的版本号,所以最终运行时打印 dyld 的代码为:

    int32_t version = NSVersionOfRunTimeLibrary("dyld");
    
    if ( version != 0xFFFFFFFF ) {
        printf("(compatibility version %u.%u.%u, current version %u.%u.%u)\n",
           (version >> 16),
           (version >> 8) & 0xff,
           (version) & 0xff,
           (version >> 16),
           (version >> 8) & 0xff,
           (version) & 0xff);
    } else {
        printf("\n");
    }
    
    NSLog(@"%d",version);
    

    结果:

    (compatibility version 433.8.0, current version 433.8.0)
    

    总结:使用 NSVersionOfRunTimeLibrary + 格式化来动态打印 dyld 的源码版本;

    四、为什么会失效

    到目前为止,我们已经可以拿到 iOS9 和 iOS10 对应的 dyld 的源码了,终于可以开始干事了~~~

    直接运行时打印 iOS10 和 iOS9 对应 SDK 上使用的 dyld 版本即可,如果找不到,可以使用就近的版本。因为 Apple 不会将所有的代码都上传;

    这里,iOS10 最接近的可下载的版本是 dyld-433.5,而 iOS9 则是 dyld-360.18,所以接下来就是看源码的处理了;

    360.18 版本中 restrict 还可以使用,我们先来看 这份代码,如图:


    360.18

    而 433.5版本中:

    #if TARGET_IPHONE_SIMULATOR
    xxx
    #elif __IPHONE_OS_VERSION_MIN_REQUIRED
    xxx
    #elif __MAC_OS_X_VERSION_MIN_REQUIRED
        // any processes with setuid or setgid bit set or with __RESTRICT segment is restricted
        if ( issetugid() || hasRestrictedSegment(mainExecutableMH) ) {
            gLinkContext.processIsRestricted = true;
        }
    #endif
    

    如上,最关键的区别在于 433 版本中在调用 hasRestrictedSegment 时,添加了 __MAC_OS_X_VERSION_MIN_REQUIRED 的判断。从字面上理解,这个 __MAC_OS_X_VERSION_MIN_REQUIRED 就是有没有 macos 支持最低版本号,而 __IPHONE_OS_VERSION_MIN_REQUIRED 就 Xcode 中我们常用的:

    版本号

    但是具体这个宏定义哪来的:

    __config 找了一圈也不知道这个 __config 是哪来的,但是在注释中可以看到: llvm

    大概就是 LLVM 编译器相关的一些配置项吧。

    通过以下代码测试:

        int a = 10;
    #if __MAC_OS_X_VERSION_MIN_REQUIRED
        a = 20;
    #else
        a = 30;
    #endif
    

    跑在 iOS 项目中,即使是模拟器, a 都是 30,跑在 Mac 项目中,a 就是 20。这结果也可以验证上面的结论。

    所以:

    • dyld-433 中,只有在模拟器中 __restrict 字段才有用;

    这里有一点需要说明:

    模拟器中的 dyld 仍然是基于 MacOS 系统,所以这个宏定义肯定是有的。但是在模拟器上跑,为什么这个定义又没有呢?因为 dyld 和 项目本身是分隔开的,dlyd 作为一个已经编译好的可执行的二进制文件存在于 Mac 中。而在模拟器中跑 iOS 代码时,并没有重新编译并重新生成这个 dyld 可执行文件。所以,只有到真机上时,dyld 的这个MAC_OS 宏定义才不存在。

    五、总结

    __restrict 模式在 iOS10 之后就已经不适合用来防止动态库的注入了,不仅没有效果,还会影响基于模拟器下的正常开发。

    相关文章

      网友评论

        本文标题:iOS逆向:__restrict防止动态库注入的方案分析

        本文链接:https://www.haomeiwen.com/subject/jnjrqltx.html