iOS逆向:__restrict防止动态库注入的方案分析
一、基本使用
1. 怎么用?
很多第三方安全监测可能会碰到这种检测结果:
安全检测如图,就是建议使用 -Wl,-sectcreate,__RESTRICT,__restrict,/dev/null
这条指令来解决注入风险;
但是,这个方法真的已经被玩烂了,把 mach-O 文件拉出来,找个编辑器就可以直接改掉,也可以直接在 machOverview 中修改,如下:
修改修改完成后用 machoverview 查看:
修改之后
改完之后,重签名即可走后面的破解流程。
2. 进阶防护
因为修改很容易,所以需要做防护。
如果我们采用这种做法,可以确定,我们上架之前肯定会添加这个配置项。如果未被修改,这个配置项一定存在。如果不存在,那肯定是被 hack 了。所以,我们可以检测这个端是否存在来判断代码是否被注入,以此做一些防护。
那么怎么判断呢?直接使用 dyld 中的 hasRestrictedSegment 方法即可;
具体代码就省略了......网上很多资料......
这里有几个重点:
- 最好对 hasRestrictedSegment 方法名进行修改,防止 hacker 直接检测并 hook 这个函数;
- 最好对 hasRestrictedSegment 方法的返回值进行修改,不要返回一个布尔值。因为这个函数被 hook 之后或者被修改成返回 YES 之后,那么多判断代码都没用了,可以写成返回特定字符串加解密的这种效果;
- 检测到被注入时,不要直接 exit(0),因为这样太明显了。安全攻防的核心不在于防护技术,而在于不被对方发现自己所使用的防护技术。说白了,这不是一个开门和堵门的博弈,而是一场猫抓老鼠的游戏。一旦门被暴露,那么门被开掉只是时间的问题,而且通常这个时间会很快;
- 基于第三点,微信的做法是,发现被注入之后,什么也不错,上报到服务器,然后直接做封号处理。这种做法简直就是大杀器......
二、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:
- setugid;
- 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 = §ionsStart[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 的代码即可判断此观点是否真实;
- 查看 iOS10 中 dyly 源码的版本;
- dlyd 代码中并无头绪,直接在头文件中查看所提供的接口;
- 源码中直接搜索 version 相关;
- 有这个接口,可是返回的是 uint_32,其解析规则又是什么
- 去源码中查看这个接口的实现;
- 源码中查找是否有其他位置调用这个接口;
1. otool工具dyly源码版本
使用 otool -help
查看 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
其中几个关键点:
- 0x0000000108015000 为随机偏移(需要减去 100000000);
- 最先加在的镜像是 dyld;
- dyld 加载之后判断是模拟器,直接到模拟器相关文件夹中运行 dyld_sim;
- 断点中也可以看到具体的执行流程;
总结一下,这里可以得出一个结论:
- 模拟器中的主流程由 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;
总结:
- 模拟器中,dyld 会将加载过程交给 dyld_sim 来处理;
- 使用 otool -l | grep 的方法可以提取 load command 中指定内容;
- 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 之后就已经不适合用来防止动态库的注入了,不仅没有效果,还会影响基于模拟器下的正常开发。