[iOS 逆向 13] 代码混淆

背景

经过逆向工程实践,可以发现静态分析在整个过程中是不可缺少的,而且静态分析工具生成的伪代码极大地提高了分析效率。想象一下如果没有静态分析,实现解除会员限制的过程:连接界面调试器 Reveal,找到目标界面,获取按钮地址,打印按钮的响应事件,获取响应方法的 C 函数指针,连接 LLDB 给该函数打断点,但是该函数内有大量的分支语句,每个分支都要通过调试才能判断是不是确定会员权限的函数,分析“一天”后找到目标函数,编写 hook 代码。致命问题是,对部分反调试策略无法应对,因为不知道在哪打断点,如果一条一条地执行指令来寻找反调试入口,可以视为找不到。

如果缺少了界面调试工具和动态调试工具,但有静态分析的汇编代码和伪代码在,只会略微增加分析时间。如果有静态混淆策略,首先在大型项目中应用的可能性就不大,就算有,因为系统符号名是不能替换的,所以可以从系统类入手。比如某个行为会弹窗,就在代码中搜索 UIAlertAction 之类的字眼,缩小目标范围,然后利用 IDA Pro 给符号重命名为有意义的字符串,感觉这个功能就是给名称混淆准备的。

综上,增加静态分析的代码逻辑复杂度才是保护 App 的关键。那么有没有办法让反编译得到的代码难以阅读呢?这就要从二进制代码的源头入手——在编译过程中替换指令、增加无用控制流、使代码扁平化等。因此,需要改变编译器生成代码的过程,而 LLVM 架构为这个思路的实现提供了可能。

LLVM

编译器架构一般分三个模块,分别是前端、优化器和后端。

源代码在预处理过程中展开宏定义,导入头文件,处理条件编译指令等,然后开始分词。分词过程中识别单词或者符号的类型并记录位置,比如一个左括号被识别为 l_paren + 位置。然后按照语言规则构造语法树,同时检验了语法。遍历语法树,将各节点翻译为一种中间代码。以上是编译器前端的工作,最终输出中间代码。对于传统编译器,每种语言的编译器前端生成的中间代码格式都不相同。前端生成的中间代码作为优化器的输入,优化器从时间、空间上优化代码,然后输出给后端。后端根据目标 CPU 支持的指令集、寄存器等信息生成目标文件。

LLVM 编译器架构与前面传统的编译过程不同的是,每种语言的前端输出的都是同种类型的中间代码,这个中间代码像是一种汇编,但在函数声明上又有一些高级语言的样子。这样做的好处是,对 IR 优化的工作可以完全复用。LLVM 架构用 Clang 作为 C 和类 C 语言的前端;优化器由多个流程(Pass)组成,各个流程对中间表示(IR)也就是中间代码从不同方面进行优化。LLVM 架构中的 IR 即 Bitcode。

苹果从 Xcode 7 开始默认使用 LLVM 编译器架构。在 App 准备上传时,打包 Archive 版本会把 Bitcode 附加到可执行文件中,查看 Mach-O 文件可以看到多出一个 LLVM 段 __LLVM,__bitcode。 苹果服务器会对 Bitcode 进一步优化,然后从 Bitcode 重新生成目标文件,因此从 App Store 下载下来的可执行文件和上传时的可执行文件会不一样。一些比较旧的第三方库不支持 LLVM 的 Bitcode,整个项目必须用传统方式编译,这时下载下来的可执行文件代码段会和上传时的相同。

回到正题,代码混淆中的改变执行流程、添加垃圾指令等操作,就相当于对代码进行负优化。而各个 Pass 正是负责对中间代码的处理、优化,因此我们可以添加自定义 Pass 来实现代码混淆。

Pass Demo

编译配置

从 GitHub 下载 LLVM 的源码,在 lib/Transforms/ 目录下可以看到 Hello 文件夹,这是 LLVM 自带的 Pass 入门项目,先模仿一下它。在相同目录下新建一个 MyDemo 文件夹,里面添加一个 cpp 文件和 CMakeLists 文件。CMakeLists 文件中使用 add_llvm_loadable_module 函数;在 Transforms 目录下的 CMakeLists 中添加 add_subdirectory(MyDemo)。然后为了方便写代码、读源码,用 CMake 生成 Xcode 项目,cmake ../llvm -G "Xcode"。用 Xcode 打开后,即可找到:
[iOS 逆向 13] 代码混淆_第1张图片
然后设置 Xcode 的编译 Scheme 为 MyDemo 的 Target,这样就可以开始写代码了。

编写 Demo

