iOS逆向(11)-砸壳原理剖析,主动加载所有framework

7,543 阅读13分钟

在平时的日常工作过程中,时常会有道友私聊我,问我能不能帮他对xxAPP砸个壳这样的场景。所以如何简单快速的对APP砸壳自然尤为重要,本文将会讲述如何实现一键砸壳,做到无痛无瘙痒。
当然按照本系列的惯例,同样会讲述砸壳原理,砸壳遇到的问题如何解决等等。

先看最终效果:

优酷砸壳gif.gif

在阅读这篇文章之前,笔者默认读者已经了解如何进入远程连接手机,如果实在不了解请阅读iOS逆向(10)-越狱!越狱原理!远程连接登录手机

在了解如何砸壳之前,需要知道到底什么是壳,砸壳的原理又是什么。

开始之前同样的放出福利:
主动加载默认不启动的framework,解决砸壳失败的痛点:DecryptApp

一、什么是壳

绝大多数APP开发者都认为iOS系统很安全,都未对APP进行加固,所以通俗意义的加壳就是在上传App的时候,由Apple对App的加密。

1、验证何时加壳

(1) Build后

首先利用xcode新建一个默认工程EncryptApp,不用写任何代码,或者随意写点东西。
Build这个工程,成功之后进入工程路径,查看此时是否加壳。

app目录.png

app目录2.png

显示上图目录中EncryptApp的包内容

app目录3.png

利用终端查看MachO文件EncryptApp是否已经被加壳

otool -l EncryptApp | grep crypt

未加密1.png

可以看到其中的cryptid字段为0,即代表该文件未被加密。

(2) 运行到手机中

先查找EncryptApp在手机中的位置。
话不多说,先利用SSH,将电脑连接上iPhone设备(也能用)。
先确保EncryptApp在设备上已经正常运行,在利用ps命令查找当前进程。

ps -ax | grep EncryptApp  

查看路径.png

从上图就可以查看到当前APP在设备上的物理路径。之后进入该路径,将其导出到电脑桌面(推荐使用iFunBox,如果使用iFunBox查看不了设备的根目录,则代表设备权限不够,需要在Cydia中下载对应版本的AFC2插件)。
或者直接使用命令拷贝到桌面

scp -r -P 12345 root@localhost:/var/containers/Bundle/Application/AD8F7993-260F-465D-B207-900F67195658/Youkui4Phone.app ~/Desktop/

同样利用otool命令查看其是否被加密

未加密2.png

同样cryptid字段为0,即代表该文件未被加密。

(3) 本地打包后

常规操作,利用证书将工程打包成ipa包,并且导出到桌面目录。
同样利用otool命令查看其是否被加密

未加密3.png

同样cryptid字段为0,即代表该文件未被加密。
这个地方有两个cryptid字段代表这个MachO是个多架构可执行文件,分别是arm7,arm64,而他们都未被加壳。

(4) AppStore下载的包

从AppStore下载一个崭新的优酷,运行后,同样利用ps查看其目录地址,再利用otool查看是否加密。

目录.png

同样将其导出到桌面,查看其是否加密

已加密

此时cryptid字段为1,即代表该文件被加密方案为1的方案加密了。

从以上过程即可得出结论,App加壳是在咱们的ipa上传到苹果后台之后被加密的。

二、怎么砸壳

要真正了解什么是砸壳,先要知道系统在启动一个app的时候做了些什么事情。

1、内核加载MachO过程

在我们点击App的从之前,APP是以MachO和各种资源的形式躺在磁盘了,在点击icon之后,内核开始读取MachO,先查看该MachO是否被加密。
如果没有被加密,则直接交给dyld加载并且运行。
如果已经被加密,则需要内核对其进行解密,得到解密后的MachO文件,再将其交给dyld加载并运行。

dyld 的加载过程可以查看文章iOS逆向(5)-不知MachO怎敢说自己懂DYLD

从这步可以看出,一个被加密的MachO是不可能全完被加密的,否则内核无法知道该MachO是否被加密,是是什么类型的文件,是什么架构的文件等等信息(具体有哪些信息可以通过查看MachO的Header段得知,具体方法同样可以点击iOS逆向(5)-不知MachO怎敢说自己懂DYLD查看)。

我们在使用otool -l EncryptApp | grep crypt命令时得出的记过出了字段cryptid,还有cryptoffcryptsize字段,这两个字段就代表着该MachO从位置16384(10进制)开始后的103153664(10进制)字节都被加密了。

内核加载加壳后的MachO大致的流程图如下:

内核加载过程

2、砸壳过程

(1) 两种砸壳原理

砸壳就就相当于一个解密过程。我们人(下文姑且成为小明同学)相当于内核和内存,而未加壳的MachO相当于明文,加壳后的MachO相当于密文。
例如:小明同学在看到aGVsbG8=的时候并不知道其代表了什么含义,但他知道这是用base64加密而来的,所以只需要通过base64解密就可以知道这其实是hello的意思。

