chaojiwantong 发表于 2019-10-28 13:23:00

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

## dumpdecrypted的发展历史:

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

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

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

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

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

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

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

他又对dumpdecrypted进行了修改,放到MonkeyDev模板变成一个tweak的形式(https://github.com/AloneMonkey/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的壳

在这个(https://github.com/AloneMonkey/dumpdecrypted/blob/master/dumpdecrypted/dumpdecrypted.m)(建议下载源码看一下,再看下面的解释)的Frameworks 分支版本中用到了上面介绍的参数:

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

```

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

```C
/*
* _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 函数:

```C
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;
                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) 到这里就很清楚了。



页: [1]
查看完整版本: iOS逆向--砸壳工具dumpdecrypted的演变