判断 SO 加固强度,我一般走两步
最近把判断SO加固强弱的思路整理了一下。核心就一句:静态那几样(符号清没清、字符串藏没藏、.text 能不能反编译)看着唬人,跟它到底防不防得住,是两回事。见过静态铁桶、一个固定脱壳点就破功的,也见过静态平平、运行时根本碰不了的——所以我不拿"静态看着强不强"当结论,而是分两步走。
第一步,静态只回答一个问题:信息还剩多少
ELF 结构从 readelf -S 和 readelf -l 入手。别只盯"节区名奇不奇怪",看几处硬指标:.text 的文件偏移在不在一个带可执行标志的 PT_LOAD 里;某个可加载段 p_filesz 和 p_memsz 差得大、又不像 .bss 那种零填充,那多出来的内存很可能是运行时才填的载荷;e_shoff 被清零或指向垃圾,IDA 就只能按段加载、函数边界得自己划。壳的解密和自校验入口常挂在 DT_INIT / .init_array 或 JNI_OnLoad,先扫这几个入口比通读 .text 快。
熵值我一般按块算,某段长期贴着 8 bits/byte 就是压缩或加密——注意只能到这,区分不了压缩还是加密,更说不出算法和密钥怎么管:
# 每 4KB 一块算 Shannon 熵,看哪段贴近 8.0(懒得写脚本用 binwalk -E 看曲线也行)
import math, collections
def block_entropy(data, n=4096):
for i in range(0, len(data), n):
b = data[i:i+n]
c = collections.Counter(b)
H = -sum(v/len(b) * math.log2(v/len(b)) for v in c.values())
print(f"{i:#08x} {H:.2f}")
符号分开看 .symtab 和 .dynsym。发布版 strip 掉 .symtab 很常见,但 .dynsym 删不干净(动态链接要靠它),readelf --dyn-syms 总能捞一批。真正要数的是里面还剩多少业务语义:导出函数、JNI 方法、C++ 的 RTTI。RTTI 尤其容易漏,_ZTV、_ZTS 这些没清的话,nm -C 一 demangle,类名就回来了。JNI 还有个分水岭——native 方法是静态导出(Java_包名_方法名 明摆在 .dynsym)还是走 RegisterNatives 动态注册,后者静态基本看不到映射,得运行时 hook 才能对上。字符串别只数 strings 的行数,栈上拼接的和加密表用时才解的它扫不到,floss 能还原一批。
这一步量的都是"读代码的门槛"。重要,但它只是门槛——静态藏得再好,也拦不住下一步。
第二步,也是真正分高下的:追代码怎么进内存
为什么静态那层撑不住?因为代码终归要执行,执行前必须在内存里还原成能跑的样子,你静态藏得越狠,那一刻内存里的明文反而越完整、越好用。破解的人很多时候根本不读你的代码,就等你自己解密。所以这一步看的是:代码什么时候、以什么形态进内存,那个恢复点好不好找、拿到的东西完不完整。
具体用 Frida 挂几个点看:dlopen / android_dlopen_ext 看加载时机,mprotect 盯段权限翻到 PROT_EXEC(现在普遍 W^X,通常是先 rw- 再 mprotect 成 r-x,多半就是载荷刚解密的落点)。再看 /proc/<pid>/maps 里是不是匿名 rwx 段——注意 ART 的 JIT 也会这样,得结合调用栈判断。确认之后把内存态 dump 下来,和静态文件按段 diff,差异区就是运行时才出现的真实代码;dump 出来往往缺节区头,用 SoFixer 按 PT_LOAD 重建一下 IDA 才好加载。
几个关键判断落在这一步:是一次性把整段解出来(脱壳点固定就能脚本化批量脱),还是分段按需、用完就擦;是真虚拟机——原生指令被翻成自定义字节码,看得到中央 dispatcher 和 handler 表——还是只是控制流平坦化(OLLVM 那种反编译出来也难看,但底层还是原生指令,这俩别混)。运行时那层顺带看它防不防你动手:.text 映射后有没有自校验、能不能发现 inline hook(比对函数头几字节的在盘值和内存值)、TracerPid 非零就中止之类。
拿手游里最常见的 IL2CPP 举个例子:global-metadata.dat 就算加密,运行时也得在固定入口(MetadataCache::Initialize 里的 LoadMetadataFile)解回明文,攻击者不碰密钥,等进程解开直接在内存里按 magic 0xFAB11BAF 搜出来 dump 走。壳类 SO 一个道理——加密载荷解到可执行段那一刻,就是恢复入口暴露的窗口。
所以我不再给一个笼统的分数
加固强度更像几条互相独立的轴:结构暴露多少、语义暴露多少、代码恢复的窗口多大、加载链保护做没做。比两个 SO,我看的是——从静态定位到运行时恢复,哪一份留下的稳定入口更少、完整代码暴露的窗口更短、篡改后触发的校验更完整。静态那三样只是第一轮筛查,能不能延续到加载阶段,才是分水岭。说到底,判断一个 SO,判的是把它逆到能用、对方得卡在哪、卡多久,不是它第一眼看着唬不唬人。
浙公网安备 33010602011771号