中文站

无源码 iOS 加固的实践方案

导读:在移动安全中,客户端层的代码安全非常重要,因为移动应用程序通常在客户端设备上执行,并处理用户敏感数据。通过加固应用程序,可以有效抵御逆向工程和恶意攻击,保护应用程序的机密信息和知识产权。加固通过代码混淆、加密保护和反调试等手段,为应用提供了强大的安全防护层,增加了攻击者破解和修改应用程序的难度。

一、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 加固产品,旨在为开发者提供全面的应用程序保护解决方案。

点击了解更多iOS应用加固相关信息