由于每次启动App的时候都是需要对App进行解密的,所以如果利用非对称对App进行加密/解密效率是非常低的,这会严重影响用户体验,所以App的加密肯定是用的对称加密的方式来加密的。

所以就能知道砸壳(解密)原理是有两种的。
静态砸壳:破解解密整个过程,还原所有代码。
动态砸壳:由于内核运行的是内存中已经被解密的MachO,所以只要将该内存中的MachO直接dump到磁盘即可,就相当于小明同学在通过解密算法解密后看到的明文后,我们直接叫小明同学告诉我们明文是什么一样。

静态解密的方式成本极大,需要逆向整个内核,而Apple对内核加固的非常严,并且就算那到了整个加密过程,只要Apple对加密方式稍作修改,则有需要从头再来,所以这种方式不可取。
而各大厂商的内存格式都是统一的,所以从内存读取数据的方式也是统一的,所以动态砸壳的方式是目前比较理想的方式。

(2) 手动动态砸壳

启动手机内部的LLDB 服务

上文提到了动态砸壳的原理是从内存dump出对应的二进制,那么我们日常开发的时候是用什么调试内存中的App呢?
答案自然是我们熟知的LLDB

我们的Mac是自带LLDB的,比如:

使用终端,敲入命令LLDB(笔者lldb有python2的脚本,但系统已经默认是python3,所有有相关报错)

终端LLDB

或者在使用xcode的时候,挂起App,App即进入LLDB调试状态。

挂起App

这也证明在我们的手机里面是是有LLDB的对应服务的。

在购买一台新的手机的时候,手机上本来是没有这个服务的,当我的连上xcode,第一次联调App的时候,xcode将手机系统对应版本的服务Copy到手机中,而这个服务的名称就叫做debugserver

在Mac电脑中,iOS 10.3版本的debugserver的位置位于:

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/10.3
../DeveloperDiskImage/usr/bin/debugserver

debugserver1

debugserver2

而在手机上,debugserver的位置位于:

usr/bin

debugserver3

我们可以利用md5验证手机内的debugserver和电脑内的debugserver是否是同一个东西。

当然结果也有可能是不一样的,因为手机内的debugserver有可能是从别的电脑,别的版本的系统里面拷贝进入的,只不过所有LLDB的指令集都是统一的罢了

总结一下,debugserver的作用就是连接Mac上的LLDB,达到Mac利用LLDB控制debugserver,从而debugserver控制iPhone的作用。

如图:

debugserver4

既然debugserver是一个服务,那么服务就必然需要开辟一个端口,让远程客户端通过这个端口来连接这个服务。我们可以在Mac连接上手机之后,通过如下命令开启这个端口:

// debugserver *:端接口 -a APP名称/进程号
debugserver *:12346 -a EncryptApp
// 或者
debugserver *:12346 -a 3657

12346端口号可以随便写,但是要注意的是不要跟别的端口号重复既可。
如果不知道APP名称/进程号,可以通过命令ps -ax查找。
如果知道APP名称或者部分APP名称可以追加grep命令进行筛选ps -ax | grep EncryptApp

debugserver5

如果有如下报错

error: failed to attach to process named: "" (os/kern) invalid task Exiting.

证明,已经该APP已经被debugserver给占用了,尝试检查xcode或者终端是否已经连接了手机。

debugserver6

关闭其他占用该App的debugserver后,重新开启端口,若出现如下信息则代表成功:

image.png

之后再开启电脑端的LLDB模式,并且吸附该端口就可以了。
相关命令如下

// 开启lldb状态
> lldb
// 吸附对应端口
> process connect connect://localhost:12346
// 查看该端口下对应进程的所有image地址,第一个为该App的ASLR
> image list

LLDB吸附

同xcode一样,LLDB状态下的app是被挂起,无法被操作的。
使用指令c可以取消挂起App,使用指令exit可以退出LLDB状态。更多指令可查看官方指令集

Dump出已加密的二进制

既然开始实操砸壳,那么就肯定需要一个已经加密的App作为对象,我们还是使用老朋友优酷😝。
部分可分为:
1、启动App,开启端口,开启LLDB状态,吸附该端口
2、查看该MachO加密的部分的位置和加密部分的大小
3、查看该App在内存中的的ASLR
4、利用memory read -force指令Dump出解密部分的二进制文件
5、利用dd指令将Dump出的二进制文件重写如原来的MachO文件中
6、更改MachO文件中的cryptid0

Step 1、不在复述,同上一步
Step 2、查看该MachO加密的部分的位置和加密部分的大小
> otool -l Youkui4Phone | grep crypt

