首页
社区
课程
招聘
[原创]Android逆向之SO防护(从逆向角度谈SO防护)
发表于: 2天前 228

[原创]Android逆向之SO防护(从逆向角度谈SO防护)

2天前
228

作为一名 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. 提高编译优化级别(打乱代码结构

  • -O2/-O3/-Os:优化与安全并重 高优化级别(如 -O2 或 -O3)通过函数内联、循环展开等技术,使反编译代码难以理解。

  • -Os 特别是专注于代码大小优化,同时可生成紧凑但晦涩的机器码。


  • 比如下面这个配置:
    • CMakeLists.txt
      set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") # 或 -Os
    • build.gradle (NDK Build)
      android {
      defaultConfig {
      externalNativeBuild {
      ndkBuild {
      cFlags "-O3"
      cppFlags "-O3"
      }
      }
      }
    • build.gradle (CMake)
      android {
      defaultConfig {
      externalNativeBuild {
      cmake {
      cppFlags "-O3"
      }
      }
      }

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。

  • -fvisibility=hidden 使用 __attribute__((visibility("hidden"))) 隐藏非 JNI 函数和变量,仅对 JNI 接口使用 JNIEXPORT 或 __attribute__((visibility("default")))
  • 代码示例
    #define API_EXPORT __attribute__((visibility("default")))
    #define INTERNAL_FUNC __attribute__((visibility("hidden")))

    API_EXPORT void JNI_OnLoad(...) { /* JNI 初始化 */ }
    INTERNAL_FUNC int some_internal_calculation() { /* 内部逻辑 */ }
  • CMakeLists.txt 配置
    set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -fvisibility=hidden")


  • 下面是经过处理的例子,在IDA中函数列表中:(可以看到函数名字全部为无语义的地址命名)


【】

在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. 关闭日志,避免泄露(这个也是正向开发的基本要求)

  • 确保 LOGDLOGE 不输出敏感信息,如密钥或函数名。(一定要各级别的关闭日志开关,切记!!!)

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

特别提示】:上述方案需要您评估策略代价

尤其是以下因素是需去要权衡的(记住,任何措施防护都是有代价的):

  • 复杂性:高级混淆(如 O-LLVM)会增加构建难度,要考虑兼容性,需评估团队能力。
  • 性能:加密和解密操作可能影响运行效率,需性能测试
  • 调试:高度混淆的代码难以调试,只需 Release 构建中启用。
  • 稳定性:不当混淆可能引入 bug,需做好充分测试~

  • 兼容性:确保高,中,低端各类手机,系统版本能正常运行。

所以有很多大厂的APP防护看起来没那么【强】,这里并不是他们没有对应的防护策略和能力,而是他们需要考虑稳定性,兼容性,还有对APP性能的影响!



[培训]科锐逆向工程师培训第53期2025年7月8日开班!

最后于 11小时前 被易码编辑 ,原因:
收藏
免费 0
支持
分享
最新回复 (2)
雪    币: 2
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
6666 卷
1天前
1
雪    币: 1498
活跃值: (2533)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
3
一股子AI味道
1天前
0
游客
登录 | 注册 方可回帖
返回