ArturoYi
安卓代码加固

安卓代码加固(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/HiddenSdkcom/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
}

通过条件:

  1. 没有 Lcom/example/minidex/hidden/HiddenLandingPage;
  2. 有 Lcom/example/minidex/sdk/HiddenSdk;
  3. 没有 Lcom/example/minidex/sdk/HiddenApi;(旧接口不得残留)
  4. 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」,但 不会、也不该 在构建机用密钥解开它。密钥属于运行时接入方。

下一步

Copyright © 2026