发新帖

iOS逆向--砸壳工具dumpdecrypted的演变

[复制链接]
24308 0

dumpdecrypted的发展历史:

在 iOS 平台上,从 App Store 下载的 App 会被 Apple 使用 FairPlay 技术加密,使得程序无法在其他未登录相同 AppleID 的设备上运行,起到 DRM 的作用。这样的文件同样也无法使用 IDA Pro 等工具进行分析。不管是出于安全研究还是再次分发的目的,都需要获取未加密的二进制文件,这一过程俗称砸壳。

最早的动态砸壳工具是stefanesser写的dumpdecrypted,通过手动注入然后启动应用程序,在内存进行dump解密后的内存实现砸壳。

但是这种砸壳只能砸APP可执行文件,对于动态库就无能为力了。为什么呢?这是个2014年就停止更新的项目,那时候iOS8系统刚出,也是苹果刚开始尝试在iOS系统中使用动态库,因此很少有人使用,iOS7之前又全是静态库,是直接编在APP的可执行文件中的,所以只要砸二进制主文件。

但是英雄总是出现在人民群众最需要他的时刻,就在大家一筹莫展之际,conradev出现了,他稍微改进了一下dumpdecrypted,使它具有了砸动态库的能力。

原理是通过_dyld_register_func_for_add_image注册回调对每个模块进行dump解密。

然而问题出现了,这种砸壳方式依然需要拷贝dumpdecrypted.dylib,然后找路径什么的,还是挺麻烦的,而且最重要的一点,自从iOS系统增加了widget和watchOS APP之后,这个版本的dumpdecrypted砸不了带有Plugins(也就是extension)的壳。

于是又一位英雄出现了,也就是iOSRE界大家熟知的庆总AloneMonkey(刘培庆),也是MokeyDev的作者。

他又对dumpdecrypted进行了修改,放到MonkeyDev模板变成一个tweak的形式dumpdecrypted,这样填写目标bundle id然后看日志把文件拷贝出来就可以了,当然,这里的dumpdecrypted已经被现在的frida-ios-dump取代了。

分析一下最终的dumpdecrypted

我们可以分析一下他的代码,看一下两个问题:

究竟做了什么操作,为什么可以砸Frameworks和extension的壳了?

原版的 dumptofile 的函数参数是怎么来的?

首先介绍一下attribute修饰词

GNU C 的一大特色就是__attribute__ 机制。

__attribute__ 可以设置函数属性(Function Attribute )、变量属性(Variable Attribute )和类型属性(Type Attribute )。

__attribute__ 书写特征是:__attribute__ 前后都有两个下划线,并切后面会紧跟一对原括弧,括弧里面是相应的__attribute__ 参数。

__attribute__ 语法格式为:__attribute__ ((attribute-list))

constructor参数的作用

constructor参数让系统执行main()函数之前调用函数(被attribute((constructor))修饰的函数).同理,destructor让系统在main()函数退出或者调用了exit()之后,调用我们的函数.带有这些修饰属性的函数,对于我们初始化一些在程序中使用的数据非常有用.

为什么可以砸Framework和extension的壳

在这个dumpdecrypted(建议下载源码看一下,再看下面的解释)的Frameworks 分支版本中用到了上面介绍的参数:

__attribute__((constructor))
static void dumpexecutable() {
        ...
        _dyld_register_func_for_add_image(&image_added);
}

所以这里 dumpexecutable() 方法在 main 函数之前执行_dyld_register_func_for_add_image方法,这里用到了dyld里的函数,我们来看一下这个函数的实现(开源大法好opensource-apple):

/*
 * _dyld_register_func_for_add_image registers the specified function to be
 * called when a new image is added (a bundle or a dynamic shared library) to
 * the program.  When this function is first registered it is called for once
 * for each image that is currently part of the program.
 */
void
_dyld_register_func_for_add_image(
void (*func)(const struct mach_header *mh, intptr_t vmaddr_slide))
{
    DYLD_LOCK_THIS_BLOCK;
    typedef void (*callback_t)(const struct mach_header *mh, intptr_t vmaddr_slide);
    static void (*p)(callback_t func) = NULL;

    if(p == NULL)
        _dyld_func_lookup("__dyld_register_func_for_add_image", (void**)&p);
    p(func);
}

dyld 会负责传递 mh 和 vmaddr_slide 参数

我们看一下这里intptr_t到底是个什么类型,继续追踪:

// usr/include/sys/_types/_intptr_t.h
typedef __darwin_intptr_t   intptr_t;
// usr/include/arm/_types.h
typedef long            __darwin_intptr_t;

可以清楚的看到,intptr_t就是__darwin_intptr_t,也就是个long类型(不懂为啥这么麻烦,还typedef两次)

继续看代码,这里写了一个image__added方法,该方法调用了 dumptofile 函数:

static void image_added(const struct mach_header *mh, intptr_t slide) {
    Dl_info image_info;
    int result = dladdr(mh, &image_info);
    dumptofile(image_info.dli_fname, mh);
}

_dyld_register_func_for_add_image从注释中可以知道,通过_dyld_register_func_for_add_image 注册的回调函数会在每次 dyld 加载镜像之后被调用。传递给回调函数的参数有两个:载入镜像的文件头:mach_header 和内存数量:vmaddr_slide。在本例中,dumpexecutable 函数中通过 _dyld_register_func_for_add_image 函数向 dyld 注册一个回调函数 image_added,每当 dyld 载入一个镜像(可以是可执行程序、动态库、Plugin等),dyld 会调用 image_added 函数,并将相应的 Mach-O header 和 vmaddr_slide 传递给 image_added,这也就是为什么可以砸Framework和extension的原因。

关于原版dumpdecrypted的dumptofile函数实现

查找 dyld 后发现在 ImageLoader.h 头文件中,有一个很长的函数:

typedef void (*Initializer)(int argc, const char* argv[], const char* envp[], const char* apple[], const ProgramVars* vars);

然后再ImageLoaderMachO.cpp中找到了调用:

void ImageLoaderMachO::doImageInit(const LinkContext& context)
{
    if ( fHasDashInit ) {
        const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
        const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
        const struct load_command* cmd = cmds;
        for (uint32_t i = 0; i < cmd_count; ++i) {
            switch (cmd->cmd) {
                case LC_ROUTINES_COMMAND:
                    Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide);
                    // <rdar://problem/8543820&9228031> verify initializers are in image
                    if ( ! this->containsAddress((void*)func) ) {
                        dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
                    }
                    if ( context.verboseInit )
                        dyld::log("dyld: calling -init function %p in %s\n", func, this->getPath());
                    //就是这句话了
                    func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
                    break;
            }
            cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
        }
    }
}

根据函数命名知道这应该是给镜像做初始化的,里面 func 函数是 Initializer 类型的,通过 context 参数获取上下文信息,原版的 dumptofile 函数的参数列表为什么会是 (int argc, const char argv, const char envp, const char *apple, struct ProgramVars pvars) 到这里就很清楚了。

举报 使用道具

回复
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表