防篡改与完整性链
签名自校验、文件校验和与组件真实性验证——如何在被重打包的应用造成损失前发现它。
重打包至今仍是 Android 应用最高频的攻击手法,而且它之所以有效,恰恰因为系统自身的签名校验发现不了。攻击者解包你的 APK,改掉授权校验或注入广告 SDK,用自己的密钥重签,然后分发出去。系统看到的是一个签名有效、作者不同的包,照装不误。要发现它,只能让应用在内部自证身份。
第一层是签名指纹校验。取 PackageManager.GET_SIGNING_CERTIFICATES,拿到 apkContentsSigners,对证书做哈希,再与构建期写入的发布指纹比对。这段 Kotlin 代码只有三行——也正因如此,攻击者删掉它同样只要三秒。所以 Java 层的校验永远只承担「快速拒绝」,不能当作真正的防线。
真正的校验要下沉到 native 层。放在 JNI 侧的校验器定位和 patch 的成本高得多,但它不能只返回一个布尔:应该返回一个被应用以密码学方式依赖的值,或者参与密钥派生,这样把返回值 NOP 掉得到的是一个坏掉的应用,而不是一个被绕过的应用。再配合对 classes.dex 与关键 .so 的校验和——构建期计算、运行时复验。VALLUM 的 SignSeal 层就是把这三层(APK 签名、文件校验和、组件真实性)嵌成一条多层完整性链。
组件真实性是最容易被跳过的一环。Android 按名字解析 activity、service、receiver,被重打包的应用可以新增或替换组件来拦截 intent。运行时应当校验「你期望的 manifest」与「实际安装的 manifest」一致:枚举 PackageInfo 的 activities、services、receivers,与构建期摘要比对。成本很低,却能堵住单靠签名校验留下的注入路径。
响应策略要克制,因为过于激进的篡改检测是自伤。厂商 ROM、会重新签名的应用商店、企业 MDM 包装,都会产生「合法」的签名不匹配。优先选择优雅降级——关闭敏感功能、向后端上报异常、给出提示——而不是立刻杀进程把边缘场景变成一星差评。只在价值最高的路径上保留硬中止,并且永远用服务端开关控制,这样不用发版就能临时关闭。
最后要接受:这一切是在抬高成本,而不是实现不可能。下定决心的攻击者拿着设备,最终总能找到校验点;目标只是让它花的时间超过资产本身的价值。完整性校验真正划算的前提是叠加混淆(让校验点难找)、函数抽取(让逻辑不在 DEX 里可读)和 RASP(让运行时 patch 被检测)——还是那句分层的道理。