从NDK提取隐藏字符串及native-lib.cpp多字符串处理咨询
嘿,我来帮你搞定这个问题——先解决你关心的隐藏字符串不被轻易提取的需求,再聊聊怎么在native-lib.cpp里更灵活地处理更多字符串。
一、NDK中隐藏字符串的核心方案:加密+运行时解密
你当前的JNI代码里,字符串是直接硬编码在二进制文件里的,用strings工具或者IDA这类逆向工具一眼就能揪出来。要隐藏它们,最简单高效的方式是提前加密字符串,在JNI运行时解密,推荐用异或加密(性能开销极小,适合Native层)。
完整实现代码
#include <jni.h> #include <cstring> #include <cstdint> // 自定义异或密钥(随便选一个字节值,自己记好就行) static const uint8_t XOR_KEY = 0x1F; // 解密函数:对字符串每个字节做异或运算 static void xor_decrypt(uint8_t* str, size_t str_len) { for (size_t i = 0; i < str_len; ++i) { str[i] ^= XOR_KEY; } } JNIEXPORT jobjectArray JNICALL Java_como_foo_bar_getData(JNIEnv *env, jobject jobj){ // 这里是提前加密后的字符串(用Python脚本把原字符串每个字节异或0x1F生成) uint8_t encrypted_strs[4][32] = { {0x46, 0x38, 0x3D, 0x3D, 0x3E, 0x12, 0x27, 0x3F, 0x3B, 0x3C, 0x12, 0x22, 0x3E, 0x12, 0x00}, {0x46, 0x38, 0x3D, 0x3D, 0x3E, 0x12, 0x27, 0x3F, 0x3B, 0x3C, 0x12, 0x20, 0x3E, 0x12, 0x00}, {0x46, 0x38, 0x3D, 0x3D, 0x3E, 0x12, 0x27, 0x3F, 0x3B, 0x3C, 0x12, 0x21, 0x3E, 0x12, 0x00}, {0x46, 0x38, 0x3D, 0x3D, 0x3E, 0x12, 0x27, 0x3F, 0x3B, 0x3C, 0x12, 0x23, 0x3E, 0x12, 0x00} }; const size_t str_count = sizeof(encrypted_strs) / sizeof(encrypted_strs[0]); // 先解密所有字符串 for (size_t i = 0; i < str_count; ++i) { xor_decrypt(encrypted_strs[i], strlen((char*)encrypted_strs[i])); } // 创建Java字符串数组返回 jclass string_cls = env->FindClass("java/lang/String"); if (string_cls == nullptr) return nullptr; // 找不到String类,直接返回 jobjectArray ret_array = env->NewObjectArray(str_count, string_cls, nullptr); if (ret_array == nullptr) { env->DeleteLocalRef(string_cls); return nullptr; // 内存分配失败 } // 把解密后的字符串逐个存入数组 for (size_t i = 0; i < str_count; ++i) { jstring j_str = env->NewStringUTF((char*)encrypted_strs[i]); env->SetObjectArrayElement(ret_array, i, j_str); env->DeleteLocalRef(j_str); // 释放局部引用,避免JNI内存泄漏 } env->DeleteLocalRef(string_cls); return ret_array; }
为什么这样有效?
加密后的字符串在二进制文件里是乱码,静态分析工具提取不到原内容,只有当程序运行时,JNI层解密后才会生成正常字符串。如果需要更高安全性,可以换成AES等对称加密算法,但异或已经能应付大部分基础逆向分析了。
二、在native-lib.cpp中处理更多字符串的实用方法
如果需要处理大量字符串,或者不想把字符串硬编码在代码里,可以试试下面几种方案:
1. 从Assets资源文件读取字符串
把字符串放在Android项目的assets目录下(比如hidden_strings.txt,每行一个字符串),JNI层通过AssetManager读取,这样字符串不会编译进二进制:
#include <jni.h> #include <android/asset_manager.h> #include <android/asset_manager_jni.h> #include <cstring> JNIEXPORT jobjectArray JNICALL Java_como_foo_bar_getData(JNIEnv *env, jobject jobj, jobject asset_mgr){ // 把Java层的AssetManager转成Native层的AAssetManager AAssetManager* native_mgr = AAssetManager_fromJava(env, asset_mgr); if (native_mgr == nullptr) return nullptr; // 打开assets目录下的文件 AAsset* file = AAssetManager_open(native_mgr, "hidden_strings.txt", AASSET_MODE_BUFFER); if (file == nullptr) return nullptr; // 读取文件内容 const char* file_content = (const char*)AAsset_getBuffer(file); off_t file_len = AAsset_getLength(file); if (file_len <= 0) { AAsset_close(file); return nullptr; } // 这里可以按行分割内容(示例用简单的换行分割) // 先统计行数 size_t line_count = 1; for (off_t i = 0; i < file_len; ++i) { if (file_content[i] == '\n') line_count++; } // 创建字符串数组 jclass string_cls = env->FindClass("java/lang/String"); jobjectArray ret_array = env->NewObjectArray(line_count, string_cls, nullptr); if (ret_array == nullptr) { AAsset_close(file); env->DeleteLocalRef(string_cls); return nullptr; } // 分割字符串并存入数组 char* line_start = (char*)file_content; size_t idx = 0; for (off_t i = 0; i < file_len; ++i) { if (file_content[i] == '\n' || i == file_len - 1) { size_t line_len = i - (line_start - file_content) + 1; char line_buf[256]; strncpy(line_buf, line_start, line_len); line_buf[line_len - (file_content[i] == '\n' ? 1 : 0)] = '\0'; // 去掉换行符 jstring j_str = env->NewStringUTF(line_buf); env->SetObjectArrayElement(ret_array, idx++, j_str); env->DeleteLocalRef(j_str); line_start = (char*)file_content + i + 1; } } // 清理资源 AAsset_close(file); env->DeleteLocalRef(string_cls); return ret_array; }
注意:要在CMakeLists.txt里添加
target_link_libraries(your-lib-name android),链接Android的Asset库。
2. 动态生成字符串
如果字符串有规律,可以在运行时动态拼接,避免硬编码完整字符串:
char buffer[64]; // 动态生成"Hello from C++"、"Hello from D++"等 for (char c = 'C'; c <= 'F'; ++c) { snprintf(buffer, sizeof(buffer), "Hello from %c++", c); jstring j_str = env->NewStringUTF(buffer); // 存入数组... env->DeleteLocalRef(j_str); }
3. 延迟加载的字符串池
把加密后的字符串数据存在一个数组里,需要的时候再解密,减少内存占用同时增加逆向难度:
// 只在需要使用某个字符串时才解密 static jstring get_hidden_string(JNIEnv* env, uint8_t* encrypted_str) { xor_decrypt(encrypted_str, strlen((char*)encrypted_str)); jstring j_str = env->NewStringUTF((char*)encrypted_str); // 可以重新加密回去,避免内存里留下明文 xor_decrypt(encrypted_str, strlen((char*)encrypted_str)); return j_str; }
对你原有代码的小补充
你原来的代码里NewObjectArray的第三个参数没写完,应该传nullptr(表示数组初始元素为空);另外要记得释放JNI局部引用,不然会导致内存泄漏哦~
内容的提问来源于stack exchange,提问作者user9427911




