初代砸壳工具

先从初代砸壳工具 stefanesser - dumpdecrypted 的源码探究一下砸壳如何实现的。

源码分析

从运行命令 $ DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /var/mobile/Applications/xxx-xxxx-xxxx/Scan.app/Scan 看出这是利用注入动态库的方式,__attribute__((constructor)) 是GCC 语法,当与函数一起使用时,会在程序启动时在**main()**函数之前执行该函数:

__attribute__((constructor))
void dumptofile(int argc, const char **argv, const char **envp, const char **apple, struct ProgramVars *pvars)

这段代码不解的是:如何给 ProgramVars 结构体赋值的?(🤔️没找到什么资料,先跳过

在 dumpdecrypted - conradev的fork版本 的源码中找到了答案:

__attribute__((constructor)) static void dumpexecutable() 中给_dyld_register_func_for_add_image 注册回调函数 image_added :

void dumptofile(const char *path, const struct mach_header *mh) {
    ...
}

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);
}

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

_dyld_register_func_for_add_image 的定义如下,简单来说当 dyld 加载或卸载程序映像(image)都会调用这个函数:

/*
* The following functions allow you to install callbacks which will be called   
* by dyld whenever an image is loaded or unloaded.  During a call to _dyld_register_func_for_add_image()
* the callback func is called for every existing image.  Later, it is called as each new image
* is loaded and bound (but initializers not yet run).  The callback registered with
* _dyld_register_func_for_remove_image() is called after any terminators in an image are run
* and before the image is un-memory-mapped.
*/
extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide))    __OSX_AVAILABLE_STARTING(__MAC_10_1, __IPHONE_2_0);
extern void _dyld_register_func_for_remove_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)) __OSX_AVAILABLE_STARTING(__MAC_10_1, __IPHONE_2_0);

事已至此就来简单梳理内核加载加壳后的 MachO,大致的流程图如下:

Untitled

<aside> 🔃 Load->>>>

  1. 内核空间

    内核空间的主要任务是创建新 task 并初始化内存页和对应的权限:

    1. 分配虚拟内存空间。
    2. fork 进程。
    3. 加载 MachO 到进程空间。
    4. 加载动态链接器 dyld 并将控制权交给 dyld 处理。
  2. 用户空间

    从内核回到用户空间,便跳转到目标的入口地址开始执行动态链接阶段,进入 dyld 动态链接器:

    1. 配置环境变量
    2. 加载共享缓存库
    3. 实例化主程序
    4. 加载动态链接库
    5. 链接主程序
    6. 加载Load和特定的C++的构造函数方法
    7. 寻找APP的main函数并调用 </aside>

所以当有image被加载时都会调用_dyld_register_func_for_add_image 传入的回调函数。dumptofile 将MachO文件转储,把其函数内容步骤简化一下:

1、提取App文件名

/* extract basename */
tmp = strrchr(rpath, '/');
printf("\\n\\n");
if (tmp == NULL)
{
    printf("[-] Unexpected error with filename.\\n");
    _exit(1);
}
else
{
    printf("[+] Dumping %s\\n", tmp + 1);
}

2、判断 mach_header 是64位还是32位,计算 load commands 指针

/* detect if this is a arm64 binary */
if (mh->magic == MH_MAGIC_64)
{
    lc = (struct load_command *)((unsigned char *)mh + sizeof(struct mach_header_64));
    printf("[+] detected 64bit ARM binary in memory.\\n");
}
else
{ /* we might want to check for other errors here, too */
    lc = (struct load_command *)((unsigned char *)mh + sizeof(struct mach_header));
    printf("[+] detected 32bit ARM binary in memory.\\n");
}

3、判断 LC_ENCRYPTION_INFO 未加密就中断执行

