安卓代码加固
安卓代码加固(7):依赖隔离
宿主只 implementation hidden-sdk。landing-page 对 SDK 必须 compileOnly,明文 class 不准进入 APK。
即使已经加密,只要宿主使用 implementation(project(":hidden-landing-page")),明文 class 仍会进入 APK。加固的第一道检查是 依赖图,不是算法。
:hidden-sdk 只作为 d8 的 --lib,接口留在主 DEX。实现进入加密 dex。两边不要互相 implementation。模块边界
settings.gradle.kts
include(":app")
include(":hidden-sdk")
include(":hidden-landing-page")
include(":ads")
| 写法 | 结果 |
|---|---|
宿主 implementation(hidden-sdk) | 正确。接口在主 dex,实现在 AAR assets |
宿主 implementation(hidden-landing-page) | 明文 class 进入 APK |
landing-page implementation(:hidden-sdk) | SDK 类打进加密 dex,体积变大且无必要 |
landing-page compileOnly(:hidden-sdk) | 本仓库内部正确 |
宿主 import com.example.minidex.hidden | 编译期把实现引用写进主 dex |
强制 compileOnly
landing-page 模块的头注释和依赖固定了方向:
hidden-landing-page/build.gradle.kts
// 不要被宿主以 implementation / api / runtimeOnly 依赖,否则明文会进 APK。
// 对 :hidden-sdk 只能 compileOnly:接口进宿主,实现进加密 dex。
dependencies {
compileOnly(project(":hidden-sdk"))
}
它还需要 SDK 里的 HiddenIncomingKeepAlive。Service 和 Receiver 必须留在主 DEX,供系统拉起。compileOnly 让编译期能解析这些符号,运行时由宿主 ClassLoader 提供,class 不会打进 jar。
解析期再加一层检查:运行时相关的 configuration 里如果出现 :hidden-sdk,直接失败:
hidden-landing-page/build.gradle.kts
configurations.configureEach {
val runtimeLike = configurationName.contains("Implementation") ||
configurationName.contains("Runtime") ||
configurationName == "api" ||
configurationName.endsWith("Api")
if (!runtimeLike) return@configureEach
incoming.afterResolve {
check(leaked.isEmpty()) {
":hidden-landing-page 把 :hidden-sdk 放进了 $configurationName。必须 compileOnly。"
}
}
}
:app 做对称检查:任何 runtime configuration 解析到 :hidden-landing-page 就失败。
禁止源码 import
即使没有写 Gradle 依赖,只要有人把实现类拷进 app 源码并 import,主 dex 仍会留下类描述符。preBuild 会扫描全部 kt/java 文件:
app/build.gradle.kts
tasks.register("verifyAppDoesNotImportHiddenLandingPage") {
doLast {
val forbidden = Regex("""import\s+com\.example\.minidex\.hidden\b""")
check(offenders.isEmpty()) {
":app 不能 import hidden-landing-page,请走 HiddenSdk.invoke。"
}
}
}
tasks.named("preBuild").configure {
dependsOn("verifyAppDoesNotImportHiddenLandingPage")
}
宿主只允许 com.example.minidex.sdk.*。
保留必须 keep 的入口
AAR 带有 consumer-rules.pro。宿主开启 R8 时,不能把 JNI 入口和系统要反射的占位类 shrink 掉:
hidden-sdk/consumer-rules.pro
-keep class com.example.minidex.sdk.HiddenSdk { *; }
-keep class com.example.minidex.sdk.HiddenInitResult { *; }
-keep class com.example.minidex.sdk.HiddenInitResult$Code { *; }
-keep class com.example.minidex.sdk.internal.HiddenIncomingKeepAlive { *; }
-keep class com.example.minidex.sdk.internal.HiddenIncomingKeepAliveService { *; }
-keep class com.example.minidex.sdk.internal.HiddenIncomingAlarmReceiver { *; }
-keep class com.example.minidex.sdk.internal.HiddenDecrypt { *; }
-keep class com.example.minidex.sdk.internal.HiddenCrypto { *; }
HiddenSdk 的 nativeInit / nativeDispatch 按类名 + 方法名绑定。R8 改名后 so 会 UnsatisfiedLinkError。KeepAlive 相关类由系统 Intent 拉起,也必须留名。
这些规则是为了保留入口,不是把实现 keep 进主 DEX。实现类本来就不在
classes.jar 里。复用时不要拆 AAR
其他工程 不要 include(":hidden-landing-page"),也不要解压 AAR 再单独拷贝 payload。
dependencies {
implementation(files("libs/hidden-sdk-1.0.0.aar"))
}
implementation("com.example.minidex:hidden-sdk:1.0.0")
includeBuild("../hidden-dex-minidemo") {
dependencySubstitution {
substitute(module("com.example.minidex:hidden-sdk"))
.using(project(":hidden-sdk"))
}
}