字节暗面

SO 加固强度怎么判断:从 ELF 结构、符号、字符串到加载链


拿到一个 SO,最先看的通常是符号、字符串和 .text。这些结果能反映静态分析的起步难度,却不能直接代表整体加固强度。有些文件让 IDA、Ghidra 很难正确识别,运行时却会集中还原完整代码;有些结构看着普通,反调试、Hook 检测和完整性校验却做得很重。

所以我一般分两步检查:先看静态还暴露多少信息,再追代码如何进入内存。

静态检查不能只看“能不能打开”

ELF 结构可以从节区表和加载段入手:readelf -S 看节区,readelf -l 看程序头和 PT_LOAD。重点检查 .text.rodata 是否仍以常规形式存在,e_shoff 是否被清零,业务代码有没有移到附加载荷中。壳的初始化逻辑常会经过 DT_INIT.init_arrayJNI_OnLoad,从这些入口往下追通常更快。

熵值只能作为辅助。按块计算后,如果某段数据长期接近 8 bits/byte,可以怀疑它经过压缩或加密,但无法据此区分二者,更不能判断算法和密钥管理是否可靠。

符号要分开看 .symtab.dynsym。发布版删除 .symtab 很常见,.dynsym 通常仍保留动态链接所需的最小集合。重点是导出函数、JNI 方法和 C++ RTTI 还剩多少业务语义。_ZTV_ZTI_ZTS 如果没有清理,用 nm -C 仍可能还原出类名。静态导出的 Java_包名_方法名 可以直接定位;改用 RegisterNatives 后,通常要结合运行时注册过程确认函数映射。

字符串也不要只数行数。接口地址、日志以及 rootfridaTracerPid 等检测线索如果以明文出现,很容易把范围缩到少数函数。strings 没有结果,也可能只是字符串被放进加密表或动态拼接,运行时依然会出现明文。

检查SO文件中的ELF结构、符号和字符串信息

加载链决定静态保护能撑多久

静态检查之后,还要厘清壳 SO、业务 SO 和载荷的关系:谁先加载,代码何时恢复,恢复出来的是完整 ELF、局部代码块,还是虚拟机字节码。

一次性还原整段代码,暴露窗口通常更集中;分段、按需解密能缩短明文驻留时间,但还要看恢复位置是否固定、用完后是否重新加密。虚拟化也不能凭“反编译失败”认定。控制流平坦化底层仍是原生指令;真正的虚拟化通常能看到字节码、dispatcher 和 handler 参与执行。

运行时可以观察 dlopenandroid_dlopen_extmprotect 以及 /proc/<pid>/maps。内存从可写转为可执行,可能对应载荷恢复,也可能来自正常加载或 ART JIT,需要结合调用栈和映射内容判断。再把运行时内存与静态文件按区域对比,就能看到哪些代码只在运行后出现、是否长期保持明文。

还要看恢复过程有没有和代码段完整性校验、GOT/PLT 改写检测及反调试能力绑定。单独看到 ptracemprotect 等导入函数不能证明防护有效,是否触发以及如何响应,仍需动态验证。

SO载荷在运行时恢复并产生明文暴露窗口

不必急着给出一个总分

SO 加固更适合拆成四个维度:

维度 主要判断依据
结构暴露 PT_LOADe_shoff、常规节区和附加载荷
语义暴露 .dynsym、RTTI、JNI 映射和明文字符串
代码恢复 解密粒度、恢复位置、明文驻留和虚拟化形态
加载链保护 恢复入口、完整性校验及反调试、反 Hook 响应

这些结果描述的是当前样本的分析成本,不能代表某家厂商的全部版本和配置。比较两款 SO 时,更值得看的是:稳定入口有多少,完整代码暴露多久,篡改后能否触发校验。

从结构、语义、代码恢复和加载链判断SO加固强度

ELF 结构、符号和字符串适合做第一轮筛查。真正拉开差距的,是这些处理能否延续到加载阶段,以及代码进入内存后还剩多少可以利用的窗口。

← 返回列表