学习iOS安全之砸壳原理小记

前言

接触 iOS 安全的同学们对越狱、砸壳、Cydia、Theos等名词应该都不陌生,用一句话总结网上文章对砸壳原理的解释:AppStore 发布的 App 都是加壳(加密)后的 ipa 文件,逆向App就得先砸壳,在iOS系统中,应用砸壳所使用的都是动态砸壳技术,即从运行在进程内存空间中的可执行程序映像(image)入手,将内存中的内容进行转储(dump)处理来实现脱壳处理。这种方法实现起来相对简单,且不必关心使用的是何种加密技术。

准备

  1. 越狱
    • https://checkra.in/
  2. Cydia工具安装
    • OpenSSH
    • Darwin cc tools
  3. 下载App
    • 在 AppStore 随便找个App

AppStore下载的App在/var/containers/Bundle/Application/$uuid 目录下,找到刚才下载的App,通过查看cryptid标志位来判断App加密状态。其中1代表加密,0代表已解密:

$ otool -lv /var/containers/Bundle/Application/xxxxx-xxxx-xxxx-xxxx-xxxxx/Foo.app/Foo | egrep 'LC_ENCRYPTION_INFO|cryptid'          cmd LC_ENCRYPTION_INFO_64      cryptid 1

如果App没有被加密,则直接交给dyld加载并且运行。如果App已经被加密,则需要内核对其进行解密,得到解密后的MachO文件,再将其交给dyld加载并运行。

再把加密 macho down 下来用 MachOView 打开看到Load Commands 中有LC_ENCRYPTION_INFOCryptID 为 1 和上面otool命令查看的内容一致:

学习iOS安全之砸壳原理小记

Hopper 打开找到入口,会提示这是个加密文件:

学习iOS安全之砸壳原理小记

接下来就是对 App 进行砸壳,目前常见的砸壳工具如下:

  • Clutch
  • dumpdecrypted
  • frida-ios-dump

初代砸壳工具

先从初代砸壳工具 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,大致的流程图如下:

学习iOS安全之砸壳原理小记

  1. 内核空间

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

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

2. 用户空间

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

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

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

1、提取App文件名

/* extract basename */tmp = strrchr(rpath, '/');printf("nn");if (tmp == NULL){	printf("[-] Unexpected error with filename.n");	_exit(1);}else{	printf("[+] Dumping %sn", 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): %08xn", 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) = %xn", &eic->cryptid, mh, off_cryptid);

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

printf("[+] Reading headern");n = read(fd, (void *)buffer, sizeof(buffer));if (n != sizeof(buffer)){	printf("[W] Warning read only %d bytesn", n);}
printf("[+] Detecting header typen");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 architecturen"); ...}else if (fh->magic == MH_MAGIC || fh->magic == MH_MAGIC_64){ printf("[+] Executable is a plain MACH-O imagen");}else{ printf("[-] Executable is of unknown typen"); _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 filen");/* 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 filen"); _exit(1); } n -= r;
r = write(outfd, buffer, toread); if (r != toread) { printf("[-] Error writing filen"); _exit(1); }}
/* now write the previously encrypted data */printf("[+] Dumping the decrypted data into the filen");r = write(outfd, (unsigned char *)mh + eic->cryptoff, eic->cryptsize);if (r != eic->cryptsize){ printf("[-] Error writing filen"); _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 filen");while (n > 0){ toread = (n > sizeof(buffer)) ? sizeof(buffer) : n; r = read(fd, buffer, toread); if (r != toread) { printf("[-] Error reading filen"); _exit(1); } n -= r;
r = write(outfd, buffer, toread); if (r != toread) { printf("[-] Error writing filen"); _exit(1); }}...

主要分成三步:

  • 复制文件头数据(header开始到eic->cryptoff
  • 将解密数据写入文件(mh+eic->cryptoff 开始大小为 eic->cryptsize 的内核解密好的数据)
  • 将文件的其余部分也写入文件

7、修改加密标识位

if (off_cryptid){	uint32_t zero = 0;	off_cryptid += fileoffs;	printf("[+] Setting the LC_ENCRYPTION_INFO->cryptid to 0 at offset %xn", off_cryptid);	if (lseek(outfd, off_cryptid, SEEK_SET) != off_cryptid || write(outfd, &zero, 4) != 4)	{		printf("[-] Error writing cryptid valuen");	}}

到此砸壳过程就结束啦。

编译&测试

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

学习iOS安全之砸壳原理小记

拷贝到 iPhone 中,运行 DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib Foo.App/Foo 出现 missing LC_DYLD_INFO load command 的错误,要将iOSsdk的版本与越狱机器的版本保持一致,的测试机为 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: 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/>

最后

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

原博客地址 https://blog.macoder.tech/article/dump-encrypted-ipa

参考链接

  • checkra.in
  • iOS-Pentest-Tools
  • Understanding the iOS File System
  • 一条命令完成砸壳
  • dumpdecrypted
  • attribute((constructor)) and attribute((destructor)) syntaxes in C
  • 深入浅出MachO
  • 砸壳原理之dumpdecrypted
  • apple-oss-distributions/dyld
  • iOS 应用的启动过程
  • iOS逆向(5)-不知MachO怎敢说自己懂DYLD

原文始发于微信公众号(百灵鸟安全团队):学习iOS安全之砸壳原理小记

版权声明:admin 发表于 2023年1月1日 下午2:15。
转载请注明:学习iOS安全之砸壳原理小记 | CTF导航

相关文章

暂无评论

暂无评论...