安卓代码加固
安卓代码加固(8):构建校验
assemble 之后检查 AAR / APK:无明文 HiddenLandingPage,有 HDX1 payload 和 so。
即便依赖写对了,Manifest Merger 或某次误改仍可能把明文打进去。工程用 打包后扫描 zip 再检查一遍。assemble 成功不等于加固成功,还要经过 verify 任务。
这些任务挂在
assemble* 的 finalizedBy 上。本地编译 debug 就能看到 [hidden-dex] … 通过。校验 SDK AAR
verifyHiddenSdkAarDebug / Release 打开刚打好的 AAR:
hidden-sdk/build.gradle.kts
val payload = zip.getEntry("assets/payload/hidden.dex.enc")
?: error("$aar 缺少 assets/payload/hidden.dex.enc")
val head = zip.getInputStream(payload).use { it.readNBytes(4) }
check(head.contentEquals("HDX1".toByteArray())) {
"$aar 里的 payload 不是 AES-GCM(缺少 HDX1 头)"
}
val nativeLib = zip.getEntry("jni/arm64-v8a/libhiddendecrypt.so")
?: zip.getEntry("jni/armeabi-v7a/libhiddendecrypt.so")
check(nativeLib != null) { "$aar 缺少 libhiddendecrypt.so" }
classes.jar 再拆一次:
| 必须有 | 必须没有 |
|---|---|
com/example/minidex/sdk/HiddenSdk | com/example/minidex/hidden/HiddenLandingPage |
加密任务里还有前置检查:landing-page 的 jar 不能 带上 HiddenSdk。出现这种情况,说明有人把 compileOnly 改成了 implementation。
校验最终 APK
宿主任务 verifyHiddenLandingPageNotInApk* 扫描 classes*.dex 的原始字节,查找 UTF-8 类描述符:
app/build.gradle.kts
fun File.containsClassDescriptor(descriptor: String): Boolean {
val marker = descriptor.toByteArray(Charsets.UTF_8)
ZipFile(this).use { zip ->
val dexEntries = zip.entries().asSequence()
.filter { it.name.matches(Regex("""classes\d*\.dex""")) }
for (entry in dexEntries) {
val bytes = zip.getInputStream(entry).use { it.readBytes() }
if (bytes.indexOfSlice(marker) >= 0) return true
}
}
return false
}
通过条件:
- 没有
Lcom/example/minidex/hidden/HiddenLandingPage; - 有
Lcom/example/minidex/sdk/HiddenSdk; - 没有
Lcom/example/minidex/sdk/HiddenApi;(旧接口不得残留) assets/payload/hidden.dex.enc头是HDX1
这是字节搜索,不是完整 dex 解析。类名被 R8 改掉后描述符会变,所以公开入口必须 keep。实现类根本不该出现在主 dex,无论是否 keep,都不应搜到。
校验源码与解析图
preBuild 禁止 import com.example.minidex.hidden。configurations.afterResolve 禁止 runtime 图里出现 :hidden-landing-page。
三道检查分别拦住这些情况:
手写 import → 源码扫描
Gradle 依赖写反 → 解析期 check
漏掉的明文 class → APK / AAR zip 扫描
发布前再打包
packageDistAar 依赖 assembleRelease,因此会先跑 release 加密(必须有环境变量)和 AAR 校验,再把 AAR 拷到仓库根目录 dist/。
hidden-sdk/build.gradle.kts
tasks.register("packageDistAar") {
dependsOn("assembleRelease")
// dist/hidden-sdk-<version>.aar
}
AAR 自检确认「有加密 payload」,但 不会、也不该 在构建机用密钥解开它。密钥属于运行时接入方。