Pass 基类中提供了一些虚函数,不同的子类通过实现这些函数来提供不同的功能,系统默认实现了一些子类。 Demo 中定义一个类继承 FunctionPass 就可以操作代码中的函数了,导入相应的头文件,Demo Pass 声明如下:
[iOS 逆向 13] 代码混淆_第2张图片
LLVM 用变量 ID 的引用来识别 Pass,可以赋任意值;MyPass 重载了 runOnFunction,这里只打印函数名。然后指定使用该 Pass 的命令行参数为 demo:
static RegisterPass X("demo", "use demo pass", false, false);
开始构建,会生成 MyLLVMDemo.dylib 文件,下面使用这个 dylib。写一段测试代码,包含 func 函数 和 main 函数,然后使用 clang -emit-llvm -c 生成 Bitcode 文件。然后在 Xcode Scheme 中选择 opt 并构建。可以在命令行中使用 opt,但使用 Xcode 可以调试,用 Xcode 给 opt 的 Scheme 添加以下参数: [iOS 逆向 13] 代码混淆_第3张图片
如果添加 -help 参数,会打印出该 Pass 接收的参数;运行时会输出函数名 func 和 main,用 Xcode 打断点,可以调试 Pass 过程:
[iOS 逆向 13] 代码混淆_第4张图片
可以在 opt 的 CMakeLists 中添加 MyPassDemo 的依赖,这样修改 Pass 代码后可以只构建 opt 也能让最新代码生效。

Obfuscator-LLVM

该论文主要介绍了三个 Pass 用于混淆。

  • 指令替换:将操作符替换为一系列的指令,例如a = b ^ c 替换为a = (b & !c) | (!b & c)
  • 控制流扁平化:把 if-else 自上而下的结构替换为 switch 扁平的结构,使逻辑变混乱。
  • 添加无用控制流:在原函数入口导向无用代码块,额外经历一系列无用流程。

现在要让 Clang 编译程序时加载以上 Pass,需要重新构建 Clang。由于该论文中使用的是比较旧的 LLVM-4.0,我下载了 LLVM-7.0.1 用于移植实验,需要执行以下操作:

  • 复制旧项目 lib/Transforms/Obfuscation 目录到相同位置,里面是 Pass 代码;
  • 修改 lib/Transforms 目录下的 CMakeLists,添加子目录 Obfuscation;
  • 修改 lib/Transforms 目录下的 LLVMBuild.txt,添加 Obfuscation;
  • 修改 Transforms/IPO 目录下的 LLVMBuild.txt,添加 Obfuscation;
  • 修改 Transforms/IPO 目录下的 PassManagerBuilder.cpp,把旧项目中相同位置关于 Obfuscation 的代码复制过去。

在 Xcode Scheme 中选择 Clang 并构建。构建完成后,用其编译下面的测试程序:
[iOS 逆向 13] 代码混淆_第5张图片
先不添加参数,生成可执行文件后用 IDA Pro 打开,生成的伪代码如下:
[iOS 逆向 13] 代码混淆_第6张图片
然后添加编译参数:-mllvm -sub 启用指令替换;-mllvm -bcf 启用伪造无用流程;-mllvm -bcf_prob=100 启用伪造流程时,一个基本块被生成无用代码的概率为 100%。
./clang test.c -o test.o -mllvm -sub -mllvm -bcf -mllvm -bcf_prob=100
生成可执行文件后,同样用 IDA Pro 反编译生成伪代码。可以看到,仅 func 函数就非常冗长,逻辑也不明显,这种混淆方式的效果比较好,可以极大地增加静态分析的难度。
[iOS 逆向 13] 代码混淆_第7张图片
在替换 Xcode 编译器之前,需要加一个 Target,执行cmake -DLLVM_CREATE_XCODE_TOOLCHAIN=ON ../llvm -G "Xcode",这样 Xcode Scheme 中才有 install-xcode-toolchain,选中并构建。注意硬盘要有 40G 左右的空闲空间,因为最终生成的工具链为 14G,编译过程中的文件 25G。生成的工具链位于 /usr/local/Toolchains,将其移动到 /Library/Developer/ 目录。打开 Xcode - Preference - Components,选择刚刚生成的工具链。
[iOS 逆向 13] 代码混淆_第8张图片
此时打开 Xcode 也可以看到当前使用的工具链:
[iOS 逆向 13] 代码混淆_第9张图片
用新工具链编译 Xcode 项目,报错unknown argument: '-index-store-path',需要到项目 Build Settings 关闭 Index-While-Building,因为这个参数在开源的 LLVM 项目中没有实现。然后编辑项目的 Compiler Flags,添加编译参数。[iOS 逆向 13] 代码混淆_第10张图片
我在参数中设置对基本块伪造无用流程的概率为 40%,那么项目的核心功能代码有六成概率不会被混淆,比较危险。但如果将概率设为 100%,大量无用代码势必减慢程序运行速度,因此需要对不同源文件采用不同的编译参数。前面提到过用命令行手动构建、打包 App,编写 makefile 确实可以做到精准控制,但是很麻烦。幸好 Xcode 提供了这个功能,在项目 Build Phase 中可以对源文件指定编译参数。
[iOS 逆向 13] 代码混淆_第11张图片
这样就可以实现对核心代码高度混淆、对不重要的代码部分混淆或不混淆。还有一个问题,项目中使用的第三方静态库,我们没有库的源码,如何进行混淆?对于启用了 Bitcode 的第三方静态库,可以先用 ar -x 命令解压得到其中的目标文件,因为 Bitcode 是保存在 Mach-O 文件的 LLVM 段 bitcode 节中,使用 segedit 命令提取出 bc 文件,然后用自己编译的 Clang 给定混淆参数将所有 bc 文件编译打包成新的静态库。这样做的缺点是新静态库没有 Bitcode,导致主项目也必须关闭 Bitcode 功能。

你可能感兴趣的:(iOS逆向)