iOS混淆探索

背景

代码混淆一直是安全加固一项重要的能力,目前我司的加固能力都是外采,为了便于后期定制化开发以及节约成本,故进行代码加固的自研探究。在本次自研加固项目中,我主要负责iOS方向的加固工作,截至目前已经测试完成,故整理此文记录一下过程踩过的坑。

发展史

提到代码加固以及混淆由易到难大致分为四个阶段:

字符串加密

字符串会暴露APP的很多关键信息,攻击者可以根据从界面获取的字符串,快速找到相关逻辑的处理函数,从而进行分析破解。一般的处理方式是对需要加密的字符串加密,并保存加密后的数据,再在使用字符串的地方插入解密算法。简单的加密算法可以把NSString转为byte或者NSData的方式,还可以把字符串放到后端来返回,尽量少的暴露页面信息。例如把NSString转为16进制的字符串:

+ (NSString *)globalString {
​
return@"5f48494748203d20323b202020202020202020202020202020202020202020202020202020202020676c6f62616c2e44495350415443485f51554555455f5052494f524954595f44454641554c54203d20303b2020202020202020202020202020202020202020202020202";
​
}

符号混淆

符号混淆的中心思想是将类名、方法名、变量名替换为无意义符号,提高应用安全性;防止敏感符号被class-dump工具提取,防止IDA Pro等工具反编译后分析业务代码。由于收到审核问题,目前市面上的IOS应用基本上是没有使用类名方法名混淆的。

逻辑混淆

代码逻辑混淆有以下几个方面的含义:

  • 对方法体进行混淆,保证源码被逆向后该部分的代码有很大的迷惑性,因为有一些垃圾代码的存在;
  • 对应用程序逻辑结构进行打乱混排,保证源码可读性降到最低,这很容易把破解者带到沟里去;

混淆之后,拥有和原始的代码一样的功能,这是最关键的,不会破坏代码逻辑。本次我们采用的也是这种能力,后续会进行深入分析。

虚拟化

代码的虚拟化是安全加固级别最高的能力,虚拟化实际上就是使用一套自定义的字节码来替换掉程序中原有的指令,而字节码在执行的时候又由程序中的解释器来解释执行。自定义的字节码是只有解释器才能识别的,所以一般的破解工具是无法识别自定义的字节码,因此基于虚拟机的保护相对其他保护而言要更加难破解。

适配过程

OLLVM(Obfuscator-LLVM)是瑞士西北应用科技大学安全实验室于2010年6月份发起的一个项目,该项目旨在提供一套开源的针对LLVM的代码混淆工具,以增加对逆向工程的难度。

github.com/obfuscator-…

1. 快速理解LLVM、clang和OLLVM

llvm是一个完整的编译器架构,作用可以理解为制作一个编译器,llvm先将源码生成为与目标机器无关的LLVM IR代码,然后把LLVM IR代码先优化,再向目标机器的汇编语言而努力。经典编译器都可以分为前端、中层优化和后端:

3_EAE47Q4U4D7NU5H.jpg

我们从上图也理解了clang,是前端的一个套件,但在实际使用时,我们对于clang是最为熟悉的,因为编译的时候,是调用clang或clang++来编译源码。而OLLVM是基于LLVM的代码分支的代码混淆,在中间表示IR层,通过编写自定义的pass来实现IR的混淆,这样目标机器的汇编语言也就被混淆了:

3_WV6QCZ8HVUYUUP9.jpg

即使不用OLLVM,LLVM本身也是有很多pass的,LLVM IR本身是一种与目标机器无关的虚拟化代码,而在转化为真实汇编代码时,肯定要删除一些虚拟的东西或者做特定的优化,必然也需要pass。

2. OLLVM的适配

OLLVM有三大功能,分别是:Instructions Substitution(指令替换)、Bogus Control Flow(混淆控制流)、Control Flow Flattening(控制流平展)。Github上也有OLLVM每个功能详细的介绍和举例:

github.com/obfuscator-…

