iOS LLVM 混淆插件:Hanabi 和 Hikari

 原创    2023-12-12

近期我将项目中使用的 Xcode LLVM 混淆方案由 obfuscator-llvm 更换为开源的 Hanabi & Hikari 插件,在 Xcode 15 上适配 Clang,编译 Objective-C 项目混淆效果达到预期,但对 Swift 项目支持的还有问题。

从 obfuscator-llvm 到 Hanabi

一直以来,我在项目中使用的是基于:

https://github.com/obfuscator-llvm/obfuscator

的混淆方案,但这个项目已经不再维护了,最新仅支持到 llvm-4.0,特别是在适配新版本 Xcode 工具链时,存在各种各样的问题。因为工作项目执行混淆的必要性以及对 Xcode 新特性不敏感,很长一段时间我都是在比较低的 Xcode 版本使用 自定义的 Toolchain 开发。每次升级 Xcode 时都需要对混淆工具做适配,总会有各种各样的问题。现在,是时候改变这种状态了,急迫需要一种更优雅的混淆方案。

在 Github 上搜索了之后发现了更合适的工具:

https://github.com/HikariObfuscator/Hanabi

因为 Apple Clang 是不开源的,想通过开源的 LLVM 项目中构建完整的 toolchain 多多少少会和 Xcode 自带的工具链存在兼容问题。而 Hanabi 方案则另辟蹊径,通过 HOOK 的方式仅干预到 LLVM IR 混淆过程,其他编译过程还是依赖 Xcode 原生的 toolchain。

这个方案包含两部分内容:

Hikari Obfuscator Pass

obfuscator-llvm 一样,使用的基于 Hikari 的 Obfuscator Pass 文件放置在 llvm/lib/Transforms/Obfuscation 目录。

Hanabi

作为 llvm 的 subproject ,放置在 llvm/projects 目录。实测中使用了 HikariObfuscator/Hanabi 的一个 fork 版本:

https://github.com/61bcdefg/Hanabi

依赖

关闭 macOS SIP(System Integrity Protection)

后续需要使用 optool 工具修改 Xcode 文件,直接修改会报文件权限错误,需要先关闭 macOS 的 SIP 特性。

  • 重启 macOS,同时按住 Command + R 键,等待进入恢复模式;
  • 打开终端工具,执行命令 csrutil disable
  • 后续随时可以在恢复模式中开启 SIP csrutil enable

optool

optool is a tool which interfaces with MachO binaries in order to insert/remove load commands, strip code signatures, resign, and remove aslr. Below is its help.

https://github.com/alexzielenski/optool

这是一个 macOS 项目,直接运行,修改最低支持 macOS 版本,编译获得 optool 二进制文件。

Hanabi 源码分析

Hanabi 方案的基本思路是,将 Hikari Pass 打包成动态库,并修改 Xcode toolchain 的 clang 和 swift-frontend Mach-O 文件,使其启动加载 Hanabi 动态库文件。源码如下:

// Hanabi/Loader.cpp

// For open-source license, please refer to
// [License](https://github.com/HikariObfuscator/Hikari/wiki/License).
#include "dobby.h"
#include <llvm/Config/abi-breaking.h>
#include <llvm/IR/LegacyPassManager.h>
#include <llvm/IR/PassManager.h>
#include <llvm/Transforms/Obfuscation/Obfuscation.h>
#include <mach-o/dyld.h>
#include <string>
#include <sys/sysctl.h>

#if LLVM_ENABLE_ABI_BREAKING_CHECKS
#error "Configure LLVM with -DLLVM_ABI_BREAKING_CHECKS=FORCE_OFF"
#endif

using namespace llvm;

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, NULL, 0) != -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);
}

Inj3c73d

static __attribute__((__constructor__)) void Inj3c73d(int argc, char *argv[])

使用 __constructor__,当该动态库被加载时,函数将自动执行。

dobby

原始 Hanabi 项目采用的是 libsubstitute.dylib,这里采用的是 dobby,都是 HOOK 框架。 DobbySymbolResolver 找到指定库中导出方法的函数地址;DobbyHook 完成方法替换。

ObfuscationPass

LLVM 混淆 Pass 提供的接口,包括给旧版本 Legacy Pass Manager 使用的 createObfuscationLegacyPass() 和 New Pass Manager 使用的 ObfuscationPass()

在合适的入口添加混淆 Pass

源码中通过调用三次 DobbyHook 分别在三个关键点添加混淆 Pass。

  • llvm::PassManagerBuilder::populateModulePassManager

Legacy Pass Manager 的 HOOK 点。通过 MPM.add(createObfuscationLegacyPass()) 注册混淆 pass 。

  • llvm::PassBuilder::buildO0DefaultPipeline

New Pass Manager 的 HOOK 点,针对 O0 优化级别的默认 pass pipeline。O0 优化级别表示没有优化,在这个阶段注册混淆 pass 可以确保即使在没有启用其他优化的情况下,代码也能够被混淆处理。

  • llvm::PassBuilder::buildPerModuleDefaultPipeline

New Pass Manager 的 HOOK 点,构建为每个 module(通常指单个源文件,每个源文件对应一个IR文件)启用的 pass pipeline。在这个阶段应用混淆 pass 可以确保每个模块在编译时都会被混淆处理。

这里的 buildO0DefaultPipeline 处添加注入似乎没什么必要,因为正常情况下发布二进制构建都不会设置 -O0 编译级别,-O0 仅会用于 Debug 调试模式,而此时我们肯定是更倾向于更快的编译速度,启用混淆用处不大,应该可以去除 buildO0DefaultPipeline 的 HOOK 处理。

