从 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
混淆参数
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
编译目前还没发现问题。
留言板