别让 so 裸奔!移植 OLLVM 到 NDK 并集成到 Android Studio

版权归作者所有,如有转发,请注明文章出处:cyrus-studio.github.io/blog/

前言

在 Android 应用安全中,Native 层 so 库往往是最容易被逆向分析的目标 。无论是游戏的核心逻辑,还是 App 的关键算法,一旦 so 被反编译,核心代码就可能暴露无遗。

传统的 Java 层混淆工具(如 ProGuard、R8)对 C/C++ 代码无能为力,因此 NDK 层代码的保护 成了安全加固中的难点。解决思路:在编译阶段对 so 进行混淆处理 ,让逆向难度大幅提升。

LLVM 生态中有一个安全扩展 ------ OLLVM (Obfuscator-LLVM) ,它在编译流程里插入了混淆 Pass,能对 C/C++ 代码做 控制流平坦化、虚假控制流、指令替换 等处理,从而显著增加逆向门槛。

本文将带你实战:如何将 OLLVM 移植到 LLVM/NDK,并在 Android Studio 工程中使用它,为 Native 代码加上一层混淆保护。

OLLVM、LLVM 与 Android NDK

LLVM 是一个高度模块化的编译器框架,它能够将 C/C++ 等高级语言源码编译为中间表示(LLVM IR),再经过优化、生成目标机器码。它不仅仅是一个编译器,更是一个"编译基础设施"。

在 Android 平台上,自 NDK r18 开始,Google 就全面弃用了 GCC,转而采用 LLVM/Clang 作为官方工具链。也就是说,所有的 C/C++ 代码编译、优化、生成 so 库的过程,底层都是由 LLVM 驱动完成的。

OLLVM (Obfuscator-LLVM) 则是在 LLVM 基础上扩展的一个安全项目。它在 LLVM 编译流程中增加了混淆 Pass,可以对 C/C++ 代码进行 控制流平坦化、虚假控制流、指令替换 等混淆处理,从而有效提高逆向分析和反编译的难度,保护 Android 应用中 Native so 层的核心逻辑不被轻易破解。

最终实现编译流程大概如下:

java 复制代码
          ┌──────────────────┐
          │   C / C++ 源码    │
          └────────┬─────────┘
                   │
                   ▼
          ┌──────────────────┐
          │   LLVM (Clang)   │  ← Android NDK 内置的官方编译器工具链
          │   前端:生成 IR   │
          └────────┬─────────┘
                   │ LLVM IR
                   ▼
          ┌──────────────────┐
          │    OLLVM Pass    │  ← 基于 LLVM 的扩展:混淆(控制流平坦化、指令替换等)
          │   (插入在中间) │
          └────────┬─────────┘
                   │ 混淆后的 IR
                   ▼
          ┌──────────────────┐
          │   LLVM 后端优化   │
          │   + 代码生成      │
          └────────┬─────────┘
                   │ 汇编
                   ▼
          ┌──────────────────┐
          │   链接生成 so     │ ← 最终供 Android 应用调用的 native 库
          └──────────────────┘

编译 LLVM

1. 下载源码

NDK 中 LLVM 所在路径:<android-ndk>/toolchains/llvm/prebuilt/<host-system>/bin/

查看 clang 版本,这里版本是 18.0.2

less 复制代码
(base) PS D:\App\android\sdk\ndk\27.1.12297006\toolchains\llvm\prebuilt\windows-x86_64\bin> ./clang --version

