Android NDK 28 自定义 Clang & OLLVM (New Pass Manager) 混淆编译实战指南
引言
在移动端安全对抗中,对 C++ 核心逻辑(如 Cocos 游戏引擎逻辑、加解密算法)进行混淆是防逆向分析的重要一环。LLVM 混淆器(OLLVM / Hikari / Pluto)是最常用的方案。
随着 Android NDK 28(搭载 LLVM 19)的发布,旧版的 Pass 管理器(Legacy Pass Manager)已被完全移除,取而代之的是 **New Pass Manager (NPM)**。这意味着以往传统的 -Xclang -load 注入方式已失效,我们必须为新版 NDK 构建自定义的 Clang 编译器并适配新版 Pass 插件。
本文将详细介绍如何从源码一步步构建支持 NDK 28 的自定义 Clang 编译器和 Obfuscator.dylib 混淆插件,并规避交叉编译中的各种“大坑”(如编译时间过长、缺失 32 位目标后端等问题)。
🚀 核心优化与避坑指南
在动手之前,先总结在实际中碰上的关键痛点:
- 适配 LLVM 19 接口变更:LLVM 19 对指令插入点 API 进行了调整,直接编译旧版 OLLVM 插件会报错。本文提供了核心补丁说明。
- clang++为什么要构建,因为google签名的和obfuscator.dylib在mac上会认证不通过,出现TEAM ID的问题。所以需要自己签clang或者自己构建一个。不想操作ndk的,索性就自己build了。
🛠️ 第一步:环境配置与源码准备
1. 定位 NDK 对应的 LLVM Commit
为了使我们自己构建的编译器与官方 NDK 完美兼容,必须拉取与 NDK 28 版本完全一致的 LLVM 提交(Commit)。
运行 NDK 内部的 clang++ 查看版本信息:
1 | export ANDROID_NDK_HOME="$HOME/Library/Android/sdk/ndk/28.2.13676358" |
输出中会显示类似于 97a699bf4812a18fb657c2779f5296a4ab2694d2 的哈希值。
2. 精准克隆 AOSP LLVM 源码
由于 LLVM 仓库体积庞大,我们采用浅克隆(Shallow Clone)与延迟检出(Sparse Checkout)策略,仅拉取我们需要的那个 Commit:
1 | mkdir -p ~/llvm-ndk28 |
3. 克隆混淆插件源码
这里以 Hikari/Obfuscator 项目为例:
1 | cd ~/llvm-ndk28 |
4.其它问题
根据我同事反馈,版本不一致也没有问题,各编各的。要混淆的部分用21,其它的用自带的19也行。
🛠️ 第二步:修补 LLVM 19 适配性问题
LLVM 19 中废弃了部分隐式转换。直接编译 ollvm-pass 插件会遇到迭代器类型不匹配的编译错误。
需要编辑 ~/llvm-ndk28/ollvm-pass/obfuscator/Substitution.cpp 文件,进行如下修改:
1 | // 找到 BinaryOperator::CreateNeg 的调用位置 |
原理:LLVM 19 中,
BinaryOperator::CreateNeg的第三个参数表示指令插入位置,它现在要求传入BasicBlock::iterator,而不能再直接传入Instruction* bo指针。这篇文章是先总结的,但我后来发现调用的地方挺多,直接在.h上加了个函数,不然一处一处修改太费劲了。
🛠️ 第三步:构建 Clang 与 LLVM 核心
我们在一处 CMake 目录中同时配置并编译 Clang 编译器与开发环境,避免重复工作。
[!WARNING]
警告:为了同时兼容arm64-v8a与armeabi-v7a,-DLLVM_TARGETS_TO_BUILD参数中必须包含ARM。
1 | cd ~/llvm-ndk28 |
安装完成后,在 ~/llvm-ndk28/llvm-install/bin/clang++ 可找到我们的自定义编译器;在 ~/llvm-ndk28/llvm-install/lib/cmake/llvm/LLVMConfig.cmake 获得了编译插件所依赖的导出配置文件。
🛠️ 第四步:构建 OLLVM 混淆插件
现在,我们可以利用刚刚编译出的 LLVM 配置,直接编译出对应的混淆动态库:
1 | cd ~/llvm-ndk28/ollvm-pass |
编译成功后,即可在 obfuscator-build 目录下收获最终的混淆插件:**Obfuscator.dylib**。
⚡ 第五步:本地交叉编译测试
测试我们编译出来的工具链是否能够正确地对 Android 目标代码执行混淆:
准备测试环境变量:
1
2
3
4export CLANG="$HOME/llvm-ndk28/llvm-install/bin/clang++"
# 注意:现代 NDK 的真实 sysroot 路径位于以下位置
export SYSROOT="$HOME/Library/Android/sdk/ndk/28.2.13676358/toolchains/llvm/prebuilt/darwin-x86_64/sysroot"
export PASS="$HOME/llvm-ndk28/ollvm-pass/obfuscator-build/Obfuscator.dylib"编写简单 C 文件并测试编译:
1
2
3
4
5
6
7
8
9
10
11cd ~/llvm-ndk28/ollvm-pass/obfuscator/test
# 使用新版 -fpass-plugin 载入插件,并开启 -O2 触发优化流水线
"$CLANG" \
--target=aarch64-linux-android24 \
--sysroot="$SYSROOT" \
-O2 \
-fpass-plugin="$PASS" \
-S -emit-llvm \
test.c \
-o /tmp/ollvm-test.ll如果终端输出了形如
>>>> run fla on ...等控制流平坦化混淆成功执行的日志,则表明您的自定义 NPM 混淆链已完全打通!
🎯 Android
gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16android {
defaultConfig {
externalNativeBuild {
cmake {
// 1. 强制指定你自己的 clang 和 clang++
arguments( "-DCMAKE_TOOLCHAIN_FILE=/Users/xxx/llvm-ndk28/hikari-toolchain.cmake",
"-DCMAKE_C_COMPILER=/Users/xxx/llvm-ndk28/bin/hikari-clang",
"-DCMAKE_CXX_COMPILER=/Users/xxx/llvm-ndk28/bin/hikari-clang++" )
// 2. 注入你的 pass 插件路径 这里其实和wrapper中的配置应该是重复了,我也不试了
cppFlags "-fpass-plugin=/Users/xxx/llvm-ndk28/ollvm-pass/hikari-build/Hikari.dylib"
cFlags "-fpass-plugin=/Users/xxx/llvm-ndk28/ollvm-pass/hikari-build/Hikari.dylib"
}
}
}
}hikari-clang++
hikari-clang也要做个wrapper,就是写个shell包一下1
2
3
4
5
6
7
8
9
10
11
12
13
14
set -e
ROOT=/Users/xxx/llvm-ndk28
HIKARI=$ROOT/ollvm-pass/hikari-build/Hikari.dylib
CLANG=$ROOT/clang-build/bin/clang++
ln -sf $ROOT/policy.json "$(pwd)/policy.json"
exec $CLANG \
-fpass-plugin=$HIKARI \
"$@"hikari-toolchain.cmake,也可放入项目目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21set(CMAKE_SYSTEM_NAME Android)
set(CMAKE_ANDROID_NDK
/Users/xxx/Library/Android/sdk/ndk/28.2.13676358
)
set(CMAKE_ANDROID_ARCH_ABI arm64-v8a)
set(CMAKE_ANDROID_API 26)
include(
${CMAKE_ANDROID_NDK}/build/cmake/android.toolchain.cmake
)
set(CMAKE_C_COMPILER
/Users/xxx/llvm-ndk28/bin/hikari-clang
)
set(CMAKE_CXX_COMPILER
/Users/xxx/llvm-ndk28/bin/hikari-clang++
)填坑:
要在policy.json中,顶级节点配置src_root,./gradlew 命令构建不混淆,光这个调试了我3个小时,能编译能构建,就是混淆不够,最后多种尝试,找到了src_root配置。
enable-indibran改为了false,否则构建失败,也不折腾了,放弃一项吧。
总结
亲妈都不认识了,自己读肯定是没办法读了,但是AI配合ghidra的mcp,还是能梳理,尽管不能还原,但逻辑上还能猜到,AI还是强。