别让 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 混淆)

完整源码

相关推荐
友人.22714 小时前
Android 底部导航栏 (BottomNavigationView) 制作教程
android
努力学习的小廉15 小时前
初识MYSQL —— 事务
android·mysql·adb
阿里云云原生15 小时前
深度解析 Android 崩溃捕获原理及从崩溃到归因的闭环实践
android
.豆鲨包15 小时前
【Android】Android内存缓存LruCache与DiskLruCache的使用及实现原理
android·java·缓存
JulyYu16 小时前
【Android】针对非SDK接口的限制解决方案
android·客户端
猪哥帅过吴彦祖17 小时前
Flutter 系列教程:应用导航 - Navigator 1.0 与命名路由
android·flutter·ios
2501_9160088917 小时前
iOS 跨平台开发实战指南,从框架选择到开心上架(Appuploader)跨系统免 Mac 发布全流程解析
android·macos·ios·小程序·uni-app·iphone·webview
stevenzqzq18 小时前
Android Hilt教程_构造函数
android
鹏多多18 小时前
flutter图片选择库multi_image_picker_plus和image_picker的对比和使用解析
android·flutter·ios