Android (12285214, based on r522817b) clang version 18.0.2 (https://android.googlesource.com/toolchain/llvm-project d8003a456d14a3deb8054cdaa529ffbf02d9b262)
Target: x86_64-w64-windows-gnu
Thread model: posix
InstalledDir: D:/App/android/sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/windows-x86_64/bin

根据 NDK 中 clang 的版本,下载和编译版本相近的 LLVM。

关于 LLVM 源码下载和编译参考:LLVM 全面解析:NDK 为什么离不开它?如何亲手编译调试 clang

2. 构建环境设置

创建并进入构建目录

bash 复制代码
mkdir build && cd build

配置编译目标

ini 复制代码
cmake -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="/utf-8" -DLLVM_ENABLE_RTTI=ON -DLLVM_ENABLE_EH=ON -DLLVM_ENABLE_PROJECTS="clang;lld" ../llvm

3. 编译

编译目标设置完成后,执行 ninja 开始编译。

ini 复制代码
D:\Projects\llvm-project\build>ninja
[1651/2426] Building CXX object tools\lld\ELF\CMakeFiles\lldELF.dir\Arch\LoongArch.cpp.obj
D:\Projects\llvm-project\lld\ELF\Arch\LoongArch.cpp(705): warning C4334: "<<": 32 位移位的结果被隐式转换为 64 位(是否希望进行 64 位移位?)
[2426/2426] Linking CXX executaset PATH=%PATH%;D:\Projects\llvm-project\build\bin

移植 OLLVM 到 Android NDK

这是 Android NDK 中 toolchains\llvm\prebuilt\windows-x86_64 目录下的文件夹结构

其中主要几个文件夹:

  • bin:包含可执行文件,例如编译器(clang、clang++)、链接器(ld)等,主要用于 NDK 工具链的操作。

  • include:包含头文件,提供编译时所需的接口定义。例如,标准 C/C++ 库的头文件以及与 Android 平台相关的头文件。

  • lib:包含静态库和动态库,提供编译和链接时使用的库文件。例如,支持标准 C/C++ 函数的实现库。

这些文件共同组成了 Android NDK 的工具链,用于开发和调试 Android native 代码。

当我们成功把 OLLVM 移植到 LLVM,并编译完成后可以在构建目录下看到同样也有相关目录

关于 OLLVM 移植到 LLVM 过程参考:

复制并替换 bin、include、lib 目录到 ndk 中

Android Studio 中使用 OLLVM

1. 创建 native 工程

2. 配置 OLLVM NDK

编辑 local.properties 添加 ndk.dir 配置为 ollvm ndk 路径

ini 复制代码
ndk.dir=D\:\\App\\android\\sdk\\ndk\\27.1.12297006

3. 代码实现

创建 OLLVMActivity,定义并调用 native 方法

kotlin 复制代码
/**
 * 移植 OLLVM 到 Android NDK
 */
class OLLVMActivity : AppCompatActivity() {

    // 声明 native 方法
    external fun sub(a: Int, b: Int): Int
    external fun bcf(input: String?): String?
    external fun fla(x: Int, y: Int): String?

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ollvmactivity)
        // 加载本地库
        System.loadLibrary("ollvm-lib");

        // 调用 native 方法并显示结果
        val textView = findViewById<TextView>(R.id.textView)

        val subResult = sub(10, 5)
        val bcfResult = bcf("Hello OLLVM!")
        val flaResult = fla(3, 2)

        val resultText = """
            sub(10, 5) = $subResult
            bcf("Hello OLLVM!") = $bcfResult
            fla(x, y) = $flaResult
            """.trimIndent()

        textView.text = resultText
    }

}

创建 ollvm-lib.cpp 实现 native 方法

c 复制代码
#include <jni.h>
#include <string>

// sub 方法:两个整数相减
extern "C" JNIEXPORT jint JNICALL
Java_com_cyrus_example_ollvm_OLLVMActivity_sub(JNIEnv* env, jobject, jint a, jint b) {
    return a - b;
}

// bcf 方法:接收字符串并返回拼接后的字符串
extern "C" JNIEXPORT jstring JNICALL
Java_com_cyrus_example_ollvm_OLLVMActivity_bcf(JNIEnv* env, jobject, jstring input) {
    const char* inputStr = env->GetStringUTFChars(input, nullptr);
    std::string result = std::string("BCF: ") + inputStr;
    env->ReleaseStringUTFChars(input, inputStr);
    return env->NewStringUTF(result.c_str());
}

// fla 方法:两个int相加判断大小并返回结果字符串
extern "C" JNIEXPORT jstring JNICALL
Java_com_cyrus_example_ollvm_OLLVMActivity_fla(JNIEnv *env, jobject , jint x, jint y) {
    int sum = x + y;

    // 使用字符串流拼接结果
    std::ostringstream result;

    if (sum < 5) {
        result << "x = " << x << ", y = " << y << ", x + y " << "小于 5";
    } else if(sum == 5){
        result << "x = " << x << ", y = " << y << ", x + y " << "等于 5";
    } else{
        result << "x = " << x << ", y = " << y << ", x + y " << "大于 5";
    }

    // 返回拼接好的字符串
    return env->NewStringUTF(result.str().c_str());
}