image.png

     cryptoff 16384     // 文件偏移
    cryptsize 103153664 // 加密文件大小
      cryptid 1         // 已经加密
Step 3、查看该App在内存中的的ASLR
> image list

ASLR

ASLR为:0x00000001000c0000

Step 4、利用memory read -force指令Dump出解密部分的二进制文件
// memory read --force --outfile 输出文件的目录名称 --binary --count 加密文件的大小  ASLR+文件偏移
> memory read --force --outfile ./Desktop/decrypted.bin --binary --count 103153664 0x00000001000c0000+16384

decrypted.bin

此时在桌面可以发现一个decrypted.bin文件。

Step 5、利用dd指令将Dump出的二进制文件重写如原来的MachO文件中
// dd seek=文件偏移地址 bs=单位为1子节 conv=转换方式(保留未被截取部分的内容) if=输入文件地址 of=输出文件地址
> dd seek=16384 bs=1 conv=notrunc if=./decrypted.bin of=./Youkui4Phone.app/Youkui4Phone

image.png

Step 6、更改MachO文件中的cryptid0

此时该MachO其实已经被解密成功了,但是MachO的cryptid字段还未被更改,所以cryptid此时还是唯1的。

cryptid1
cryptid2

直接打开MachO,手动更改cryptid字段为0。

cryptid3
cryptid4

验证是否真的成功

利用class-dump的原理,如果砸壳成功,可以导出头文件,如果失败则不能导出头文件。

(3) frida-ios-dump

在真正日常开发中,当然不可能每次砸壳多来这么一遍,既耗时又有耗心力,已经有张总给我们写出了非常简单易用的砸壳工具,配置好就可实现一条命令完成砸壳。具体可查看frida-ios-dump

原作者发的配置文档: 一条命令完成砸壳
网友写好的详细文档: Frida-ios-dump一键砸壳菜鸡版

已经有非常优秀的配置文档我这就不再复述。

那么是否只要配置好Frida-ios-dump就能无敌天下,在砸壳路上就一帆风顺呢?

答案当然不是的。

三、特殊的壳

记得上面提到的提到的砸壳原理是从内存中dump已经解密的文件,那如果是否有文件在App运行的不再内存中呢?

肯定是有的,比如iOS中常用的动态库framework,有部分的动态库只有在使用其功能的时候才会内加载到内存中,在使用frida砸壳的时候这部分的framework一定是砸壳失败的,砸壳失败在我们对其重签名的时候必然也会失败,在进行分析的时候也就加大了难度(先不讨论非重签名的调试方式),例如:某宝,肥死book等。
使用MonkeyDev对其重签名的时候会报如下错误:

/Users/xxx/Desktop/xxx/TargetApp/xxx.app/Frameworks/xxx.framework/xxx This file is encrypted! please use github.com/AloneMonkey… to decrypt!

看过dyld源码的同学肯定知道dyld在接管App启动的时候的步骤:
1、配置环境变量 2、加载共享缓存库 3、实例化主程序 4、加载动态链接库 5、链接主程序 6、加载Load和特定的C++的构造函数方法 7、寻找APP的main函数并调用

自然我们利用dyld的一些方法就可以获取到当前App所有的framework

具体代码可见libAllFramework

使用方法:
1、下载好工程,Bulid工程 2、将Build成功的framework拷贝到手机的Home目录

> scp -r -P 12345 libAllFramework.framework root@localhost:~/

Copy libAllFramework

这里需要注意Build的环境应该是真机环境,并且注意framework的权限

framework权限

3、利用DYLD_INSERT_LIBRARIES执行libAllFramework

// 查看Facebook目录地址
> ps -ax | grep Facebook
// 插入我们的动态库
> DYLD_INSERT_LIBRARIES=libAllFramework.framework/libAllFramework /var/containers/Bundle/Application/B4217EF2-9F16-453F-B1DE-9EC5D0E69B60/Facebook.app/Facebook

loadAll

可以在看到绝大多数的动态库已经被正确的加载了,只有一个名为FBCardIOSDKWrapperFramework的动态库加载失败,笔者至今没有找到原因为什么会失败,如果有了解的道友请告知一下,万分感谢。

但这并不影响大局,因为我们只需将FBCardIOSDKWrapperFramework从源文件中删除即可。再次对其砸壳后,重签名即可成功。

不想下载源码的同学也可以直接下载笔者打包好的framework(arm64架构),直接Copy入手机。

四、总结

本文讲述了在iOS系统中什么是传统意义上的壳,也就是对App进行对称加密。Apple是在什么时候对其加密的。砸壳原理,实操手动砸壳,利用工具砸壳,以及最后举例了一个砸壳会遇到的一些坑。

希望在日后的工作中砸壳将是一种轻松加愉快的事情😸

五、参考

1、TZLoadAllLibs
2、一条命令完成砸壳