Android NDK 编译 so 文件 抹除导出符号 反逆向

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

前言

NDK 编译 so 默认情况下,所有 public 函数 都会导出,通过 IDA、GHIDRA 可以看到函数名

word/media/image1.png

但除了必须导出的 JNI 函数外,其余 C/C++ 函数符号不导出其实也完全没影响,编译器/链接器内部可以调用,运行时照样可以正常执行。

必须导出的 JNI 函数

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

使用 linker version script 精细控制导出

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

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

{
    global:
        JNI_*;
        Java_*;

    local:
        *;
};

解释

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

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

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

在你的 CMakeLists.txt 中这样写:

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

参考:https://android-docs.cn/ndk/guides/symbol-visibility

测试

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

word/media/image2.png

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

word/media/image3.png

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

word/media/image4.png

动态注册 JNI 方法,JNI 方法反跟踪

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

如果你用 RegisterNatives 动态注册 JNI 方法,就可以完全避免导出 Java_com_xxx 的方法名:

#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;
}

函数名字可以自定义:

void secretMethod(JNIEnv *env, jobject obj) {
    // your native code
}

此时 .so 中不需要导出 Java_com_example_xxx 符号,IDA 也就看不到!

完整源码

开源地址:https://github.com/CYRUS-STUDIO/AndroidExample

相关文章:

