导读:在移动安全中,客户端层的代码安全非常重要,因为移动应用程序通常在客户端设备上执行,并处理用户敏感数据。通过加固应用程序,可以有效抵御逆向工程和恶意攻击,保护应用程序的机密信息和知识产权。加固通过代码混淆、加密保护和反调试等手段,为应用提供了强大的安全防护层,增加了攻击者破解和修改应用程序的难度。
一、iOS 加固方案
目前主流的加固方案分为三种:
源码加固:直接基于源码工程进行混淆,该模式一般只能在开发者环境下部署加固工具,需要额外做一些环境配置。
BItCode 加固:因为 Bitcode 本质上是 IPA 编译过程的中间代码,其加固原理和源码并无太大区别,主要区别在对接方式,通过上传带 BitCode 的包体,加固流程可以在加固厂商进行,减少对接和环境部署的成本。
无源码加固:基于 ipa 包的加固,因其作用于二进制文件,功能上控制力度没有源码灵活,但接入成本低。
在新的 xcode15 发布后,官方已经移除了 bitcode 的生成开关。
https://developer.apple.com/documentation/xcode-release-notes/xcode-15-release-notes
这使 iOS 客户端加固可能最后只剩下源码加固和无源码两种方案。因源码加固方案原理上是开源且有参考依据的,这里不做过多讲述,下面简单介绍无源码加固的一些原理。
二、iOS 无源码加固
任何平台的无源码加固都无法脱壳文件格式,iOS也不例外。同Android下ELF文件, iOS 的可执行文件是 MACHO 格式文件的一种。从程序加载运行角度, MACHO 和 ELF 有以下相同点和不同点。
相同点:
○ 都是用 Segment 描述内存加载范围和权限,并使用 Section 对代码和数据进行更详细的划分。
○ 都包含符号表和字符串表记录函数,变量信息。
○ 都有重定位概念。
不同点:
○ iOS Section 有更明确的意义,尤其是 objc 以及 swift 相关的代码信息。
○ iOS 支持懒加载。
○ iOS 对符号有更严谨的分类和排序。
○ iOS 中 load command 更类似于 ELF 中 dynamic 和 phdr 的组合,是对程序结构和依赖的描述。
要理解一个平台程序的加固方式,首先需要理解程序加载和执行流程。
加载
加载流程包括内存映射,内存修复和初始化过程。
Macho 从文件内容看,包含三大块数据结构。
○ Macho Header :包含了 Mach-O 文件的基本信息,如文件类型、CPU 类型、Load Command 数量等。
○ Load Command :每个加载命令都包含一个标头和数据。标头包含了加载命令的类型、大小和其他相关信息。
○ Segment:用于描述 Mach-O 文件中的代码段、数据段和加载器需要的数据信息。
Load Command 所有关联的数据都可以通过解析 Command 结构读取,且数据必定在以上数据范围。整个 Macho 文件大小等于以上三块数据大小总和。
程序在编译过程中存在一些约定,部分自实现的变量或函数的调用,使用的是一个相对地址,可以简单理解成相对于程序加载的首地址的偏移 ,而不是真实地址。但程序加载到内存,首地址一般是随机的,运行前需要对这些地址进行修复,这个过程称之为 rebase。
程序开发过程中,如用到动态库中的函数或变量时,生成的二进制产物中,会标识这些函数或变量是需要导入的,会在一块地址区域(got 表)预留该函数地址。动态链接的过程就是要修复这些位置的函数地址或者变量值,而这个过程被叫做 bind。
程序加载过程简单理解可以分成以下三个流程。
○ 按照 Segment 设定的地址,分配数据内存,并分配权限。
○ 按照 rebase 中的规则,对编译生成的相对地址进行修复,转成真实地址。
○ 按照 bind 中的规则,对编译依赖的其他函数和变量地址进行修复,保证程序能正常调用。
因 iOS 版本迭代,rebase 和 bind 的描述以及数据存储方式在不同版本之间是有差异的。
iOS14 以下可以通过解析 LC_DYLD_INFO 或者 LC_DYLD_INFO_ONLY 来进行 rebase 和 bind 操作。
rebase 和 bind 操作都遵循 iOS 自定义一套 opcode 进行解析,可以参考 dyld 源码去理解 opcode 的处理过程。
rebase 通过一系列操作码进行特定运算,结果是为了保证 app 经过加载,内存基址随机化的情况下,程序关联的内部地址可以得到修复。
bind 使用的是另一套编码,bind 过程用于绑定符号,比如程序使用其他库中的变量函数等情况,需要将地址信息写入程序内存,保证正常访问和使用这些函数和变量。
iOS14 以上虽兼容 14 以下格式,但提供新的格式(Fixup chains)来完成动态链接。当编译 app 时选择仅支持 iOS14 以上的情况下,MACHO 文件中,不再有 LC_DYLD_INFO 或 LC_DYLD_INFO_ONLY,取而代之的是 DYLD_CHAINED_FIXUPS和DYLD_EXPORTS_TRIE。
○ DYLD_CHAINED_FIXUPS 对应旧版本的 bind 和 rebase,其中删除了 lazy bind 机制,但仍有 bind 和 weak bind 之分。
○ DYLD_EXPORTS_TRIE对应旧版本的导出符号。
DYLD_CHAINED_FIXUPS的数据关系图如下:
以上 iOS 重新定义了一套重定位数据的方法,Fixup chains 的数据关键功能有两个:
○ 初始化 import table,导入表是一张链表或数组,其中包含符号信息和lib信息,指向符号的来源,其数组索引被bind数据关联。
○ 引导 dyld 找到 dyld_chained_starts_in_segment 数据,计算出代码数据中真正需要进行修复的地址,地址一般在 got 表或一些引用的本地变量。
而在数据部分,采用不同于之前版本的初始化方法。
○ 旧版格式在 linkedit 中直接指定好了哪些地址需要进行 rebase,哪些地址需要 bind。真正要修复的地址中,默认值可能是 0 或者是一个相对地址。
○ 新版格式 fixupchains 只是获取数据的首地址,而数据需要以链表的形式,通过 ChainedFixupPointerOnDisk 的结构进行解析,根据结构类型做对应的修复操作。
新版本 MACHO 在格式上添加了复杂度,但在存在大量重定位的程序中,可以很少地减少数据占用的体积。
执行
执行流程涉及程序内部默认执行的一些接口。这里我们更关注 main 函数启动之前的行为。
○ 加载并注册类(Class)和类扩展(Category)中的方法。
○ 调用 ObjC 的 +load 函数。
○ 执行__mod_init_func 中的函数其中包含但不限于以下规则中声明的函数。
○ 声明为__attribute__((constructor)) 的 C 函数。
○ 全局变量中已经初始化的对象。
○ 执行 main 函数。
关注启动流程,是为了在做加固时,在合适的时机对加固相关的功能进行初始化,比如在 load 中进行初始化,或者是在插入 init 执行初始化。需要保证初始化时机在加固功能生效之前。
方案和效果
在确认程序的加载和执行流程后,我们可以对程序进一步做类似数据加密、混淆变换、功能对抗等操作,以达到需要的保护效果。二进制的加密混淆几乎都是基于汇编和文件去处理的,相对于源码有一定难度,但处理得当,其分析难度并不比源码加固差,相反,会因为汇编和文件处理的灵活性,更容易对特征进行修改。
以下是其中一种保护效果:
从效果看,不同于普通的混淆,表面上存在一层抽取,并对真实代码二次进行了混淆处理,不仅无法分析函数体,甚至对于参数类型也无法正常解析。
三、结语
在本文中,我们详细介绍了部分 iOS 应用程序加固的原理。希望能从中帮助一些需要了解加固朋友了解一些原理上的知识。在当前数字化时代,应用程序安全至关重要。为了确保用户数据和业务的安全,我们不断完善 iOS 加固产品,旨在为开发者提供全面的应用程序保护解决方案。