CydiaSubstrate 引导过程

这里是基于老版本开源的 CydiaSubstrate 源码进行分析,一定程度上可以印证现版本的 CydiaSubstrate 注入流程。
为了解答下面的问题:

CyidaSubstrate 是什么时候被加载的?
CydiaSubstrate 是怎么注入到每一个 APP 中的?

Cydia 结构

在越狱手机的 /Library/Frameworks/CydiaSubstrate.framework,可以看到CyidaSubstrate 主要分为以下几个部分:

  • CydiaSubstrate(libsubstrate.dylib) 提供核心 MSHookFunctionMSHookMessageEx 等 hook 功能
  • SubstrateBootstrap.dylib 提供 CydiaSubstrate 的引导
  • SubstrateLauncher.dylib 用于启动引导
  • SubstrateLoader.dylib 负责加载 /Library/MobileSubsrate/ 目录下的对应动态库

查找启动相关的代码

为了了解每一个对应的二进制是如何产生的,就需要去检查它是如何被编译出来的。
查看它的编译文件 darwin.mk

1
2
3
4
5
6
7
8
9
10
11
libsubstrate.dylib: DarwinFindSymbol.o DarwinInjector.o ObjectiveC.o $(lsubstrate)
$(cycc) -dynamiclib $(hde64c) -lobjc -install_name $(framework)/CydiaSubstrate
SubstrateBootstrap.dylib: Bootstrap.o
$(cycc) -dynamiclib
SubstrateLauncher.dylib: DarwinLauncher.o $(lsubstrate)
$(cycc) -dynamiclib $(hde64c)
SubstrateLoader.dylib: DarwinLoader.o Environment.o
$(cycc) -dynamiclib -framework CoreFoundation

可以检索到对应的文件,我们的关键带你就落在下面这些文件上

1
2
3
Bootstrap.cpp
DarwinLauncher.cpp
DarwinLoader.cpp

Boostrap.cpp

文件的源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "CydiaSubstrate.h"
#include <unistd.h>
MSInitialize { //__attribute__((__constructor__)) static void _MSInitialize(void)
if (getenv("MSExitZero") != NULL) {
// skeels / planetbeing / rpetrich <- reporting SMS crash
if (dlopen("/System/Library/PrivateFrameworks/Search.framework/AppIndexer", RTLD_LAZY | RTLD_NOLOAD) != NULL)
_exit(EXIT_FAILURE);
_exit(EXIT_SUCCESS);
}
// Skype
if (dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY | RTLD_NOLOAD) == NULL)
return;
// Maps
dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", RTLD_LAZY | RTLD_GLOBAL);
dlopen("/Library/Frameworks/CydiaSubstrate.framework/Libraries/SubstrateLoader.dylib", RTLD_LAZY | RTLD_GLOBAL);
}

做的工作很简单,即动态加载一些必要的动态库,并且核心是为了引导 SubstrateLoader.dylib 的加载。

DarwinLoader.cpp

这个动态库被加载时,会做安全模式的检查,如果不是安全模式,则遍历

1
/Library/MobileSubsrate/DynamicLibraries/

目录下的所有 *.plist 文件,根据文件描述的信息来选择加载动态库。这就是为什么我们编写 Cydia 插件时,我们的代码可以正确的被加载并且完成注入。

在安全模式下会跳过加载的流程,为了处理因为一些插件崩溃而导致所有插件不可用的情况,为了可以在安全模式下删除或者调整崩溃的插件。

不过这对我们研究启动过程关系不大。

DarwinLauncher.cpp

下面取自初始化部分的源码,功能是 hook posix_spawn 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
MSInitialize {
// 获取 posix_spawn 的符号信息,拿到对应的实现
Dl_info info;
if (dladdr(reinterpret_cast<void *>(&$posix_spawn), &info) == 0)
return;
void *handle(dlopen(info.dli_fname, RTLD_NOLOAD));
// before we unload the previous version, we hook posix_spawn to call our replacements
// the original posix_spawn (from Apple) is kept in _MSPosixSpawn for use by new versions
// 核心部分:替换系统的 posix_spawn 的实现,为了在调用前做一些工作
if (const char *cache = getenv("_MSPosixSpawn")) {
MSReinterpretAssign(_posix_spawn, strtoull(cache, NULL, 0));
MSHookFunction(&posix_spawn, &$posix_spawn);
} else {
MSHookFunction(&posix_spawn, MSHake(posix_spawn));
char cache[32];
sprintf(cache, "%p", _posix_spawn);
setenv("_MSPosixSpawn", cache, false);
}
// ... 其他清理代码
}

