开始
之前写过一篇文章介绍了在LLDB中的脱壳工具,原理是将dumpdecrypted移植成了LLDB脚本。该脱壳的场景主要是针对一些无法正常启动的app进行脱壳,例如,检测到越狱直接闪退、不明crash等。因为现在的脱壳工具前提必须是app至少能正常启动。但是在LLDB中脱壳那篇文章关于脱壳流程其实很繁琐,所以这里将进行改进,用一种优雅的方式实现,本文默认讨论是针对启动就crash的场景。
问题?
由于在LLDB中脱壳的特殊性,LLDB以后台模式启动App后,能拿到最早的执行点,所以App代码还没执行,自然是不会crash的,但是这个时机是没有办法进行脱壳的,因为很多基础模块都没有加载到进程,而我写的脱壳代码是需要执行环境(直接进行内存dump的话是不需要)。所以才有了之前那篇文章设置断点的方式,主要就是为了能够在基础模块加载完后,app代码还未执行之前进行dump。也因为如此,写了一个能对mod_init_func
下断点的命令,但这种方式一直都不优雅,很麻烦,而且+[Class load]
的执行时机在mod_init_func
之前,如果把检测的代码放到那里,这时候程序以及退出了。
逆向分析
其实对于dump的时机问题,最开始就在思考怎么容易的去拿到一个点。但之前在dyld源码中查找过相关api,不过没有找打合适的点。最近分析一个app正是用的+[Class load]
方式进行的检测,导致脱壳费了一点精力,所以想完善一下。开始分析:
首先+[Class load]
的调用链如下
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
* frame #0: 0x0000000100fd67f0 TestAPP`+[OCClassDemo load](self=OCClassDemo, _cmd="load") at OCClassDemo.m:20:5
frame #1: 0x00000001b6c2cecc libobjc.A.dylib`load_images + 736
frame #2: 0x00000001011ea0d4 dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 448
frame #3: 0x00000001011f958c dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 524
frame #4: 0x00000001011f8308 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 184
frame #5: 0x00000001011f83d0 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 92
frame #6: 0x00000001011ea420 dyld`dyld::initializeMainExecutable() + 216
frame #7: 0x00000001011eedb4 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 4616
frame #8: 0x00000001011e9208 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 396
frame #9: 0x00000001011e9038 dyld`_dyld_start + 56
可以看到是dyld在进行递归初始化的时候发出,然后调用libobjc.A.dylib
的load_images
函数,最后才执行到App自身的+[OCClassDemo load]
代码。由于objc代码开源的,所以去看下load_images
这个函数
/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
* Calls ABI-agnostic code after taking ABI-specific locks.
*
* Locking: write-locks runtimeLock and loadMethodLock
**********************************************************************/
const char *
load_images(enum dyld_image_states state, uint32_t infoCount,
const struct dyld_image_info infoList[])
{
bool found;
// Return without taking locks if there are no +load methods here.
found = false;
for (uint32_t i = 0; i < infoCount; i++) {
if (hasLoadMethods((const headerType *)infoList[i].imageLoadAddress)) {
found = true;
break;
}
}
if (!found) return nil;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
rwlock_writer_t lock2(runtimeLock);
found = load_images_nolock(state, infoCount, infoList);
}
// Call +load methods (without runtimeLock - re-entrant)
if (found) {
call_load_methods();
}
return nil;
}
注释上也写了,这个函数其实主要就是去处理+load
方法的,这样说来我们直接在这个方法下断点可以吗?事实上是可以的,但是进程会加载很多模块,在调试器中去需要频繁的进行check是否当前就是app的模块。那这个点其实也是不优雅的。
柳暗花明
在调试的时候由于一个偶然的bug,调试器断在了TweakInject.dylib
这里面的一个函数。而且此时app的+load
代码并没有执行,而且看名字应该是加载tweak的dylib用的,然后就去google 了一下这个,发现是有开源代码的
https://github.com/chr1s0x1/TweakInject
看了下源码发现确实就是用来加载/Library/MobileSubstrate/DynamicLibraries
下的dylib的,规则就是根据plist的字段。而且现在像libobjc.A.dylib
这样的基础模块以及初始化完成。所以是能够执行OC代码,那这个时机应该就是最佳的dump时机。所以我对CoreFoundation
里面的CFBundleGetMainBundle
函数下断点。堆栈如下
* frame #0: 0x00000001fd2d18ac CoreFoundation`CFBundleGetMainBundle
frame #1: 0x00000001fdbfeadc Foundation`+[NSBundle mainBundle] + 112
frame #2: 0x00000001012b6ee0 TweakInject.dylib`___lldb_unnamed_symbol1$$TweakInject.dylib + 96
frame #3: 0x000000010146f56c dyld`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 424
frame #4: 0x000000010146f7ac dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 40
frame #5: 0x0000000101469f64 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 512
这时候执行dumpdecrypted发现就没问题了,顺利脱壳。后面在封装成一个命令,这样只要调试器挂上以后,执行命令自动下断点,触发以后自动执行脱壳命令,这样就要优雅很多。
现在xia0LLDB中dumpdecrypted已经更新,在以后台模式启动app,lldb挂上去以后,直接执行dumpdecrypted -X
即可
更多思考
本文来说,这里应该就完了,已经找到了一个比较优雅的dump点。不过我却陷入了思考,这个TweakInject.dylib
凭啥就能优先初始化执行代码,能够保证在基础模块初始化之后,其他模块之前。正常来说肯定是做不到的,除非对dyld进行修改,使其优先初始化这个dylib。所以是怎么做到这得呢?搜索了一番没有发现是什么资料,随后我又对unc0ver进行了测试(因为我用的Chimera越狱),之前我就说过这两个越狱其实差异很大,导致我想看下unc0ver中这里是怎么处理的,同样对CFBundleGetMainBundle
下断点,堆栈如下
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001b6f4a510 CoreFoundation`CFBundleGetMainBundle
frame #1: 0x000000010d27bd14 SubstrateLoader.dylib`___lldb_unnamed_symbol1$$SubstrateLoader.dylib + 124
frame #2: 0x000000010d37e18c cy-c5bKjj.dylib`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 428
frame #3: 0x000000010d37e560 cy-c5bKjj.dylib`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 52
frame #4: 0x000000010d3795a4 cy-c5bKjj.dylib`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 548
frame #5: 0x000000010d378308 cy-c5bKjj.dylib`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 184
frame #6: 0x000000010d3783d0 cy-c5bKjj.dylib`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 92
frame #7: 0x000000010d36d894 cy-c5bKjj.dylib`dyld::runInitializers(ImageLoader*) + 88
frame #8: 0x000000010d3743b8 cy-c5bKjj.dylib`dlopen_internal + 1064
frame #9: 0x00000001b6d03560 libdyld.dylib`dlopen + 172
frame #10: 0x000000010d37e18c cy-c5bKjj.dylib`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 428
frame #11: 0x000000010d37e560 cy-c5bKjj.dylib`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 52
frame #12: 0x000000010d3795a4 cy-c5bKjj.dylib`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 548
frame #13: 0x000000010d378308 cy-c5bKjj.dylib`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 184
frame #14: 0x000000010d3783d0 cy-c5bKjj.dylib`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 92
frame #15: 0x000000010d36a3d0 cy-c5bKjj.dylib`dyld::initializeMainExecutable() + 136
frame #16: 0x000000010d36edb4 cy-c5bKjj.dylib`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 4616
frame #17: 0x000000010d369208 cy-c5bKjj.dylib`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 396
frame #18: 0x000000010d369038 cy-c5bKjj.dylib`_dyld_start + 56
这里可以发现并不是TweakInject.dylib
,而是SubstrateLoader.dylib
从名字上来看应该也是用作加载tweak的dylib的。另外有个注意的地方在于,调用者并不是dyld,而是cy-c5bKjj.dylib
完整的路径是/Library/Caches/cy-c5bKjj.dylib
,然而去文件中发现并不存在。
更新(2020-02-28)
找到原因了,本来写了一段,不过又删除了。因为后面涉及到的知识点太多了,如果只是用这一个节去简单说下原理,那无疑会遗漏很多细节,于是我准备再写一篇,详细介绍这背后的所有知识点。主要涉及到以下内容
- jailbreak post exploit patching
- tweak loader
- dyld how to load and init image
- DYLD_INSERT_LIBRARIES details
+[Class load]
and mod_init_func- …
分析在这里
最后
这里目前还有几个疑问没解决,后面找到了答案再来更新。