本文以coolstar系越狱为基础讨论
背景
本文由如何优雅的在LLDB里dumpdecrypted中tweak loader优先初始化的一个思考而来,里面提出了一个疑问在于,tweak loader到底何时加载到进程以及何时被初始化的。而在去探索这个答案的过程中,发现背后涉及到的却是和整个越狱相关,正好借此去深入了解越狱的相关细节。
Tweak loader
我最开始搜到的源码是:https://github.com/chr1s0x1/TweakInject
代码本身很简单,就是去/Library/MobileSubstrate/DynamicLibraries
下面去搜索,通过plist文件里面的字段决定是否将dylib加载到当前进程中,就和平时开发tweak去hook那些app的过程一样。不过我感到疑问的地方在于那本身这个tweak loader dylib又是谁以及何时被加载到进程中的呢。这里不难想到应该就是越狱后支持hook环境相关,所以我又去翻阅相关越狱源码。
Electra
我研究的越狱源码是:GitHub - coolstar/electra: Electra iOS 11.0 - 11.1.2 jailbreak toolkit based on async_awake
至于为什么会选择Electra,而且是早期的一个版本?有以下几点原因:
- coolstar由于在开发Electra后saurik没有提供substrate的支持,所以后面自己实现了完整的hook环境,这里面很多东西都是重新研究实现的,保留了很多当时的曲折过程,而这些恰好很便于去学习。
- 代码开源,而且早期一切都是以实用为主,代码都比较有代表性。
pspawn_payload
回到之前的问题,tweak loader是谁负责加载的呢?最直接相关的代码在:
代码里面的SBInject.dylib
就是tweak loader,如下
#define SBINJECT_PAYLOAD_DYLIB "/usr/lib/SBInject.dylib"
这个模块叫做pspawn_payload
,你在coolstar系越狱里进程模块里面就会有这个模块名。
下面是该模块初始化的代码
__attribute__ ((constructor))
static void ctor(void) {
if (getpid() == 1) {
current_process = PROCESS_LAUNCHD;
pthread_t thd;
pthread_create(&thd, NULL, thd_func, NULL);
} else {
current_process = PROCESS_XPCPROXY;
rebind_pspawns();
}
}
代码就是如果当前进程不是launchd,就hook pspawns函数。pspawns正是创建一个新进程的函数,所以hook以后会在创建前注入dylib。下面简单列举里面几个关键地方的代码
int fake_posix_spawn_common(pid_t * pid, const char* path, const posix_spawn_file_actions_t *file_actions, posix_spawnattr_t *attrp, char const* argv[], const char* envp[], pspawn_t old) {
...
if (strcmp(path, "/usr/libexec/amfid") == 0) {
DEBUGLOG("Starting amfid -- special handling");
inject_me = AMFID_PAYLOAD_DYLIB;
} else {
inject_me = SBINJECT_PAYLOAD_DYLIB;
}
...
int envcount = 0;
if (envp != NULL){
DEBUGLOG("Env: ");
const char** currentenv = envp;
while (*currentenv != NULL){
DEBUGLOG("\t%s", *currentenv);
if (strstr(*currentenv, "DYLD_INSERT_LIBRARIES") == NULL) {
envcount++;
}
currentenv++;
}
}
char const** newenvp = malloc((envcount+2) * sizeof(char **));
int j = 0;
for (int i = 0; i < envcount; i++){
if (strstr(envp[j], "DYLD_INSERT_LIBRARIES") != NULL){
continue;
}
newenvp[i] = envp[j];
j++;
}
char *envp_inject = malloc(strlen("DYLD_INSERT_LIBRARIES=") + strlen(inject_me) + 1);
envp_inject[0] = '\0';
strcat(envp_inject, "DYLD_INSERT_LIBRARIES=");
strcat(envp_inject, inject_me);
newenvp[j] = envp_inject;
newenvp[j+1] = NULL;
...
origret = old(pid, path, file_actions, newattrp, argv, newenvp);
}
这里我分析下几个关键的地方,第一是如果当前要创建进程为/usr/libexec/amfid
(也就是验证代码签名的进程),那么就注入AMFID_PAYLOAD_DYLIB
模块,这个模块主要就是去patch验证签名的地方,这样才能执行任意代码;如果是其他进程,那么就注入SBINJECT_PAYLOAD_DYLIB
模块(也就是tweak loader)。第二个地方是介绍了注入的实现方式,原理就是设置当前的环境变量,在调用原函数前,将当前DYLD_INSERT_LIBRARIES
这个环境变量里面增加一个dylib路径,这样在进程穿件后就会去加载tweak loader。到这里,我们明白了tweak loader原来就是在这里被注入进去的。但同时又引出了一个问题,pspawn_payload
模块本身又是谁加载的呢?这不又回到了之前的问题,接着分析。
the fun part
这里的名字并不是我随便起的,因为在Electra里面就是就是。相关的代码路径在
https://github.com/coolstar/electra/blob/02858b14dac30c9ba868bd3024529e9ae6592e67/electra/the%20fun%20part/fun.c
这个文件里面的代码会做Post exploit patching所有事情,即在漏洞完成利用以后的所有事,这里当然就是指越狱环境。和其他越狱一样,主要就下面几个工作
-
初始化jailbreakd
-
setuid(0):将当前进程设为root进程
-
Remap tfp0
-
Remount / as rw :重新挂起根目录,实现任意文件读写
-
Prepare bootstrap binary:准备预装的一些基本的二进制文件,包括sshd,gnu命令等
-
setup hook enviroment : 支持hook环境
-
launch some daemon:加载一些守护进程
-
respring :重启Springboard
这里的最后一步如果完成,也就是平时看到的那样,代表越狱成功了。
回我之前的问题,这些步骤里面我们关心的地方就是hook环境的支持,直接相关代码在
if (enable_tweaks){
const char* args_launchd[] = {BinaryLocation, itoa(1), "/bootstrap/pspawn_payload.dylib", NULL};
rv = posix_spawn(&pd, BinaryLocation, NULL, NULL, (char **)&args_launchd, NULL);
waitpid(pd, NULL, 0);
const char* args_recache[] = {"/bootstrap/usr/bin/recache", "--no-respring", NULL};
rv = posix_spawn(&pd, "/bootstrap/usr/bin/recache", NULL, NULL, (char **)&args_recache, NULL);
waitpid(pd, NULL, 0);
}
也就是说如果设置了支持tweak的话,就会将pspawn_payload
模块注入到1号进程,而1号进程就是launchd进程。另外你可能想问,那这里的注入又是怎么实现的呢?不可能又是hook posix_spawn
注入环境变量吧,这不就产生鸡生蛋和蛋生鸡的问题了吗?事实上这里的注入并不是通过hook posix_spawn
,注入的实现在
https://github.com/coolstar/electra/tree/02858b14dac30c9ba868bd3024529e9ae6592e67/basebinaries/inject_criticald
注入的原理就是通过task_for_pid
来实现的,如果你的设备是用的coolstar系越狱(Electra或者Chimera)的话,你会在越狱目录下存在这个可执行文件,下面是Chimera越狱的信息
xia0:/chimera root# ls -la
total 1100
drwxr-xr-x 8 root wheel 256 Feb 26 13:19 ./
drwxr-xr-x 28 root wheel 896 Sep 16 17:44 ../
-rwxr-xr-x 1 root wheel 168736 Sep 17 10:21 inject_criticald*
-rwxr-xr-x 1 root wheel 207920 Sep 17 10:21 jailbreakd*
-rwxr-xr-x 1 root wheel 133840 Sep 17 10:21 jailbreakd_client*
-rwxr-xr-x 1 root wheel 167296 Sep 17 10:21 libjailbreak.dylib*
-rwxr-xr-x 1 root wheel 236896 Sep 17 10:21 pspawn_payload-stg2.dylib*
-rwxr-xr-x 1 root wheel 202640 Sep 17 10:21 pspawn_payload.dylib*
xia0:/chimera root# ./inject_criticald
Usage: inject_criticald <pid> <dylib>
inject_criticald
这个命令可以直接对进程进行注入dylib。到这里,关于tweak loader的加载问题已经得到了解决,整个加载的过程可以说就是越狱的整个过程。现在tweak loader由谁加载的问题解决了,但是何时被加载和初始化的问题还没解决?接下来就和越狱本身不相关了,要从DYLD_INSERT_LIBRARIES
说起
DYLD_INSERT_LIBRARIES && dyld
下面就抛开越狱,进入DYLD_INSERT_LIBRARIES和dyld的实现细节里面,由于dyld本事是开源的,所以从源码开始分析。直接搜索DYLD_INSERT_LIBRARIES
最可疑的地方就在这里
// In order for register_func_for_add_image() callbacks to to be called bottom up,
// we need to maintain a list of root images. The main executable is usally the
// first root. Any images dynamically added are also roots (unless already loaded).
// If DYLD_INSERT_LIBRARIES is used, those libraries are first.
static void addRootImage(ImageLoader* image)
{
//dyld::log("addRootImage(%p, %s)\n", image, image->getPath());
// add to list of roots
sImageRoots.push_back(image);
}
注释里面说到了一句,If DYLD_INSERT_LIBRARIES is used, those libraries are first.
也就是说,如果DYLD_INSERT_LIBRARIES
环境变量注入的模块会被优先处理。本身这个函数的话是在下面函数中调用
void link(ImageLoader* image, bool forceLazysBound, bool neverUnload, const ImageLoader::RPathChain& loaderRPaths)
{
// add to list of known images. This did not happen at creation time for bundles
if ( image->isBundle() && !image->isLinked() )
addImage(image);
// we detect root images as those not linked in yet
if ( !image->isLinked() )
addRootImage(image);
// process images
try {
image->link(gLinkContext, forceLazysBound, false, neverUnload, loaderRPaths);
}
catch (const char* msg) {
garbageCollectImages();
throw;
}
}
接下来一个重要的函数就是initializeMainExecutable()
void initializeMainExecutable()
{
// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;
// run initialzers for any inserted dylibs
ImageLoader::InitializerTimingList initializerTimes[sAllImages.size()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}
// run initializers for main executable and everything it brings up
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
// dump info if requested
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoaderMachO::printStatistics((unsigned int)sAllImages.size(), initializerTimes[0]);
}
这里可以看到会去先初始化sImageRoots
,然后才初始化sMainExecutable
。当然后面就是一个递归的初始化过程,即是说如果初始化的模块依赖其他模块,那么又先初始化依赖的模块。在递归初始化函数之中有个地方
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
...
// let objc know we are about to initialize this image
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_dependents_initialized, this);
// initialize this image
bool hasInitializers = this->doInitialization(context);
// let anyone know we finished initializing this image
fState = dyld_image_state_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_initialized, this);
...
}
从这里可以看出,+load
函数确实会比mod_init_func
优先执行。
最后总结一下,由于tweak loader是通过DYLD_INSERT_LIBRARIES
注入的,所以会优先初始化,只有这样才能实现加载tweak模块的功能。到这里tweak loader何时被初始化的疑问也得到了解决,后面会用实验去验证这个分析。
实验
前面分析了这么多,那实际情况到底是不是这样的呢。首先我们还是对CFBundleGetMainBundle
、App里面的+load
和mod_init_func
下断点,首先断下来的是:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
* frame #0: 0x00000001fd2d18ac CoreFoundation`CFBundleGetMainBundle
frame #1: 0x00000001fdbfeadc Foundation`+[NSBundle mainBundle] + 112
frame #2: 0x00000001024a2ee0 TweakInject.dylib`___lldb_unnamed_symbol1$$TweakInject.dylib + 96
frame #3: 0x000000010256f56c dyld`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 424
frame #4: 0x000000010256f7ac dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 40
frame #5: 0x0000000102569f64 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 512
frame #6: 0x0000000102568dd8 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 152
frame #7: 0x0000000102568e98 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 88
frame #8: 0x00000001025567d4 dyld`dyld::initializeMainExecutable() + 188
frame #9: 0x000000010255b88c dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 4708
frame #10: 0x0000000102555044 dyld`_dyld_start + 68
从调用链来看initializeMainExecutable
后就是先初始化TweakInject.dylib
,这时候app自身代码还没执行。接下来断下来是
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
* frame #0: 0x000000010244e7f0 TestAPP`+[OCClassDemo load](self=OCClassDemo, _cmd="load") at OCClassDemo.m:20:5
frame #1: 0x00000001fc476a24 libobjc.A.dylib`call_load_methods + 188
frame #2: 0x00000001fc477d94 libobjc.A.dylib`load_images + 148
frame #3: 0x00000001025564c4 dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 488
frame #4: 0x0000000102569f40 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 476
frame #5: 0x0000000102568dd8 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 152
frame #6: 0x0000000102568e98 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 88
frame #7: 0x00000001025567f8 dyld`dyld::initializeMainExecutable() + 224
frame #8: 0x000000010255b88c dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 4708
frame #9: 0x0000000102555044 dyld`_dyld_start + 68
从调用链看,这时候开始初始化主模块,而且正是+load
函数,所以+load
就是app最早执行代码的地方。而且是通知libobjc.A.dylib
去完成的。这里可以回到dyld的源码中:
static void notifySingle(dyld_image_states state, const ImageLoader* image)
{
//dyld::log("notifySingle(state=%d, image=%s)\n", state, image->getPath());
std::vector<dyld_image_state_change_handler>* handlers = stateToHandlers(state, sSingleHandlers);
if ( handlers != NULL ) {
dyld_image_info info;
info.imageLoadAddress = image->machHeader();
info.imageFilePath = image->getRealPath();
info.imageFileModDate = image->lastModified();
for (std::vector<dyld_image_state_change_handler>::iterator it = handlers->begin(); it != handlers->end(); ++it) {
const char* result = (*it)(state, 1, &info);
if ( (result != NULL) && (state == dyld_image_state_mapped) ) {
//fprintf(stderr, " image rejected by handler=%p\n", *it);
// make copy of thrown string so that later catch clauses can free it
const char* str = strdup(result);
throw str;
}
}
}
...
}
notifySingle
函数会循环调用所有注册通知的处理模块
// Callback that provides a bottom-up array of images
// For dyld_image_state_[dependents_]mapped state only, returning non-NULL will cause dyld to abort loading all those images
// and append the returned string to its load failure error message. dyld does not free the string, so
// it should be a literal string or a static buffer
//
typedef const char* (*dyld_image_state_change_handler)(enum dyld_image_states state, uint32_t infoCount, const struct dyld_image_info info[]);
这里就是由libobjc.A.dylib
去处理的+load
函数。最后断下来的就是:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x000000010242bd20 TestAPP`temp_init at temp.c:98:5
frame #1: 0x000000010256f56c dyld`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 424
frame #2: 0x000000010256f7ac dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 40
frame #3: 0x0000000102569f64 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 512
frame #4: 0x0000000102568dd8 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 152
frame #5: 0x0000000102568e98 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 88
frame #6: 0x00000001025567f8 dyld`dyld::initializeMainExecutable() + 224
frame #7: 0x000000010255b88c dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 4708
frame #8: 0x0000000102555044 dyld`_dyld_start + 68
这里看出就是mod_init_func
了,当然在dyld中调用这个函数的地方就是
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
其中doModInitFunctions
就会解析load command找到_DATA,_mod_init_func
列表进行初始化调用。
最后
对于coolstar系的越狱简单的分析就到这里,unc0ver越狱的话整体流程应该差不多,但在substrate
层应该实现差异挺大的,后面有时间的话再分析下相关实现了。