针对功能说明在此不做赘述,可自行查看,主要分享一下适配过程。该项目仅支持到OLLVM-4.0,后续转向商业项目strong.protect。所以如果我们想要使用这些混淆pass,需要进行最新版的LLVM以及clang的适配工作。

2.1 下载LLVM

llvm地址:github.com/llvm/llvm-p…

swift-llvm地址:github.com/apple

从上面的地址下载最新的自己需要的llvm和clang,此时需要注意要找自己Xcode版本对应的clang版本来进行移植。通过clang --version可以查看自己的版本,之后在下方查询对应关系。 en.wikipedia.org/wiki/Xcode#… image-20230731175912163.png

2.2 添加混淆代码

主要就是将OLLVM文件夹里/include/llvm/Transforms/Obfuscation/lib/Transforms/Obfuscation移动到刚才下载好的llvm源码文件夹相同的位置。然后需要手动修改以下文件: 3994053-0e44f460d8fc63fa.png.webp 3994053-3d3054a05c96b72a.png.webp 3994053-358e0b85dec95f1a.png.webp 3994053-79683114fe1a73c7.png.webp 3994053-b6703baf65c2858c.png.webp 3994053-d3c16047c368349d.png.webp 3994053-e76e7039112a47c8.png.webp 3994053-e117da8394ea0169.png.webp

2.3 编译&使用

mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_CREATE_XCODE_TOOLCHAIN=ON ../obfuscator/
make -j12

2.4 适配Xcode

cd build
sudo make install-xcode-toolchain
mv /usr/local/Toolchains /Library/Developer/

2.5 Swift支持

因为Apple大量的修改以及优化,适配Swift想直接编译llvm就适配基本不可能,我们可以选择编译swift的工具链来实现,这个时候只需要下载Swift的源码,它在编译Toolchain时下载的llvm上把之前的修改移植过来,然后编译出来就可以直接支持swift的混淆了。

官方的教程在这里: github.com/apple/swift…

这边给到我总结的大致步骤,仅供参考。首先在文稿中新建一个文件夹,然后 cd 到我们的目录:

$ mkdir ~/Documents/swift-project
$ cd ~/Documents/swift-project

然后找到Xcode所支持的 Swift 版本,比如我的 Xcode 为 14.3.1 版本,所以直接下载 Swift 5.8.1 Release。查找 Xcode 对应的 Swift 的版本有两种方式:

  1. 去官网,查看 Xcode 的 Release Notes,在 Overview 中会有介绍。例如:Xcode 14.3.1Release Notes
  2. 终端运行命令查看 xcrun swift -version

在刚才新建的目录中执行如下命令拉取对应的 Swift 源码,并 cd 到源码目录:

$ git clone --branch swift-5.8.1-RELEASE [email protected]:apple/swift.git
$ cd swift

拉取源码后还须拉取依赖

$ utils/update-checkout --tag swift-5.8.1-RELEASE --clone

最后执行build_toolchain或build_script,其中build_toolchain最简单,全自动,先看看有没有错,不报错就可以做ollvm移植了。

# 后面必须要跟个唯一标识
utils/build_toolchain com.xxxx

2.6 OLLVM遇到的问题

Q:代码中遇到 @available的时候,会报错 _isPlatformVersionAtLeast meishi_20230731_9P9P8MXSFH.png A:没链接clangRT,需要cmake编译时添加:-DLLVM_ENABLE_PROJECTS="compiler-rt"

Compiler-RT(RT指运行时)项目用于为硬件不支持的低级功能提供特定于目标的支持。例如,32位目标通常缺少支持64位除法的指令。Compiler-RT通过提供特定于目标并经过优化的功能来解决这个问题,该功能在使用32位指令的同时实现了64位除法。它提供相同的功能,因此是LLVM项目中libgcc的替代品。 ——《LLVM编译器实战教程》P17

Q: 编译没问题,添加flags也成功编译,但是没有混淆效果

A:主要原因是Legacy PM模式不生效,临时解决方案有以下两种,最好的办法是进行New Pass Manager的适配

  • 在cmake的时候加一下-DLLVM_ENABLE_NEW_PASS_MANAGER =OFF来禁用掉NEW PM
  • 在项目中设置-flegacy-pass-manager的cflag

