-
-
[原创]Android逆向之SO防护(从逆向角度谈SO防护)
-
-
[原创]Android逆向之SO防护(从逆向角度谈SO防护)
作为一名 Android 逆向开发者,我们工作的相当大一部分就是分析 so 文件,so的安全性对保护Android APP核心逻辑和敏感数据是极其重要的,特别其中的明文字符串、函数名等很容易成为逆向者的突破口。那么,从攻击者的角度来看,该如何提高SO的防护呢?这里提供一些并不需要太多coding的方案,其对于程序的加固提升效果明显,可以显著提升逆向的门槛。
注: 下面内容适合有一定 NDK 开发经验的小伙伴阅读!
一、施加编译选项,让 so 难以被分析
简单说,就是在 Android 项目文件 build.gradle 或 CMakeLists.txt 中配置编译选项,下面是可以采用配置:
1. 代码混淆层面(让代码逻辑难以捉摸)
- LLVM Obfuscator:混淆利器
在研究的多个逆向目标中,LLVM Obfuscator 是 C/C++ 混淆的首选工具。包括:
- 控制流平坦化:将函数控制流打乱,生成复杂的跳转逻辑。
- 指令替换:用复杂指令序列替换简单指令,保持功能不变。
- 虚假控制流:插入无意义的跳转,干扰静态分析。
- 字符串加密:编译时加密字符串,运行时动态解密,防止明文泄露。
- 实现要点:集成 Obfuscator-LLVM (O-LLVM) 修改 NDK 工具链配置。配置稍复杂,但效果显著,适合高安全性需求的场景。
- 简易混淆脚本
如果项目资源有限,你还可以编写脚本在编译前重命名非 JNI 接口的函数和变量。这方法简单,但效果比O-LLVM要弱(需小心避免影响 JNI 签名)
2. 提高编译优化级别(打乱代码结构)
3. 链接时优化 (LTO):全局混淆
- -flto:跨文件优化
-flto
启用链接时优化,跨越编译单元进行全局优化,打乱代码逻辑,增加逆向难度。 - 可以按照如下设置:
- CMakeLists.txt:
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -flto")
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} -flto")
set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} -flto") - build.gradle:
cFlags "-flto"
cppFlags "-flto"
ldFlags "-flto"
4. 剥离符号表(隐藏关键函数信息)
- -s 和 strip 命令 剥离符号表可防止函数名和变量名暴露。Android Gradle 插件在 Release 构建中默认剥离,但最好手动确认一下。
- 示例:
- CMakeLists.txt:
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -s")
add_custom_command(TARGET your_library_name POST_BUILD
COMMAND ${CMAKE_STRIP} $<TARGET_FILE:your_library_name>) - build.gradle:通过
packagingOptions
控制剥离行为。
5. 禁用 RTTI 和异常(减少元数据)
- -fno-rtti 和 -fno-exceptions 如果代码不依赖运行时类型信息 (RTTI) 或 C++ 异常,可以禁用以减小 .so 文件体积并移除元数据。
- 示例:
- CMakeLists.txt:
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -fno-rtti -fno-exceptions")
6. 控制符号可见性(隐藏函数内部实现)
建议处理JNI等需要EXport的,其他都尽可能hide。

【】
在Android逆向分析中,明文字符串是我们的首要目标。你在项目中可以用下策略隐藏敏感字符串:
1. 字符串加密,运行时解密
- XOR 加密:简单高效 使用 XOR 操作加密字符串,运行时解密。这个通常要结合 Base64 编码处理不可打印字符。
std::string ENCRYPTION_KEY = "com.facemn.newlab"; // 密钥需进一步保护,不可硬编码
std::string get_string_runtime(const char* encrypted_bytes, size_t len, char key_char) {
std::string decrypted_str(len, '\0');
for (size_t i = 0; i < len; ++i) {
decrypted_str[i] = encrypted_bytes[i] ^ key_char;
}
return decrypted_str;
} - 编译时加密 可以编写脚本在编译前加密字符串,生成字节数组存储在头文件中,运行时动态解密,避免明文。
- 自定义编码 对于短字符串,可使用查找表或自定义编码方案,增加逆向难度。
2. 分割和重组字符串(分割)
- 将敏感字符串分割为多个部分,运行时拼接:
std::string part1 = "sec";
std::string part2 = "ret_";
std::string part3 = "key";
std::string secret = part1 + part2 + part3;
3. 栈上构造字符串(上栈)
- 对于临时短字符串,在栈上逐字符构造,避免存储在
.rodata
段:
char my_secret[7];
my_secret[0] = 's';
my_secret[1] = 'e';
my_secret[2] = 'c';
my_secret[3] = 'r';
my_secret[4] = 'e';
my_secret[5] = 't';
my_secret[6] = '\0';
4. 关闭日志,避免泄露(这个也是正向开发的基本要求)
- 确保
LOGD
、LOGE
不输出敏感信息,如密钥或函数名。(一定要各级别的关闭日志开关,切记!!!)
5. 保护加密密钥(一定要避免硬编码密码到项目中!)
- 密钥本身需加密或分割存储。复杂场景可考虑白盒加密,将密钥与算法融合,虽然实现成本较高,但真有效。
三、其他安全措施
除了上面的编译和代码优化,还可以在项目中加入以下保护手段:
1. 反调试技术(也算常规的手段了~)
- 检查
/proc/self/status
中的 TracerPid
或使用 ptrace(PTRACE_TRACEME, 0, 0, 0)
检测调试器。 - 检查
AndroidManifest.xml
的 debuggable
标志,检测到调试时退出程序或返回错误数据。
2. 完整性校验
- 运行时计算 .so 文件的校验和(CRC32、MD5、SHA1),与预存值比较,检测篡改。需安全存储校验和。
3. JNI 函数名混淆(避免一些三方工具找到JNI函数的地址)
- 使用
RegisterNatives
动态注册 JNI 函数,隐藏原始函数名:
jstring my_obfuscated_name_for_stringFromJNI(JNIEnv* env, jobject /* this */) {
std::string hello = "Hello from C++ (obfuscated)";
return env->NewStringUTF(hello.c_str());
}
static const JNINativeMethod gMethods[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void*)my_string_ojbsc_for_stringFromJNI}
};
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = nullptr;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jclass clazz = env->FindClass("com/newxjt/slotGames");
if (!clazz) {
return JNI_ERR;
}
if (env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) < 0) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
【特别提示】:上述方案需要您评估策略代价
尤其是以下因素是需去要权衡的(记住,任何措施防护都是有代价的):
所以有很多大厂的APP防护看起来没那么【强】,这里并不是他们没有对应的防护策略和能力,而是他们需要考虑稳定性,兼容性,还有对APP性能的影响!
[培训]科锐逆向工程师培训第53期2025年7月8日开班!
最后于 11小时前
被易码编辑
,原因: