在 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取代了。
我们可以分析一下他的代码,看一下两个问题:
究竟做了什么操作,为什么可以砸Frameworks和extension的壳了?
原版的 dumptofile 的函数参数是怎么来的?
GNU C 的一大特色就是__attribute__ 机制。
__attribute__ 可以设置函数属性(Function Attribute )、变量属性(Variable Attribute )和类型属性(Type Attribute )。
__attribute__ 书写特征是:__attribute__ 前后都有两个下划线,并切后面会紧跟一对原括弧,括弧里面是相应的__attribute__ 参数。
__attribute__ 语法格式为:__attribute__ ((attribute-list))
constructor参数让系统执行main()函数之前调用函数(被__attribute__((constructor))修饰的函数).同理, destructor让系统在main()函数退出或者调用了exit()之后,调用我们的函数.带有这些修饰属性的函数,对于我们初始化一些在程序中使用的数据非常有用.
在这个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的原因。
查找 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);
// 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) 到这里就很清楚了。