在更替的实现中,核心工作就是在读取环境变量 DYLD_INSERT_LIBRARIES ,向其中添加 /Library/MobileSubstrate/MobileSubstrate.dylib,并且把这个参数放在了变量的第一位,保证其能第一个加载。

为什么是 MobileSubstrate.dylib

在真机的调试中进行查找:

1
2
3
4
5
6
7
8
9
10
iPhone:/ root# ls -al /Library/MobileSubstrate/MobileSubstrate.dylib
lrwxr-xr-x 1 root staff 79 Mar 16 2017 /Library/MobileSubstrate/MobileSubstrate.dylib -> /Library/Frameworks/CydiaSubstrate.framework/Libraries/SubstrateInjection.dylib
iPhone:/ root# ls -al /Library/Frameworks/CydiaSubstrate.framework/Libraries/
total 348
drwxr-xr-x 2 root staff 204 Mar 16 2017 .
drwxr-xr-x 5 root staff 238 Mar 16 2017 ..
-rwxr-xr-x 1 root staff 99056 Sep 13 2016 SubstrateBootstrap.dylib
lrwxr-xr-x 1 root staff 24 Mar 16 2017 SubstrateInjection.dylib -> SubstrateBootstrap.dylib
-rwxr-xr-x 1 root staff 139296 Sep 13 2016 SubstrateLauncher.dylib
-rwxr-xr-x 1 root staff 103536 Sep 13 2016 SubstrateLoader.dylib

从输出可以看到,MobileSubstrate.dylib 链接到了 SubstrateInjection.dylib,然后后者有链接到了 SubstrateBoostrap.dylib

所以 hook 这个函数是为了 利用 DYLD_INSERT_LIBRARIESSubstrateBoostrap.dylib 注入到程序中然后引导其他的动态库。

至此似乎就结束了,但是还有一些问题没有解决:

是谁加载的 SubstrateLauncher.dylib
为什么要 hook posix_spwan ?

寻找 SubstrateLauncher 的启动源头

继续查看 darwin.mk 来寻找线索,下面的一个编译目标比较引人注意:

1
2
3
4
5
extrainst_ postrm: LaunchDaemons.o Cydia.o
deb: ios extrainst_ postrm
./package.sh i386
./package.sh arm

control.tar.gz 则包含了一个 deb 安装的时候所需要的控制信息。一般有 5 个文件:control,用了记录软件标识,版本号,平台,依赖信息等数据;preinst,在解包data.tar.gz 前运行的脚本;postinst,在解包数据后运行的脚本;prerm,卸载时,在删除文件之前运行的脚本;postrm,在删除文件之后运行的脚本;在 Cydia 系统中,Cydia 的作者 Saurik 另外添加了一个脚本,extrainst_,作用与 postinst 类似。
摘自百度百科

由此可知,这两个是在 Cydia 被安装时做了一些工作。

extrainst_.mm

