欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

iOS逆向工程:解析__restrict如何防止动态库注入

最编程 2024-01-14 14:50:36
...

一、基本使用

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 之后就已经不适合用来防止动态库的注入了,不仅没有效果,还会影响基于模拟器下的正常开发。