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 位目标后端等问题)。


🚀 核心优化与避坑指南

在动手之前,先总结在实际中碰上的关键痛点:

  1. 适配 LLVM 19 接口变更:LLVM 19 对指令插入点 API 进行了调整,直接编译旧版 OLLVM 插件会报错。本文提供了核心补丁说明。
  2. clang++为什么要构建,因为google签名的和obfuscator.dylib在mac上会认证不通过,出现TEAM ID的问题。所以需要自己签clang或者自己构建一个。不想操作ndk的,索性就自己build了。

🛠️ 第一步:环境配置与源码准备

1. 定位 NDK 对应的 LLVM Commit

为了使我们自己构建的编译器与官方 NDK 完美兼容,必须拉取与 NDK 28 版本完全一致的 LLVM 提交(Commit)。

运行 NDK 内部的 clang++ 查看版本信息:

1
2
export ANDROID_NDK_HOME="$HOME/Library/Android/sdk/ndk/28.2.13676358"
$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang++ --version

输出中会显示类似于 97a699bf4812a18fb657c2779f5296a4ab2694d2 的哈希值。

2. 精准克隆 AOSP LLVM 源码

由于 LLVM 仓库体积庞大,我们采用浅克隆(Shallow Clone)延迟检出(Sparse Checkout)策略,仅拉取我们需要的那个 Commit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mkdir -p ~/llvm-ndk28
cd ~/llvm-ndk28

# 克隆元数据但不检出文件
git clone \
--filter=blob:none \
--no-checkout \
https://android.googlesource.com/toolchain/llvm-project.git \
llvm-project
cd llvm-project

# 获取并检出指定 Commit
git fetch --depth=1 origin 97a699bf4812a18fb657c2779f5296a4ab2694d2
git checkout FETCH_HEAD

3. 克隆混淆插件源码

这里以 Hikari/Obfuscator 项目为例:

1
2
cd ~/llvm-ndk28
git clone https://github.com/lich4/llvm-pass-hikari.git ollvm-pass

4.其它问题

根据我同事反馈,版本不一致也没有问题,各编各的。要混淆的部分用21,其它的用自带的19也行。


🛠️ 第二步:修补 LLVM 19 适配性问题

LLVM 19 中废弃了部分隐式转换。直接编译 ollvm-pass 插件会遇到迭代器类型不匹配的编译错误。

需要编辑 ~/llvm-ndk28/ollvm-pass/obfuscator/Substitution.cpp 文件,进行如下修改:

1
2
3
// 找到 BinaryOperator::CreateNeg 的调用位置
- op = BinaryOperator::CreateNeg(bo->getOperand(1), "", bo);
+ op = BinaryOperator::CreateNeg(bo->getOperand(1), "", bo->getIterator());

原理:LLVM 19 中,BinaryOperator::CreateNeg 的第三个参数表示指令插入位置,它现在要求传入 BasicBlock::iterator,而不能再直接传入 Instruction* bo 指针。这篇文章是先总结的,但我后来发现调用的地方挺多,直接在.h上加了个函数,不然一处一处修改太费劲了。


🛠️ 第三步:构建 Clang 与 LLVM 核心

我们在一处 CMake 目录中同时配置并编译 Clang 编译器与开发环境,避免重复工作。

[!WARNING]
警告:为了同时兼容 arm64-v8aarmeabi-v7a-DLLVM_TARGETS_TO_BUILD 参数中必须包含 ARM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
cd ~/llvm-ndk28

# 导出工具链环境变量 (此处使用 Android SDK 附带的 CMake 与 Ninja 即可)
export NDK="$HOME/Library/Android/sdk/ndk/28.2.13676358"
export CMAKE="$HOME/Library/Android/sdk/cmake/4.0.2/bin/cmake"
export NINJA="$HOME/Library/Android/sdk/cmake/4.0.2/bin/ninja"

# 配置 CMake 选项
"$CMAKE" \
-S llvm-project/llvm \
-B clang-build \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_OSX_ARCHITECTURES=arm64 \
-DLLVM_TARGETS_TO_BUILD="AArch64;ARM;X86" \
-DLLVM_ENABLE_PROJECTS="clang;lld" \
-DLLVM_BUILD_TOOLS=ON \
-DLLVM_BUILD_EXAMPLES=OFF \
-DLLVM_INCLUDE_TESTS=OFF \
-DLLVM_INCLUDE_BENCHMARKS=OFF \
-DLLVM_INCLUDE_DOCS=OFF

# 编译 Clang 与 LLD 链接器
"$NINJA" -C clang-build -j8

# 将全部编译产物(包含编译器和开发依赖的 LLVMConfig.cmake)安装至统一目录
mkdir -p ~/llvm-ndk28/llvm-install
"$CMAKE" \
--install clang-build \
--prefix ~/llvm-ndk28/llvm-install

安装完成后,在 ~/llvm-ndk28/llvm-install/bin/clang++ 可找到我们的自定义编译器;在 ~/llvm-ndk28/llvm-install/lib/cmake/llvm/LLVMConfig.cmake 获得了编译插件所依赖的导出配置文件。


🛠️ 第四步:构建 OLLVM 混淆插件

现在,我们可以利用刚刚编译出的 LLVM 配置,直接编译出对应的混淆动态库:

1
2
3
4
5
6
7
8
9
10
11
cd ~/llvm-ndk28/ollvm-pass

"$CMAKE" \
-S obfuscator \
-B obfuscator-build \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DLLVM_DIR="$HOME/llvm-ndk28/llvm-install/lib/cmake/llvm"

# 编译混淆 Pass 插件
"$NINJA" -C obfuscator-build -j8

编译成功后,即可在 obfuscator-build 目录下收获最终的混淆插件:**Obfuscator.dylib**。


⚡ 第五步:本地交叉编译测试

测试我们编译出来的工具链是否能够正确地对 Android 目标代码执行混淆:

  1. 准备测试环境变量

    1
    2
    3
    4
    export 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"
  2. 编写简单 C 文件并测试编译

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    cd ~/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

  1. gradle

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    android {
    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"
    }
    }
    }
    }
  2. hikari-clang++
    hikari-clang也要做个wrapper,就是写个shell包一下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #!/bin/bash

    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 \
    "$@"
  3. hikari-toolchain.cmake,也可放入项目目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    set(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++
    )
  4. 填坑:
    要在policy.json中,顶级节点配置src_root,./gradlew 命令构建不混淆,光这个调试了我3个小时,能编译能构建,就是混淆不够,最后多种尝试,找到了src_root配置。
    enable-indibran改为了false,否则构建失败,也不折腾了,放弃一项吧。


总结

亲妈都不认识了,自己读肯定是没办法读了,但是AI配合ghidra的mcp,还是能梳理,尽管不能还原,但逻辑上还能猜到,AI还是强。