判断一个 SO 加固强不强,别停在"符号清没清、字符串搜不搜得到"。一句话先摆这儿:静态只量"读代码"的门槛,加载链才决定"dump"的成本。 下面按三组过,能自查也能拿去比两个方案。
一、静态:看信息还剩多少
- ☐ ELF 结构 :
readelf -S/readelf -l------.text、.rodata是否还在常规位置;e_shoff是否被清零(清了 IDA 只能按段加载);某个可加载段p_filesz和p_memsz差得大、又不像.bss零填充(可能是运行时才填的载荷);业务代码是否被搬进附加载荷。 - ☐ 熵 :按块算(
binwalk -E看曲线,或每 4KB 算 Shannon 熵)。长期贴 8 bits/byte = 压缩或加密,注意区分不了两者。 - ☐ 符号 :
.symtab常被 strip,.dynsym删不干净,readelf --dyn-syms捞一批。看导出函数、JNI、RTTI(_ZTV/_ZTS,nm -C能还原类名)还剩多少;JNI 是静态导出Java_包名_方法名,还是RegisterNatives动态注册(后者要运行时 hook 才能对上映射)。 - ☐ 字符串 :接口地址、
root/frida/TracerPid有没有明文;栈上拼接和加密表strings扫不到,上floss。
二、加载链:看代码怎么进内存(决定结论)
- ☐ Frida 挂
dlopen/android_dlopen_ext看加载时机;挂mprotect盯段权限翻到PROT_EXEC。 - ☐
/proc/<pid>/maps看有没有匿名rwx段(ART 的 JIT 也会rw-→r-x,结合调用栈判断)。 - ☐ 运行时内存 dump 出来,和静态文件按段 diff,差异区 = 运行后才出现的真实代码;dump 缺节区头,用 SoFixer 按段重建再进 IDA。
- ☐ 恢复形态:一次性全解还是分段按需;脱壳点固不固定(固定 = 可脚本化批量脱);真 VM(dispatcher / handler / 字节码)还是只是控制流平坦化。
三、运行时:防不防你动手
- ☐
.text映射后有没有自校验(CRC / 哈希);inline hook 检测(函数头几字节在盘 vs 内存);GOT / PLT 有没有被改写。 - ☐ 反调试(
TracerPid非零、ptrace占位)、反 hook(/proc/self/maps找frida/gum特征)是否绑在恢复过程上------刚解密就自校验,比事后补检查难绕。
一段可复现的观察脚本
javascript
// SO 何时加载、加载后哪段内存翻成可执行------多半是载荷刚解密的落点
["dlopen", "android_dlopen_ext"].forEach(n =>
Interceptor.attach(Module.findExportByName(null, n), {
onEnter(a) { this.path = a[0].readCString(); },
onLeave() { console.log("load:", this.path); }
}));
Interceptor.attach(Module.findExportByName(null, "mprotect"), {
onEnter(a) {
if (a[2].toInt32() & 4) // PROT_EXEC
console.log("+X @", a[0], "len", a[1].toInt32());
}
});
拆成四条轴对照
别打一个笼统总分,按四轴分别看:
| 维度 | 弱的表现 | 强的表现 |
|---|---|---|
| 结构暴露 | .text 完整、可正常反编译 |
空壳 + e_shoff 清零、有附加载荷 |
| 语义暴露 | 导出 / JNI / RTTI 大量明文 | 符号清干净、RegisterNatives 动态注册 |
| 代码恢复 | 一次性全解、脱壳点固定 | 分段按需、OEP 混淆 / 真 VM |
| 加载链保护 | 无自校验、无反调试反 hook | .text 自校验 + 反调试 / hook 绑恢复过程 |
几条建议
- 别拿"符号干不干净"当唯一标尺,四条轴分开看才不容易误判。
- 比较两个 SO:从静态定位到运行时恢复,谁留下的稳定入口更少、完整代码暴露的窗口更短、篡改后触发的校验更完整。
- 说到底,判断加固强不强,判的是把它逆到能用、对方要卡在哪、卡多久。静态那三样只是第一轮筛查,能不能延续到加载阶段才是分水岭。