目录
  1. 1. 一、为什么要对 SO 加固
  2. 2. 二、编译期保护手段
  3. 3. 三、运行时保护手段
  4. 4. 面试常考问题
【逆向安全技术-防护篇】so加固原理

一、为什么要对 SO 加固

在 Android 逆向攻防中,即便是做了一层 DEX 加固,攻击者仍可从 lib 目录下的 SO 文件下手——SO 中通常包含了核心算法、签名校验逻辑、加解密密钥等敏感信息。对 SO 的加固就是要在编译期和运行期对 Native 代码施加各类保护手段,使其难以被静态分析和动态调试。

二、编译期保护手段

符号剥离(Strip): 编译时使用 -s 参数或调用 strip 工具删除符号表,使 IDA Pro 打开后无法看到函数名,只能看到 sub_1234 这样的无意义符号。代码混淆(OLLVM): LLVM 编译套件提供 -mllvm -fla(控制流平坦化)、-mllvm -sub(指令替换)、-mllvm -bcf(虚假控制流)等混淆选项,将原本清晰的逻辑分支打散成由分发器控制的 switch-case 结构,使静态分析的控制流图变得极为复杂。字符串加密: 自定义 LLVM Pass 在编译时将字符串常量加密存储,运行时调用统一的解密函数还原,防止通过 strings 命令或 IDA 的字符串窗口直接定位到敏感信息。

三、运行时保护手段

代码段加密: 使用链接脚本或编译后处理工具,将 .text 段中的函数加密存放,在 .init_array(动态链接器最先执行的初始化回调)中解密恢复后清空解密密钥内存区域。

反调试(Anti-Debugging): 最常见的做法是通过 ptrace(PTRACE_TRACEME, 0, 0, 0) 自附加——每个进程最多被一个调试器附加,一旦自占成功,攻击者就无法再用 gdb/lldb 附加。同时循环读取 /proc/self/status 中的 TracerPid 字段,若不为 0 则立即终止。检查 /proc/self/wchan 内容也可以检测 ptrace 是否挂起。

// SO 加固常用的反调试检测
void anti_debug() {
// 方式1: ptrace 自占
if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) {
__android_log_print(ANDROID_LOG_ERROR, "protect", "ptrace failed, exit");
_exit(0);
}
// 方式2: 循环检测 TracerPid
while (1) {
FILE *fp = fopen("/proc/self/status", "r");
char buf[1024];
while (fgets(buf, sizeof(buf), fp)) {
if (strncmp(buf, "TracerPid:", 10) == 0) {
int pid = atoi(buf + 10);
if (pid != 0) _exit(0);
}
}
fclose(fp);
sleep(1);
}
}

反 Hook: 检测 Xposed 框架可以通过检查 /proc/self/maps 中是否加载了 xposed 相关模块;检测 Frida 可以通过扫描自身进程内存中的 frida-agent 特征字符串或使用 open 调用检测 /proc/self/task/<tid>/maps 中是否存在 frida 线程段。此外还可以通过 dlopen/dlsym 方式直接获取原始系统调用(如原始 ptrace),避免被上层 hook 拦截。

完整性校验:.init_array 中计算 .text 段的 CRC32 或 MD5 哈希值,与编译期预埋的期望值比较。不一致则说明 SO 被静态修改或打了补丁,立即退出进程。

面试常考问题

Q1: OLLVM 的控制流平坦化(-fla)是如何工作的?
A: 它将函数的控制流结构重组:引入一个状态变量和中心分发器(dispatcher),每个基本块执行完后更新状态变量并跳回分发器,分发器根据状态值跳转到下一个基本块。这打破了原始的顺序流程,使得从静态反编译结果中恢复逻辑变得非常困难。AOSP 中 LLVM 工具链路径:/prebuilts/clang/host/linux-x86/ 下的 Android NDK 编译器链支持 OLLVM 的集成。

Q2: ptrace 自占反调试能否被绕过?
A: 可以。一种方式是使用 Frida 的 -f 模式在应用启动前注入,此时 ptrace 尚未执行;另一种是 hook ptrace 系统调用使其始终返回 0;更底层的方式是修改内核 ptrace 的实现(如 Magisk 模块)允许多调试器附加。但加固的策略是组合使用多种检测,任何单一手段被绕过都不会导致整体失效。

Q3: 代码段加密与普通 SO 加载有什么不同?
A: 普通 SO 的 .text 段由系统 linker(/bionic/linker/linker.cpp)通过 mmap 直接映射为可执行内存。加密 SO 则需要自定义链接脚本将 .text 段先映射为 PROT_READ | PROT_WRITE,在 .init_array 中调用 mprotect 恢复可执行权限后再解密。AOSP 中 linker 源码路径:/bionic/linker/linker.cpp/bionic/linker/linker_soinfo.cpp

打赏
  • 微信
  • 支付宝

评论