3. Hikari的适配

Hikari(github.com/HikariObfus…)也是开源的优秀混淆项目,并且支持的功能更加全面。

  • -mllvm -enable-bcfobf 启用伪控制流
  • -mllvm -enable-cffobf 启用控制流平坦化
  • -mllvm -enable-splitobf 启用基本块分割
  • -mllvm -enable-subobf 启用指令替换
  • -mllvm -enable-acdobf 启用反class-dump
  • -mllvm -enable-indibran 启用基于寄存器的相对跳转,配合其他加固可以彻底破坏IDA/Hopper的伪代码(俗称F5)
  • -mllvm -enable-strcry 启用字符串加密
  • -mllvm -enable-funcwra 启用函数封装

Hikari的使用直接参考git地址即可,写的比较全面,此处不再赘述。主要记录遇到的一个刺手问题。

问题描述

对接的某个业务线是OC&Swift混编项目,并且需全部支持Module化,项目中还配置了PCH文件,在实际使用时编译会报错:

image-20230731162938670.png

看报错信息像是clang未开启pch选项,试着去搜索CLANG_ENABLE_PCH,未找到相关的配置项。多方查找无果,最终在Hikari的作者Naville那得到回复:第三方的Clang/LLVM一直不支持Modules。 根据他的回复,试着将项目中C/OC的Modules关闭以后,确实该问题不存在。但是关闭该选项,业务线的适配工作更大,只能另寻出路。 image-20230731163733131.png

此期间安全厂商的技术人员给了新的研究方向,在看了目前我的实现方式以后,他提出直接替换编译链的方式会遇到很多问题,由于Apple-clang不开源,自身做的很多优化与适配,仅靠开源LLVM项目是难以覆盖的。所以最好是做成插入的方式:

  1. 使用Apple clang编译出LLVM IR
  2. 调用自己的混淆库去混淆LLVM IR
  3. 再调用Apple clang编译LLVM IR

按照这个思路,惊喜的发现Hikari的作者已经有了这方面的实践:

If you are targeting Apple platforms on macOS, it's strongly advised to use Hanabi which injects into Apple Clang instead of compiling the full toolchain. This methods takes way shorter time to build (~5 Minutes) and introduces way less compatibility issues

4. Hanabi的使用

Hanabi的项目地址在: github.com/HikariObfus…

该项目的原理是将混淆的pass编译成单独的模块生成动态库。然后将该动态库注入到Apple clang/Apple swift-frontend,然后hook对应的pass加载流程,将自定义的混淆pass注册进去。核心代码不到100行,贴出来大致看一下:

 void (*old_pmb)(void *dis, legacy::PassManagerBase &MPM);
    static void new_pmb(void *dis, legacy::PassManagerBase &MPM) {
     MPM.add(createObfuscationLegacyPass());
     old_pmb(dis, MPM);
    }
    ​
    ModulePassManager (*old_bo0dp)(void *Level, bool LTOPreLink);
    static ModulePassManager new_bo0dp(void *Level, bool LTOPreLink) {
     ModulePassManager MPM = old_bo0dp(Level, LTOPreLink);
     MPM.addPass(ObfuscationPass());
     return MPM;
    }
    ​
    ModulePassManager (*old_bpmdp)(void *Level, bool LTOPreLink);
    static ModulePassManager new_bpmdp(void *Level, bool LTOPreLink) {
     ModulePassManager MPM = old_bpmdp(Level, LTOPreLink);
     MPM.addPass(ObfuscationPass());
     return MPM;
    }
    ​
    static __attribute__((__constructor__)) void Inj3c73d(int argc, char *argv[]) {
     char *executablePath = argv[0];
     // Initialize our own LLVM Library
     if (strstr(executablePath, "swift-frontend"))
       errs() << "Applying Apple SwiftC Hooks...\n";
     else
       errs() << "Applying Apple Clang Hooks...\n";
    #if defined(__x86_64__)
     int ret = 0;
     size_t size = sizeof(ret);
     if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL0) != -1 &&
         ret == 1)
       errs() << "[Hanabi] Looks like you are currently running the process in "
                 "Rosetta 2 mode, which will prevent DobbyHook from "
                 "working.\nPlease close it.\n";
    #endif
     DobbyHook(DobbySymbolResolver(
                   executablePath,
                   "__ZN4llvm18PassManagerBuilder25populateModulePassManagerERNS_"
                   "6legacy15PassManagerBaseE"),
              (dobby_dummy_func_t)new_pmb, (dobby_dummy_func_t *)&old_pmb);
     DobbyHook(
         DobbySymbolResolver(executablePath,
                             "__ZN4llvm11PassBuilder22buildO0DefaultPipelineENS_"
                             "17OptimizationLevelEb"),
        (dobby_dummy_func_t)new_bo0dp, (dobby_dummy_func_t *)&old_bo0dp);
     DobbyHook(DobbySymbolResolver(
                   executablePath,
                   "__ZN4llvm11PassBuilder29buildPerModuleDefaultPipelineENS_"
                   "17OptimizationLevelEb"),
              (dobby_dummy_func_t)new_bpmdp, (dobby_dummy_func_t *)&old_bpmdp);
    }