/* searching all load commands for an LC_ENCRYPTION_INFO load command */
for (i = 0; i < mh->ncmds; i++)
{
    /*printf("Load Command (%d): %08x\\n", i, lc->cmd);*/
    if (lc->cmd == LC_ENCRYPTION_INFO || lc->cmd == LC_ENCRYPTION_INFO_64)
    {
        eic = (struct encryption_info_command *)lc;
        /* If this load command is present, but data is not crypted then exit */
        if (eic->cryptid == 0)
        {
            break;
        }
...

4、得到 cryptid 偏移量

off_cryptid = (off_t)((void *)&eic->cryptid - (void *)mh);
printf("[+] offset to cryptid found: @%p(from %p) = %x\\n", &eic->cryptid, mh, off_cryptid);

5、读取原MachO header,判断 FAT_CIGAM 啥的重定位到真正的header地址:

printf("[+] Reading header\\n");
n = read(fd, (void *)buffer, sizeof(buffer));
if (n != sizeof(buffer))
{
    printf("[W] Warning read only %d bytes\\n", n);
}

printf("[+] Detecting header type\\n");
fh = (struct fat_header *)buffer;

/* Is this a FAT file - we assume the right endianess */
if (fh->magic == FAT_CIGAM)
{
    printf("[+] Executable is a FAT image - searching for right architecture\\n");
    ...
}
else if (fh->magic == MH_MAGIC || fh->magic == MH_MAGIC_64)
{
    printf("[+] Executable is a plain MACH-O image\\n");
}
else
{
    printf("[-] Executable is of unknown type\\n");
    _exit(1);
}

6、得到砸壳内容输出到文件

...
/* calculate address of beginning of crypted data */
n = fileoffs + eic->cryptoff;

restsize = lseek(fd, 0, SEEK_END) - n - eic->cryptsize;
lseek(fd, 0, SEEK_SET);

printf("[+] Copying the not encrypted start of the file\\n");
/* first copy all the data before the encrypted data */
while (n > 0)
{
    toread = (n > sizeof(buffer)) ? sizeof(buffer) : n;
    r = read(fd, buffer, toread);
    if (r != toread)
    {
        printf("[-] Error reading file\\n");
        _exit(1);
    }
    n -= r;

    r = write(outfd, buffer, toread);
    if (r != toread)
    {
        printf("[-] Error writing file\\n");
        _exit(1);
    }
}

/* now write the previously encrypted data */
printf("[+] Dumping the decrypted data into the file\\n");
r = write(outfd, (unsigned char *)mh + eic->cryptoff, eic->cryptsize);
if (r != eic->cryptsize)
{
    printf("[-] Error writing file\\n");
    _exit(1);
}

/* and finish with the remainder of the file */
n = restsize;
lseek(fd, eic->cryptsize, SEEK_CUR);
printf("[+] Copying the not encrypted remainder of the file\\n");
while (n > 0)
{
    toread = (n > sizeof(buffer)) ? sizeof(buffer) : n;
    r = read(fd, buffer, toread);
    if (r != toread)
    {
        printf("[-] Error reading file\\n");
        _exit(1);
    }
    n -= r;

    r = write(outfd, buffer, toread);
    if (r != toread)
    {
        printf("[-] Error writing file\\n");
        _exit(1);
    }
}
...

到此砸壳过程就结束啦。

编译&测试

在项目根目录下执行$ make 生成dumpdecrypted.dylib

Untitled

拷贝到 iPhone 中,运行 DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib Foo.App/Foo 出现 missing LC_DYLD_INFO load command 的错误,要将xcode iPhone sdk的版本与越狱机器的版本保持一致,我的测试机为 iPhone 6 Plus iOS 12.5.5 要下个 iOS12.* 的sdk,辣鸡 xcode和sdk之间是绑定的,需要下载指定版本的xcode,再把sdk添加到当前xcode就行了(😢一边下载一边测试下其他工具吧。

Clutch

下载执行文件:https://github.com/KJCracks/Clutch/releases 拷贝到测试机中,运行 Clutch -b xxx.xxx.xxx 又报错啦:Error: Failed to dump with arch arm64 。好在翻到一条issue: KJCracks/Clutch/issues/233 给Clutch添加些权限就好了(在我的文章《macOS反反调试小记》 中也有记录操作步骤):

<key>platform-application</key>
<true/>
<key>get-task-allow</key>
<true/>
<key>run-unsigned-code</key>
<true/>
<key>com.apple.private.skip-library-validation</key>
<true/>
<key>com.apple.private.security.no-container</key>
<true/>

ok!使用命令:./Clutch -b [xxx.xxx.xxx Bundle ID] 开始砸壳吧!

frida-ios-dump

  1. 安装frida
  2. 克隆代码&安装依赖:
git clone <https://github.com/AloneMonkey/frida-ios-dump>
pip3 install -r frida-ios-dump/requirements.txt
  1. 安装usbmuxd

ok!使用命令:python3 dump.py [App Name] 开始砸壳吧!

CrackerXI+

添加软件源&安装CrackerXI+

总结

这几个工具用来下,推荐 CrackerXIfrida-ios-dump 砸壳效果都还不错,dumpdecrypted 要匹配iOS版本下载对应的SDK编译麻烦,Clutch 砸壳效果不咋地。

先水到这,还有 frida-ios-dump 和 clutch 源码没研究,原理估计都差不多,读取内核解密后的内容并计算其偏移位置将其从内存中拷贝出来。

参考链接