iOS 代码签名的校验

最近学习了一下iOS代码签名机制 ,这里做个笔记记录,整理思路,加深理解。
想了解更多iOS签名相关,强烈推荐去看看原文,写的太棒了。原文出处 深度长文:细说iOS代码签名 。

背景

经过前面几章的学习,我们已经了解 iOS 代码签名相关知识,这章主讲签名的校验过程。签名的校验并非一次性完成,在安装、启动和运行时有着不同的校验规则。

安装

App安装时的校验由位于iOS设备上的 /usr/lib/libmis.dylib (dyld_shared_cache)提供。

App的安装是由 /usr/libexec/installd 完成的,installd 会通过 libmis.dylib 校验。

这里先校验 ProvisioningProfile 的合法性,确定 ProvisioningProfile 合法后,这个文件会自动被拷贝到iOS设备的 /Library/MobileDevice/Provisioning Profiles/ 路径下。再用 ProvisioningProfile 来校验 App 二进制文件中的 Code Directory 内 Entitlements、签名证书是否合法,以及本机的UUID等是否满足来自官方的授权。

上面的步骤满足后,根据证书内的公钥,校验最终的签名是否合法。进一步,递归调用校验每一步生成的哈希值:CDHash(整个Code Directory的hash值), Code Directory(macho文件每页及特殊负数页的hash值),_CodeSignature/CodeResources(bundle内资源文件的hash值)。

总结上述流程, ProvisioningProfile合法性 -> Entitlements、证书、设备UUID合法性 -> 签名的合法性 -> 每一个步骤生成的哈希值:CDHash, Code Directory, _CodeSignature/CodeResources。

启动

进程启动时,loader 会先将可执行文件加载到虚拟内存,在加载的过程中 mach_loader 会自动解析 MachO 文件中的 LC_CODE_SIGNATURE 并进行校验,可以参考 mach_loader 的代码 bsd/kern/mach_loader.c

load_code_signature 在解析完签名的数据后,最终还是调用到 libmis.dylib 来校验签名。这一校验过程基本与安装时一致,防止安装后的篡改。

需要注意的是,加载过程中为了提升加载效率,签名校验并不会去检查Code Directory与实际的代码是否匹配,仅仅只检查了CMS Signature,根据证书中的公钥,校验 CDHash 的合法性。

运行时

当一页代码被加载到虚拟内存后,会立即触发 page fault,此时内核中的 vm_fault 函数会被调用,紧接着调用 vm_fault_enter,在 vm_fault_enter 的实现中会判断代码页是否需要签名校验,并执行校验的操作,参考代码 osfmk/vm/vm_fault.c

当需要校验时,会计算当前代码页的哈希值,并与签名中 CodeDirectory 记录的值进行比对,完成代码签名的验证。如果不符,且不满足系统预设的例外条件,则会向内核发出CS_KILL指令,将进程结束。

至此签名的校验流程就全部完成了。

越狱

越狱之后,签名校验机制会被破坏掉,否则用于实现越狱的代码自身就无法运行。比如在 iOS6/7 时代,典型的方式是替换 libmis.dylib 中的 _MISValidateSignature 函数,使其永远返回验证成功,简单粗暴但很有效,因此越狱的设备可以不受签名限制运行任意程序。但是单纯解决掉这个函数只是解决了MachO 文件的 Load 问题,运行时仍然会有沙盒和 Code Directory 的校验,想要对系统完全的控制权必须同时解决掉这两个问题。

由于沙盒机制的实现分散在系统的各个角落,没有简单的方式可以将沙盒一刀切地屏蔽掉,因此一般越狱并不会破坏掉沙盒。但因为越狱设备签名校验机制被绕过,不再会根据 embedded.mobileprovision 文件检查 Entitlements 的合法性,因此我们可以在沙盒范围内,声明任意的权限。Code Directory的校验在内核层,破解难度相对较大,并且完全没有必要进行破解,因为Code Directory只是单纯地校验未加密的哈希值而已,只需要按照代码签名的格式做好 Code Directory 即可。

重签名

有的时候出于各种原因,我们需要对一个App进行重签名,然后在自己的设备上进行测试。回顾一下签名的必备条件:

  • 开发者证书,以及对应的密钥
  • Entitlements文件
  • embedded.mobileprovision

开发者证书和密钥我们已经有了,对于 Entitlements 和 embedded.mobileprovision 文件,为了确保重签后的 App 能够正常运行,必须使用和原 App 相同或者至少包含原 App 所需权限的 Entitlements 文件。这个并不难操作,只需要新建一个工程,开启相应的功能,让 Xcode 自动为我们生成即可。但是 Entitlements 文件中还有一些跟 Team ID 和 App ID 相关的配置,这两个是没有办法伪造的,因为我们不能使用已经被其他开发者注册过的ID。使用自己的ID一般也不会有什么问题,但在某些情况下可能导致最终的程序逻辑出现异常,这根具体的代码实现细节有关。

Entitlements 文件中还标识了 application-identifier,也就是 Bundle ID,正常签名的 App 中,这个值和 Info.plist 中的CFBundleIdentifier 的值是相同的,但实际在签名校验过程中,系统并不会检查二者是否一致。因此即使 Entitlements 中与 Info.plist 文件使用了不同的 Bundle ID ,理论上也不会影响重签名之后的运行。

需要注意,App 中除了可执行程序文件外,还会可能会有 Frameworks 及 Plugins,里面都会包含二进制的代码文件,他们的哈希值也会被存储在 _CodeSignature/CodeResources 中。所有的二进制代码都必须进行签名,而签名后二进制文件的哈希值就会产生变化,因此需要先对这两个文件夹下的二进制文件进行签名,再对 App 进行签名。

重签名的基本流程,使用 -f 参数可以强制覆盖掉已有的签名:

$ # 对Frameworks及Plugins中的每一个文件进行签名,此时不需要指定entitlements
$ codesign -f -s "证书名称或者SHA1值" Target.app/Frameworks/xxxxx.framework
$ codesign -f -s "证书名称或者SHA1值" Target.app/Frameworks/libxxxx.dylib
$ ...
$ # 将准备好的Provisioning Profile拷贝到App根目录
$ cp ~/Library/MobileDevice/Provisioning\ Profiles/xxxxx.mobileprovision Target.app/embedded.mobileprovision
$ # 对App进行签名
$ codesign -f -s "证书名称或者SHA1值" --entitlements resign.entitlements Target.app

你可能感兴趣的:(iOS 代码签名的校验)