From b0416cd3aa49dc6c9e4bc30af17729cb384b4545 Mon Sep 17 00:00:00 2001 From: Danny Lin Date: Sun, 22 Aug 2021 20:36:09 -0700 Subject: [PATCH] java: Initial implementation of high-level module payload R8 minification is essential for this, because otherwise the Kotlin standard library is too big and results in the app compiling to multiple dex files. It's not impossible to load multiple dex buffers, but let's keep it simple here. --- java_module/app/build.gradle | 7 +- java_module/app/proguard-rules.pro | 10 ++- .../dev/kdrag0n/safetynetriru/EntryPoint.kt | 16 +++++ .../kdrag0n/safetynetriru/ProxyKeyStoreSpi.kt | 72 +++++++++++++++++++ .../kdrag0n/safetynetriru/ProxyProvider.kt | 27 +++++++ .../kdrag0n/safetynetriru/SecurityBridge.kt | 26 +++++++ .../java/dev/kdrag0n/safetynetriru/Utils.kt | 24 +++++++ 7 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 java_module/app/src/main/java/dev/kdrag0n/safetynetriru/EntryPoint.kt create mode 100644 java_module/app/src/main/java/dev/kdrag0n/safetynetriru/ProxyKeyStoreSpi.kt create mode 100644 java_module/app/src/main/java/dev/kdrag0n/safetynetriru/ProxyProvider.kt create mode 100644 java_module/app/src/main/java/dev/kdrag0n/safetynetriru/SecurityBridge.kt create mode 100644 java_module/app/src/main/java/dev/kdrag0n/safetynetriru/Utils.kt diff --git a/java_module/app/build.gradle b/java_module/app/build.gradle index 98da551..757c301 100644 --- a/java_module/app/build.gradle +++ b/java_module/app/build.gradle @@ -18,8 +18,9 @@ android { buildTypes { release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.debug } } compileOptions { @@ -32,7 +33,5 @@ android { } dependencies { - implementation 'androidx.core:core-ktx:1.6.0' - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'com.google.android.material:material:1.4.0' + implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.5.21' } diff --git a/java_module/app/proguard-rules.pro b/java_module/app/proguard-rules.pro index 481bb43..4667fda 100644 --- a/java_module/app/proguard-rules.pro +++ b/java_module/app/proguard-rules.pro @@ -18,4 +18,12 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +-keep class dev.kdrag0n.safetynetriru.EntryPoint { + public static void init(); +} + +-keepclassmembers class dev.kdrag0n.safetynetriru.ProxyKeyStoreSpi { + public (...); +} diff --git a/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/EntryPoint.kt b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/EntryPoint.kt new file mode 100644 index 0000000..625487a --- /dev/null +++ b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/EntryPoint.kt @@ -0,0 +1,16 @@ +package dev.kdrag0n.safetynetriru + +@Suppress("unused") +object EntryPoint { + @JvmStatic + fun init() { + runCatching { + logDebug("Entry point: Initializing SafetyNet patch") + SecurityBridge.init() + }.recoverCatching { e -> + // Throwing an exception would require the JNI code to handle exceptions, so just catch + // everything here. + logDebug("Error in entry point", e) + } + } +} diff --git a/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/ProxyKeyStoreSpi.kt b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/ProxyKeyStoreSpi.kt new file mode 100644 index 0000000..79e393e --- /dev/null +++ b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/ProxyKeyStoreSpi.kt @@ -0,0 +1,72 @@ +package dev.kdrag0n.safetynetriru + +import java.io.InputStream +import java.io.OutputStream +import java.security.Key +import java.security.KeyStoreSpi +import java.security.cert.Certificate +import java.util.* + +class ProxyKeyStoreSpi private constructor( + private val orig: KeyStoreSpi, +) : KeyStoreSpi() { + @Suppress("unused") + constructor() : this(androidImpl!!) + + init { + logDebug("Init proxy KeyStore SPI") + } + + // Avoid breaking other, legitimate uses of key attestation in Google Play Services, e.g. + // - com.google.android.gms.auth.cryptauth.register.ReEnrollmentChimeraService + // - tk_trace.129-RegisterForKeyPairOperation + private fun isCallerSafetyNet() = Thread.currentThread().stackTrace.any { + // a.a.engineGetCertificateChain(Unknown Source:15) + // java.security.KeyStore.getCertificateChain(KeyStore.java:1087) + // com.google.ccc.abuse.droidguard.DroidGuard.initNative(Native Method) + // com.google.ccc.abuse.droidguard.DroidGuard.init(DroidGuard.java:447) + // java.lang.reflect.Method.invoke(Native Method) + // xvq.b(:com.google.android.gms@212621053@21.26.21 (190400-387928701):1) + // xuc.a(:com.google.android.gms@212621053@21.26.21 (190400-387928701):5) + // xuc.eX(:com.google.android.gms@212621053@21.26.21 (190400-387928701):1) + // dzx.onTransact(:com.google.android.gms@212621053@21.26.21 (190400-387928701):8) + // android.os.Binder.execTransactInternal(Binder.java:1179) + // android.os.Binder.execTransact(Binder.java:1143) + logDebug("Stack trace element: $it") + it.className.contains("DroidGuard", ignoreCase = true) + } + + override fun engineGetCertificateChain(alias: String?): Array? { + logDebug("Proxy key store: get certificate chain") + + if (isCallerSafetyNet()) { + logDebug("Blocking call") + throw UnsupportedOperationException() + } else { + logDebug("Allowing call") + return orig.engineGetCertificateChain(alias) + } + } + + // Direct delegation. We have to do this manually because the Kotlin compiler can only do it + // for interfaces, not abstract classes. + override fun engineGetKey(alias: String?, password: CharArray?): Key? = orig.engineGetKey(alias, password) + override fun engineGetCertificate(alias: String?): Certificate? = orig.engineGetCertificate(alias) + override fun engineGetCreationDate(alias: String?): Date? = orig.engineGetCreationDate(alias) + override fun engineSetKeyEntry(alias: String?, key: Key?, password: CharArray?, chain: Array?) = orig.engineSetKeyEntry(alias, key, password, chain) + override fun engineSetKeyEntry(alias: String?, key: ByteArray?, chain: Array?) = orig.engineSetKeyEntry(alias, key, chain) + override fun engineSetCertificateEntry(alias: String?, cert: Certificate?) = orig.engineSetCertificateEntry(alias, cert) + override fun engineDeleteEntry(alias: String?) = orig.engineDeleteEntry(alias) + override fun engineAliases(): Enumeration? = orig.engineAliases() + override fun engineContainsAlias(alias: String?) = orig.engineContainsAlias(alias) + override fun engineSize() = orig.engineSize() + override fun engineIsKeyEntry(alias: String?) = orig.engineIsKeyEntry(alias) + override fun engineIsCertificateEntry(alias: String?) = orig.engineIsCertificateEntry(alias) + override fun engineGetCertificateAlias(cert: Certificate?): String? = orig.engineGetCertificateAlias(cert) + override fun engineStore(stream: OutputStream?, password: CharArray?) = orig.engineStore(stream, password) + override fun engineLoad(stream: InputStream?, password: CharArray?) = orig.engineLoad(stream, password) + + companion object { + @Volatile internal var androidImpl: KeyStoreSpi? = null + } +} diff --git a/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/ProxyProvider.kt b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/ProxyProvider.kt new file mode 100644 index 0000000..3644e0b --- /dev/null +++ b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/ProxyProvider.kt @@ -0,0 +1,27 @@ +package dev.kdrag0n.safetynetriru + +import java.security.Provider + +// This is mostly just a pass-through provider that exists to change the provider's ClassLoader. +// This works because Service looks up the class by name from the *provider* ClassLoader, not +// necessarily the bootstrap one. +class ProxyProvider( + orig: Provider, +) : Provider(orig.name, orig.version, orig.info) { + init { + logDebug("Init proxy provider - wrapping $orig") + + putAll(orig) + this["KeyStore.${SecurityBridge.PROVIDER_NAME}"] = ProxyKeyStoreSpi::class.java.name + } + + override fun getService(type: String?, algorithm: String?): Service? { + logDebug("Provider: get service - type=$type algorithm=$algorithm") + return super.getService(type, algorithm) + } + + override fun getServices(): MutableSet? { + logDebug("Get services") + return super.getServices() + } +} diff --git a/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/SecurityBridge.kt b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/SecurityBridge.kt new file mode 100644 index 0000000..b90f273 --- /dev/null +++ b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/SecurityBridge.kt @@ -0,0 +1,26 @@ +package dev.kdrag0n.safetynetriru + +import java.security.KeyStore +import java.security.KeyStoreSpi +import java.security.Security + +internal object SecurityBridge { + const val PROVIDER_NAME = "AndroidKeyStore" + + fun init() { + logDebug("Initializing SecurityBridge") + + val realProvider = Security.getProvider(PROVIDER_NAME) + val realKeystore = KeyStore.getInstance(PROVIDER_NAME) + val realSpi = realKeystore.get("keyStoreSpi") + logDebug("Real provider=$realProvider, keystore=$realKeystore, spi=$realSpi") + + val provider = ProxyProvider(realProvider) + logDebug("Removing real provider") + Security.removeProvider("AndroidKeyStore") + logDebug("Inserting provider $provider") + Security.insertProviderAt(provider, 1) + ProxyKeyStoreSpi.androidImpl = realSpi + logDebug("Security hooks installed") + } +} diff --git a/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/Utils.kt b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/Utils.kt new file mode 100644 index 0000000..d8600d5 --- /dev/null +++ b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/Utils.kt @@ -0,0 +1,24 @@ +package dev.kdrag0n.safetynetriru + +import android.util.Log + +private const val DEBUG = true +private const val TAG = "SafetyNetRiru/SARU" + +internal fun Any.get(name: String) = this::class.java.getDeclaredField(name).let { field -> + field.isAccessible = true + @Suppress("unchecked_cast") + field.get(this) as T +} + +internal fun logDebug(msg: String) { + if (DEBUG) { + Log.d(TAG, msg) + } +} + +internal fun logDebug(msg: String, e: Throwable) { + if (DEBUG) { + Log.d(TAG, msg, e) + } +}