如何防止 so 文件被轻松逆向?精准控制符号导出 + JNI 动态注册

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

前言

在使用 Android NDK 编译 so 文件 时,默认情况下,所有 public C/C++ 函数都会被导出 。这意味着无论函数是否真正需要对外使用,它们的符号表都会出现在 so 文件中。只要把 so 丢进 IDA、GHIDRA 等逆向工具 ,攻击者就能轻松看到完整的函数名列表,进而快速定位核心逻辑。

实际上,除了必须导出的 JNI 函数 外,其余大多数 C/C++ 函数根本不需要对外暴露。即便不导出,这些函数在 编译器/链接器内部依然可以正常调用 ,运行时也不会受到影响。换句话说,大量符号的默认导出既没有必要,还无形中增加了被逆向的风险。

必须导出的 JNI 函数:

函数名 是否必须导出 说明
JNI_OnLoad ✅ 是(总是) 系统通过 dlsym() 查找,初始化用
Java_... ✅ 是(如果用静态注册) Java 层方法通过名称匹配
JNI_OnUnload ❌ 否(可选) 卸载时调用,不导出也不会出错
JNI_OnLoad_LibName(非标准) ❌ 否(特殊系统扩展) Android 未使用
JNI_GetCreatedJavaVMs、JNI_CreateJavaVM ❌ 否 仅在 native 启动 JVM 时使用(一般用不到)

因此,为了提升安全性,我们需要通过精细化控制导出符号,只保留最小必要的导出集 。这就是 linker version script 发挥作用的地方:它能让我们像"白名单"一样,只暴露需要的 JNI 接口,隐藏其他实现函数,从而显著提升逆向门槛。

使用 linker version script 精细控制导出

linker version script 是 GNU 链接器(ld)提供的一种机制,用来控制 .so 或 .a 文件中哪些符号可以导出、哪些必须隐藏。

一个简单示例:

创建 hide.map 文件(仅导出所有 JNI_ 和 Java_ 开头的 JNI 方法)

ini 复制代码
{
    global:
        JNI_*;
        Java_*;

    local:
        *;
};

含义:

  • global: 表示这些符号会被导出,可供外部(如 ART)通过 dlsym() 使用。

  • Java_* 会匹配所有以 Java_ 开头的方法 ------ 即静态注册 JNI 方法。

  • local: *; 表示其余全部符号(如内部 C 函数、C++ mangled 符号、加密算法、字符串处理等)一律隐藏,无法通过 IDA 等工具直接查看函数名。

编译时在 CMakeLists.txt 中加上:

bash 复制代码
# 抹除符号
set_target_properties(native-lib PROPERTIES LINK_FLAGS "-Wl,--version-script=${CMAKE_SOURCE_DIR}/hide.map")

这样生成的 so 文件中,只有指定的 JNI 函数会对外可见,其他 C/C++ 函数即使是 public 也不会暴露。

参考:android-docs.cn/ndk/guides/...

测试

重新编译运行,使用 IDA Pro 打开 so ,可以看到只导出了 JNI 相关函数

只控制导出符号,不影响内部调用,程序运行时不会出错。

除了增加逆向难度,同时还能减少 so 文件的体积

动态注册 JNI 方法:进一步隐藏调用入口

即使我们通过 linker version script 精准控制了导出符号,只保留必要的 JNI 接口,逆向人员依然可以在 so 符号表中看到这些 JNI 函数的完整名字,例如:

复制代码
Java_com_example_native_NativeUtils_secretMethod

这种函数名一眼就暴露了 Java 层的类名、方法名,逆向者很容易定位和跟踪调用关系。

Android 的 ART 虚拟机会用 dlsym() 查找你导出的 JNI 方法,所以这些你不能隐藏,否则会导致运行时崩溃。

为了解决这一问题,可以使用 JNI 动态注册 。动态注册的思路是:

  • 在 C/C++ 层不再定义形如 Java_xxx 的函数名;而是通过一个普通的本地函数实现逻辑;

  • 再在 JNI_OnLoad 中调用 RegisterNatives,把 Java 方法与对应的 native 函数指针绑定。

例如:

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

// 定义方法签名
static JNINativeMethod methods[] = {
    {"secretMethod", "()V", (void *)secretMethod},
};

// JNI_OnLoad 动态注册方法
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env = nullptr;
    vm->GetEnv((void**)&env, JNI_VERSION_1_6);

    jclass clazz = env->FindClass("com/example/native/NativeUtils");
    env->RegisterNatives(clazz, methods, sizeof(methods)/sizeof(methods[0]));

    return JNI_VERSION_1_6;
}

函数名字可以自定义:

javascript 复制代码
void secretMethod(JNIEnv *env, jobject obj) {
    // your native code
}

这样生成的 so 文件里,已经看不到带有类名/方法名信息的 JNI 符号,逆向者无法直接通过函数名反推出调用入口。即使在 IDA、GHIDRA 中分析 so,也只能看到一些无意义的 C/C++ 函数符号,而找不到明确的 Java 关联。

完整源码

开源地址:github.com/CYRUS-STUDI...

相关文章:

相关推荐
和光同尘 、Y_____7 小时前
BRepMesh_IncrementalMesh 重构生效问题
c++·算法·图形渲染
louisgeek8 小时前
Java 线程池取消的方式
android
猫耳君8 小时前
汽车网络安全 CyberSecurity ISO/SAE 21434 测试之一
python·安全·网络安全·汽车·iso/sae 21434·cybersecurity
Billy_Zuo8 小时前
人工智能机器学习——模型评价及优化
android·人工智能·机器学习
Rverdoser8 小时前
如何打造自主安全的下一代域名系统
安全
起个名字费劲死了8 小时前
手眼标定之已知同名点对,求解转换RT,备份记录
c++·数码相机·机器人·几何学·手眼标定
雅雅姐8 小时前
C++中的单例模式的实现
c++
lingran__9 小时前
速通ACM省铜第一天 赋源码(The Cunning Seller (hard version))
c++·算法
tangweiguo030519879 小时前
Flutter与原生混合开发:实现完美的暗夜模式同步方案
android·flutter
沐怡旸9 小时前
【基础知识】仿函数与匿名函数对比
c++·面试