编译

首先获取LLVM对应的源码,跟其他项目一样,要注意Xcode版本的对应关系。然后将Hanabi的代码下载到llvm项目里面即可:

git clone https://github.com/61bcdefg/Hanabi.git $(LLVM_SOURCE_PATH)/projects/

在$(LLVM_SOURCE_PATH)下,运行git submodule update --init --recursive --remote确保子模块也全部更新。之后进行对应的编译即可:

cmake $(LLVM_SOURCE_PATH) -DCMAKE_BUILD_TYPE=Release -DLLVM_ABI_BREAKING_CHECKS=FORCE_OFF -GNinja
ninja LLVMHanabi

大致等待几分钟,在编译路径下lib文件夹能找到ibLLVMHanabiDeps.dylib、libLLVMHanabi.dylib。将这两个动态库拷贝到 /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/

打补丁

首先下载 github.com/alexzielens…,将之放入到环境变量$PATH,然后按照以下顺序执行:

  • sudo optool install -c load -p @executable_path/libLLVMHanabi.dylib -t /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang
  • sudo optool install -c load -p @executable_path/libLLVMHanabi.dylib -t /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend
  • sudo codesign -fs - /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang
  • sudo codesign -fs - /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend

遇到的问题

Q:编译完成以后,使用会报错:

Applying Apple Clang Hooks...
: CommandLine ErrorOption 'eagerly-invalidate-analyses' registered more than once!
LLVM ERROR: inconsistency in registered CommandLine options

A: submodule的branch不对,要切换到hanabi

Q: 在使用过程中,编译会报错:Assertion failed: (dylib != NULL), function classicOrdinalForProxy, file LinkEditClassic.hpp

A: 查询得知Xcode 14以上引入的一项优化功能,该功能是在链接时进行优化来减少msgSend调用的开销的,默认是开启状态。通过www.wwdcnotes.com/notes/wwdc2… 我们可以看到:

(ObjC) Message send
with new compilers and linker in Xcode 14,message send calls are up to 8 bytes smaller, down from 12 bytes, on ARM64
binaries up to 2% smaller overall
enabled by Xcode 14, even when targeting an older OS release as deployment target
Defaults to balanced performance and size optimization
Opt into optimizing for size only using -Wl,-objc_stubs_small linker flag

我们主要是发现开启这个功能后加固后会在link阶段产生报错,所以建议设置-fno-objc-msgsend-selector-stubs关闭这项新的优化。

总结

该文章干货含量比较少,主要是记录加固过程中遇到的一系列问题。一整套流程走下来,对于LLVM以及对应的pass算是有了初步理解。目前实现了基于开源LLVM的OLLVM、Hikari的适配,实现了基于Swift源码的Hikari适配以及基于注入的Hanabi实现,根据不同的项目背景可自由选择相应混淆方式。后续会针对具体的混淆PASS进行分析,分享一下混淆PASS具体是怎样起作用的。

猜你喜欢

转载自juejin.im/post/7261889424450633788