1、基本原理
从AppStore中下载的包,是被苹果使用FairPlay技术加密的,没办法直接重签名给其他人用,也不能做IDA分析之类的逆向操作。所以为了得到没有加密的包,就需要进行砸壳操作。
看完Mach-O文件跟dyld加载介绍后,使劲拍拍脑袋能想到两种砸壳思路:
- 1、直接破解FairPlay加密技术:很可惜暂时做不到,还没有公开的破解方案出来
- 2、从APP的加载运行入手,越过解密阶段,直接从内存中读取被dyld解密后的内容
所以目前常见的砸壳工作,都是使用第二种思路来做的,这里我们以dumpdecrypted为例进行分析。
2、dumpdecrypted
dumpdecrypted分为初代stefanesser做的版本,和后面conradev改进的版本。但其原理基本不变,改进版主要是为了能够dump出动态加载库。咱们这次的分析以改进版的为例,先看使用步骤:
- 1、make生成.dylib文件
- 2、使用ssh把dylib文件放入目标app的沙盒路径下
- 3、执行砸壳并导出砸壳后的ipa文件
除了使用步骤以外,dumpdecrypted还要求手机中已经安装了cycript。所以我们不难看出,dumpdecrypted是通过dylid的方式,介入到dyld的加载过程中的。
然后再看源码,主要定义了三个函数:dumptofile
、image_added
、dumpexecutable
,没有main函数。但可以看出这三个函数大致的调用关系,是dumpexecutable
-> image_added
-> dumptofile
。继续分析,dumpexecutable
是如何被调用的呢。答案在于其方法定义上:
1 | __attribute__((constructor)) |
把换行去掉,就成了
1 | __attribute__((constructor)) static void dumpexecutable() { |
其中比较特殊的是__attribute__((constructor))
这一段,可以大致理解成使用这段话,让我们新定义的dumpexecutable
方法,在main函数之前被执行。而其得以做到的原因,就在于首先constructor
函数,是在main函数之前执行的,而__attribute__
修饰,又使得我们新定义的方法,可以和constructor
一道,在main函数之前执行。这部分更详细的解释可以参照GUNC的文档
所以重新梳理一下大致的调用关系:constructor
-> dumpexecutable
-> image_added
-> dumptofile
,需要注意这里说的调用关系仅仅是大致的。
弄清了整个的启动时机后,再看dumpexecutable
函数内部,实际的操作只做了一件事:_dyld_register_func_for_add_image(&image_added);
。因为咱们已经意识到整个砸壳过程是跟dyld有关的,所以可以从dyld源码中找到对_dyld_register_func_for_add_image
的解释
1 | /* |
大致的意思,就是说注册了一个监听方法,用于监听bundle或者动态加载库的加载事件,而且加载事件,对于每个bundle或者动态库仅会触发一次。到这里,我们就可以梳理出整个砸壳工具的逻辑顺序了:
- 1、伴随
constructor
函数的调用,在main函数执行前,调用dumpexecutable
函数 - 2、在
dumpexecutable
函数中,注册bundle或动态库的加载事件监听,而且其响应方法是image_added
函数 - 3、当bundle或者动态库被加载时,触发事件监听,调用
image_added
方法,并向其传递header结构体指针,和另外一个不知道有什么用的参数slide - 4、然后在
image_added
方法内,调用dumptofile
函数,进行真正的文件导出工作
所以到这里,我们转向dumptofile
函数,看一下其内部实现。越过前面的变量定义,首先看这一段
1 | /* extract basename */ |
这块的作用,是把路径进行裁剪,得到目标APP的文件名。然后下面这一段:
1 | /* detect if this is a arm64 binary */ |
大致的作用,是通过对Mach-O文件中,header的magic字段进行判断,判断当前可执行文件是32还是64位的。然后,因为Mach-O文件的内容是连续的,所以可以通过header指针加上header区间大小的方式,拿到load commands的指针。
拿到了指针之后,再下一段代码
1 | for (i=0; i<mh->ncmds; i++) { |
通过mh->ncmds
得到load commands数量,对所有load command进行遍历。而在for循环内部,则对load command进行类型判断,只有当是LC_ENCRYPTION_INFO
或者LC_ENCRYPTION_INFO_64
类型时,才进入下一步。在Mach-O文档里没有找到LC_ENCRYPTION_INFO
,但是可以google一下,大致的意思是加密相关的。所以我们可以把这个if语句,简单的理解成过滤加密相关的load command。然后继续深入if语句内部,首先是这么一段:
1 | eic = (struct encryption_info_command *)lc; |
为了理解这段,得先看encryption_info_command
的定义:
1 | /* |
然后结合这段代码的注释理解下,大致意思可能是说从LC_ENCRYPTION_INFO
类型的load command,取出数据的加密状态,如果不是被加密的状态就中断执行。继续往下看这一段:
1 | off_cryptid=(off_t)((void*)&eic->cryptid - (void*)mh); |
第一句的意思,是通过(void*)eic->cryptid
减去(void*)mh
的方式,得到cryptid偏移量。然后最后一句,输出的日志里表示从eic->cryptoff
开始,eic->cryptsize
大小的区域,都是已经被dyld解密并加载后的内容。
继续看源码,下面跟着的一大段,主要用处是读取Mach-O文件的header内容,所以就不贴源码了。但其中比较特殊的一段if语句:
1 | /* Is this a FAT file - we assume the right endianess */ |
这段的作用,是区分FAT类型Mach-O文件,然后重定位到真正的header地址。再往下:
1 | strlcpy(npath, tmp+1, sizeof(npath)); |
这块是创建一个文件路径并open得到句柄,里面一大堆复杂的if判断,主要是用来处理文件操作失败的情况。而这个新创建的文件,就是以后我们会得到的砸壳输出结果文件。继续往下:
1 | /* calculate address of beginning of crypted data */ |
这块就是向输出文件写入数据了,主要分成三步:
- 第一步:计算并定位到解密数据的起始位置
- 第二步:把解密数据位置前的数据,写入到输出文件内,这部分的数据估计是包含了header的标识位信息
- 第三步:把解密数据写入到文件内。
然后再往下,直到整个dumptofile
函数结束,剩下的代码段的作用,在于把剩下的数据一并写入输出文件里,并关闭文件句柄。
至此整个dumpdecrypted砸壳工具的代码已经分析完了,但是也留下了这几个问题:
- 在判断加密状态那一段,当数据处于非加密的时候,为什么是break,而不是continue
- 在数据读取及写入阶段,加密数据段之前、之后的内容,具体是哪些数据
这些问题估计可以从Mach-O文件格式里面找到答案,所以还得再回头研究Mach-O文件。