如何优雅的在LLDB里dumpdecrypted

By xia0

#如何优雅的在LLDB里dumpdecrypted

#开始

之前写过一篇文章介绍了在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.dylibload_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

#最后

这里目前还有几个疑问没解决,后面找到了答案再来更新。

#参考