编辑 CMakeLists.txt,添加动态库 ollvm-lib

bash 复制代码
add_library( # 设置库的名称
        ollvm-lib

        # 设置库的类型
        SHARED

        # 设置源文件路径
        ollvm-lib.cpp)

4. 全局混淆

编辑 CMakeLists.txt,添加如下配置启用 OLLVM 混淆

bash 复制代码
# 全局启用指令替换
add_definitions("-mllvm -sub")

通过 -mllvm 选项开启 OLLVM 的代码混淆功能:

  • -mllvm -bcf:启用基本块控制流混淆。

  • -mllvm -fla:启用控制流平坦化。

  • -mllvm -sub:启用指令替换。

5. 动态库混淆

编辑 CMakeLists.txt,只为 ollvm-lib 动态库启用虚假控制流

vbnet 复制代码
# 为 ollvm-lib 动态库启用虚假控制流
target_compile_options(
        ollvm-lib
        PRIVATE
        -mllvm -bcf)

如果有多个编译项

bash 复制代码
target_compile_options(
    ollvm-lib
    PRIVATE
    -mllvm -bcf  # 启用 Bogus Control Flow 混淆
    -mllvm -sub  # 启用 Substitution 混淆
    -mllvm -fla  # 启用 Flattening 混淆
)

6. 函数混淆

通过注解为 fla 方法禁用虚假控制流和启用控制流平坦化

c 复制代码
extern "C" JNIEXPORT jstring JNICALL
__attribute__((annotate("nobcf,fla"))) Java_com_cyrus_example_ollvm_OLLVMActivity_fla(JNIEnv *env, jobject, jint x, jint y) {
    int sum = x + y;

    // 使用字符串流拼接结果
    std::ostringstream result;

    if (sum < 5) {
        result << "x = " << x << ", y = " << y << ", x + y " << "小于 5";
    } else if(sum == 5){
        result << "x = " << x << ", y = " << y << ", x + y " << "等于 5";
    } else{
        result << "x = " << x << ", y = " << y << ", x + y " << "大于 5";
    }

    // 返回拼接好的字符串
    return env->NewStringUTF(result.str().c_str());
}

测试与验证

编译运行正常

把 apk 中的 so 文件解压出来

使用 IDA 打开 libollvm-lib.so,可以看到 sub 函数反汇编视图如下(启用虚假控制流+指令替换)

bcf 函数反汇编视图(启用虚假控制流+指令替换)

fla 函数反汇编视图(禁用虚假控制流并启用控制流平坦化)

其他动态库中函数(未启用 OLLVM 混淆)

完整源码

相关推荐
尚久龙7 小时前
安卓学习 之 图片控件和图片按钮
android·java·学习·手机·android studio·安卓
东风西巷7 小时前
Don‘t Sleep:保持电脑唤醒,确保任务不间断
android·电脑·软件需求
tangweiguo030519877 小时前
FlutterActivity vs FlutterFragmentActivity:全面对比与最佳实践
android·flutter
葱段8 小时前
【Flutter】TextField 监听长按菜单粘贴点击事件
android·flutter·dart
CYRUS_STUDIO8 小时前
OLLVM 移植 LLVM18 踩坑:一步步调试修复控制流平坦化
c语言·c++·llvm
用户098 小时前
Gradle 现代化任务依赖方案
android·kotlin
东坡肘子9 小时前
从开放平台到受控生态:谷歌宣布 Android 开发者验证政策 | 肘子的 Swift 周报 #0101
android·swiftui·swift
脚踏实地,坚持不懈!9 小时前
ANDROID,Jetpack Compose, 贪吃蛇小游戏Demo
android
Just_Paranoid9 小时前
【JobScheduler】Android 后台任务调度的核心组件指南
android·alarmmanager·jobscheduler·workmanager