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