Loader.cpp 中包含了全部核心代码,下面看下 CMakeLists.txt 文件:

cmake_minimum_required(VERSION "3.13.0")
add_library(LLVMHanabiDeps SHARED #This is for linking a minimum subset of LLVM needed which serves as the escape plan
  ${CMAKE_CURRENT_LIST_DIR}/Dummy.cpp
)
add_dependencies(LLVMHanabiDeps
  LLVMCore
  LLVMSupport
  LLVMPasses
)

set(CMAKE_INSTALL_RPATH "@loader_path/")
set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)

add_library(LLVMHanabi SHARED
  ${CMAKE_CURRENT_LIST_DIR}/Loader.cpp
)
add_dependencies(LLVMHanabi
  LLVMHanabiDeps
  LLVMObfuscation
)
target_link_libraries(LLVMHanabiDeps PRIVATE LLVMCore LLVMSupport LLVMPasses ${CMAKE_CURRENT_LIST_DIR}/libdobby.a)
target_link_options(LLVMHanabiDeps PRIVATE -all_load -lobjc)

target_link_libraries(LLVMHanabi PRIVATE LLVMObfuscation LLVMHanabiDeps)
target_link_options(LLVMHanabi PRIVATE -flat_namespace)
target_compile_options(LLVMHanabi PRIVATE -Wno-dollar-in-identifier-extension PRIVATE -Wno-variadic-macros PRIVATE)

定义了两个动态库,分别是:

  • libLLVMHanabiDeps.dylib

包含 libdobby.a,仅提供 HOOK 能力。

  • libLLVMHanabi.dylib

依赖 LLVMObfuscation、LLVMHanabiDeps,包含 Loader.cpp,提供混淆 pass 的加载和注入。

对 Xcode clang / swift-frontend 的修改

在产出 libLLVMHanabi.dylib 相关的动态库之后,如何确保 clang / swift-frontend 在运行时加载 libLLVMHanabi.dylib?这就要借助 optool 工具了。作用就是在 Mach-O 文件中新增 LC_LOAD_DYLIB 类型的 Load Commands,内容为 libLLVMHanabi.dylib 的相对路径:@executable_path/libLLVMHanabi.dylib。clang 启动时会自动加载所有标记为 LC_LOAD_DYLIB 的动态库。

sudo $OTOOL_PATH install -c load -p @executable_path/libLLVMHanabi.dylib -t ${XCODE_PATH}/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang
sudo $OTOOL_PATH install -c load -p @executable_path/libLLVMHanabi.dylib -t ${XCODE_PATH}/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend

重签名:

sudo codesign -fs - ${XCODE_PATH}/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang
sudo codesign -fs - ${XCODE_PATH}/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend

混淆参数

HikariObfuscator wiki

bogus control flow

-mllvm -enable-bcfobf -mllvm -bcf_prob=90 -mllvm -bcf_loop=1

伪控制流,可设置混淆概率及混淆次数,BogusControlFlow

control flow flattening

-mllvm -enable-cffobf

控制流平坦化。

instructions substitution

-mllvm -enable-subobf -mllvm -sub_loop=1

指令替换,将基本的加、减、位运算,替换为更复杂的指令。

AntiClassDump

-mllvm -enable-acdobf

反class-dump,默认会造成 Objective-C load 方法不执行,解决办法参考:AntiClassDump

Basic Block Spliting

-mllvm -enable-splitobf

基本块分割,开启后编译异常缓慢,包体积膨胀很大。

Register-Based Indirect Branching

-mllvm -enable-indibran

基于寄存器的相对跳转,启用后通过 IDA/Hopper 难以获得伪代码,但包体积膨胀很大。折中考虑,可以针对工程中的部分关键函数使用 __attribute((__annotate__(("indibr")))) 声明以启用该特性。IndirectBranching

String Encryption

-mllvm -enable-strcry

对 Objective-C 字符串加密,StringEncryption

Function Wrapper

-mllvm -enable-funcwra -mllvm -fw_prob=50 -mllvm -fw_times=1

将函数包装为多级调用,对的调用foo(1)被转换为DummyA(1)->DummyB(1)->foo(1),FunctionWrapper

FunctionCallObfuscate

-mllvm -enable-fco

使用 JSON 配置来解析符号,FunctionCallObfuscate

allobf

-mllvm -enable-allobf

启用上述所有特性。

除了使用 LLVM 的编译参数,还可以针对每一个函数自定义混淆参数,Functions-Annotations

Xcode 中添加混淆参数

在 Xcode 中的 Other C Flags 中新增混淆参数:每一个混淆参数分两行,第一行固定为 -mllvm,第二行为具体的参数,比如 -enable-allobf

其它

如何确认混淆是否生效?

使用 IDA 等反编译工具,查看混淆后的二进制文件是否生效。

不支持 bitcode

这是主流 LLVM 混淆工具的通病,不过在 Xcode 15 上已经废弃了 bitcode,目前没有影响。

编译 Swift 时报错

Command SwiftGeneratePch failed with a nonzero exit code

swift-frontend存在兼容问题,目前没有好的解决方案,clang编译目前还没发现问题。

文章最后修改于 2024-04-25

相关文章:

LLDB命令速查手册
xcodebuild build failed:Use the $(inherited) flag
iOS 64/32位系统在处理BOOL值时的区别
iOS安全:Tweak开发环境及入门
iOS 抓取网络数据包

发表留言

您的电子邮箱地址不会被公开,必填项已用*标注。发布的留言可能不会立即公开展示,请耐心等待审核通过。