针对 arm 编译条件的跳转,观察到函数执行了这个部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
static int InstallSemiTether() {
MSClearLaunchDaemons();
NSFileManager *manager([NSFileManager defaultManager]);
NSError *error;
// 下面的内容是因为一些BUG,需要对库进行一下拷贝以期望能正确运行
// 核心是获取 SubstrateLauncher_
// 即 "/Library/Frameworks/CydiaSubstrate.framework/Libraries/SubstrateLauncher.dylib"
NSString *temp([NSString stringWithFormat:@"/tmp/ms-%f.dylib", [[NSDate date] timeIntervalSinceReferenceDate]]);
NSString *dylib;
if ([manager copyItemAtPath:@ SubstrateLauncher_ toPath:temp error:&error])
dylib = temp;
else {
fprintf(stderr, "unable to copy: %s\n", [[error description] UTF8String]);
// XXX: this is not actually reasonable
dylib = @ SubstrateLauncher_;
temp = nil;
}
// 用 system 函数执行命令,这个命令用到了 cynject 这是个疑点
system([[@"/usr/bin/cynject 1 " stringByAppendingString:dylib] UTF8String]);
// if we are unable to remove the file copied into /tmp, it is interesting, but harmless
if (temp != nil && ![manager removeItemAtPath:temp error:&error])
if (unlink([temp UTF8String]) == -1)
fprintf(stderr, "unable to remove: (%s):%d\n", [[error description] UTF8String], errno);
// 核心,这里读取了 "/etc/launchd.conf" 的配置信息
NSString *config([NSString stringWithContentsOfFile:@ SubstrateLaunchConfig_ encoding:NSNonLossyASCIIStringEncoding error:&error]);
// XXX: if the file fails to load, it might not be missing: it might be unreadable for some reason
if (config == nil)
config = @"";
NSArray *lines([config componentsSeparatedByString:@"\n"]);
NSMutableArray *copy([lines mutableCopy]);
[copy removeObject:@""];
// 向其中添加 SubstrateBootstrapExecute_
// 即 bsexec .. /usr/bin/cynject 1 SubstrateLauncher_
if ([lines indexOfObject:@ SubstrateBootstrapExecute_] == NSNotFound)
[copy addObject:@ SubstrateBootstrapExecute_];
[copy addObject:@""];
if (![copy isEqualToArray:lines])
[[copy componentsJoinedByString:@"\n"] writeToFile:@ SubstrateLaunchConfig_ atomically:YES encoding:NSNonLossyASCIIStringEncoding error:&error];
return 0;
}

这里看到了安装程序对启动程序作了一些处理,位置是 /etc/launchd.conf。但在实机中并没有出现,但是相应的,我们有如下的发现:

1
2
3
iPhone:/ root# cat /etc/rc.d/substrate
#!/bin/bash
exec /usr/bin/cynject 1 /Library/Frameworks/CydiaSubstrate.framework/Libraries/SubstrateLauncher.dylib

在系统开机时,系统会自动跑这个脚本,调用了 cynjectSubstrateLauncher.dylib 做处理。

原来是在开机的时候作了一些动作,但是具体是什么动作,还需要继续挖掘。

cynject.cpp

以下是其源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "CydiaSubstrate.h"
#include <stdio.h>
int main(int argc, const char *argv[]) {
if (argc != 3) {
fprintf(stderr, "usage: %s <pid> <dylib>\n", argv[0]);
return 1;
}
pid_t pid(strtoul(argv[1], NULL, 10));
const char *library(argv[2]);
if (!MSHookProcess(pid, library)) {
fprintf(stderr, "MSHookProcess() failed.\n");
return 1;
}
return 0;
}

代码非常简单。其中展现了这个程序的用法,第一个参数是 pid,第二个参数是动态库路径。
核心调用了 MSHookProcess 函数。那么焦点就在这了。

跳转到对应函数,该函数实现在 DarwinInjector.cpp 中。
到这里,从函数名到文件名,可以猜测,这个函数将动态库注入到了这个进程当中。
观察一下刚才那条命令:

1
exec /usr/bin/cynject 1 /Library/Frameworks/CydiaSubstrate.framework/Libraries/SubstrateLauncher.dylib

注入的程序被硬编码为 1 ,因为这个一定是 launchd 的 pid,而且这个进程在整个系统关机是不会消亡的,保证了注入代码的常驻。

结论

现在我们可以回答我们之前的疑问了。

CyidaSubstrate 是什么时候被加载的?

CyidaSubstrate 的加载时是在程序启动时,环境变量DYLD_INSERT_LIBRARIES被设置为 SubstrateBootrap.dylib 来保证 Loader 被加载。

CydiaSubstrate 是怎么注入到每一个 APP 中的?

这个问题题的解答就在 Cydia 入侵了 launchd 进程,替换了它的 posix_spawn 函数,因为每一个进程都诞生于 launchdlaunchd 利用 posix_spawn 中调用 fork() 来产生新的进程。而 Cydia 拦截这个入口的函数,这个时候将 SubstrateBootstrap.dylib注入到环境中,引导组件的加载。

Extra

对于 Cydia 如何对各种东西进行 hook,之后会有下一篇文章进行探索。