<think>嗯,用户问的是如何使用加密后的.so文件。首先,我需要理解.so文件是什么。.so文件是Linux系统的共享库,类似于Windows的DLL。加密后的.so文件可能意味着这个库被某种方式加密了,用户需要在使用前解密或者通过某种机制来加载它。 接下来,我得考虑用户可能遇到的场景。用户可能是一个开发者,想要保护自己的代码不被编译,所以对.so文件进行了加密,现在想知道如何在运行时正确加载和使用它。或者用户可能从第三方获得了加密的.so,需要在自己的程序中使用,但不知道如何处理加密的问题。 然后,我需要分析可能的加密方法。常见的加密方式有两种:静态加密和动态加密。静态加密可能是在编译后对.so文件进行加密,然后在程序启动时解密到内存中再加载。动态加密可能是在运行时解密部分代码,或者使用特定的工具链在编译时集成加密措施。 接下来,使用步骤可能包括解密.so文件到内存,然后通过dlopen加载。不过,直接解密到磁盘可能会有安全风险,因为解密后的文件可能被提取。因此,正确的做法可能是在内存中解密,然后使用dlopen从内存加载。但Linux的dlopen通常需要文件路径,所以可能需要一些技巧,比如使用memfd_create来创建匿名文件描述符,然后将解密后的内容写入,再通过该描述符加载。 此外,还可能涉及加密工具的选择,比如使用openssl库进行AES加密解密,或者使用专门的代码保护工具如Virbox、OLLVM等。需要指导用户如何集成解密逻辑到他们的应用程序中,确保解密密钥的安全存储,比如放在服务器端,运行时动态获取,或者使用白盒加密技术。 还要考虑用户的技术水平,是否需要分步骤说明,比如如何解密文件,如何修改代码来加载解密后的库,如何处理可能的错误,比如权限问题、依赖项缺失等。同时,提醒用户注意法律问题,确保加密的.so文件是合法获得的,避免涉及破解或盗版问题。 可能还需要提到一些常见的陷阱,例如加密后的.so文件如果修改了原有的导出符号,可能导致动态链接失败;或者在解密过程中内存泄漏的风险。此外,跨平台的问题,如果用户是在其他系统上使用,方法是否适用,比如Android的.so文件加密是否有不同的处理方式。 最后,总结关键步骤:加密方法的选择、解密过程的实现、安全加载的方式、密钥管理以及测试和验证。确保用户能够按照步骤操作,同时保护好自己的知识产权和遵守相关法律法规。</think>使用加密后的 `.so`(共享库)文件通常是为了保护代码逻辑或敏感数据,防止被逆向分析。以下是实现这一目标的常见方法和步骤: --- ### **1. 加密 .so 文件的目的** - **保护知识产权**:防止他人直接编译或分析你的代码。 - **动态解密加载**:运行时解密并加载到内存,避免明文存储在磁盘中。 - **依赖安全环境**:需要特定密钥或环境才能解密,增加破解难度。 --- ### **2. 实现步骤** #### **(1) 加密 .so 文件** - **方法一:静态加密** 1. 使用加密工具(如 `openssl`、`AES`)对 `.so` 文件进行加密。 2. 示例命令: ```bash openssl enc -aes-256-cbc -in original.so -out encrypted.so -k your_secret_key ``` - **方法二:代码混淆工具** 使用专业工具(如 **Virbox Protector**、**OLLVM**)对代码进行混淆和加密,生成加密后的 `.so`。 --- #### **(2) 在程序中动态解密并加载** 加密后的 `.so` 文件无法直接通过 `dlopen` 加载,需在内存中解密后使用: ```c #include <dlfcn.h> #include <stdio.h> #include <stdlib.h> // 解密函数(需自行实现) void* decrypt_so_file(const char* encrypted_path, const char* key, size_t* decrypted_size) { // 读取加密文件到内存 // 使用密钥解密数据 // 返回解密后的内存指针及大小 return decrypted_buffer; } int main() { const char* key = "your_secret_key"; size_t decrypted_size; // 解密 .so 到内存 void* decrypted_data = decrypt_so_file("encrypted.so", key, &decrypted_size); // 将解密后的数据写入临时文件(或内存加载) FILE* tmp = fopen("/tmp/decrypted.so", "wb"); fwrite(decrypted_data, 1, decrypted_size, tmp); fclose(tmp); // 加载解密后的 .so void* handle = dlopen("/tmp/decrypted.so", RTLD_LAZY); if (!handle) { fprintf(stderr, "Error: %s\n", dlerror()); return 1; } // 使用符号(例如调用函数) typedef void (*func_ptr)(); func_ptr my_func = (func_ptr)dlsym(handle, "my_function"); my_func(); // 清理 dlclose(handle); remove("/tmp/decrypted.so"); free(decrypted_data); return 0; } ``` --- #### **(3) 进阶方法:避免写入磁盘** 直接写入磁盘可能被恶意读取,可通过以下方式改进: - **内存加载(Linux 专用)** 使用 `memfd_create` 创建匿名内存文件描述符,绕过物理文件: ```c #include <sys/mman.h> #include <sys/syscall.h> #include <unistd.h> int fd = syscall(SYS_memfd_create, "decrypted.so", 0); ftruncate(fd, decrypted_size); write(fd, decrypted_data, decrypted_size); char proc_path[128]; sprintf(proc_path, "/proc/self/fd/%d", fd); void* handle = dlopen(proc_path, RTLD_LAZY); ``` --- #### **(4) 密钥管理** - **安全存储密钥**: - 将密钥硬编码在代码中(简单但不安全)。 - 从远程服务器动态获取(需网络权限)。 - 使用白盒加密技术或硬件安全模块(HSM)。 --- ### **3. 注意事项** - **性能影响**:加解密会增加启动时间和内存占用。 - **依赖项**:确保解密后的 `.so` 文件依赖的其他库已正确加载。 - **调试保护**:结合调试技术(如 `ptrace` 检测)防止动态分析。 - **合法性**:确保你有权加密和分发该 `.so` 文件。 --- ### **4. 工具推荐** - **加密工具**:`openssl`、`mbed TLS` - **代码保护工具**:Virbox Protector、OLLVM(代码混淆) - **内存加载库**:`libelfin`(高级内存操作) --- 通过以上方法,你可以在保护 `.so` 文件的同时,实现安全的动态加载。若需要更详细的代码示例或工具配置说明,请进一步说明你的使用场景(如 Android NDK 或 Linux 环境)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值