Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add spoofing of signingInfo #23

Merged
merged 1 commit into from
Dec 15, 2024
Merged

Conversation

gilbsgilbs
Copy link

@gilbsgilbs gilbsgilbs commented Dec 13, 2024

Some major Google Apps stopped relying on the deprecated packageInfo.signature field and started using
packageInfo.signingInfo instead. This commit introduces spoofing for the signingInfo field to maintain compatibility.

Unfortunately, spoofing the signingInfo field is a little more
involved than spoofing the signature field, because the former relies
on a SigningDetails class which is not part of the public SDK API [1].
Also, there is no public constructor for SigningInfo before SDK 35.
Therefore we rely on reflection to instantiate the SigningDetails
object, and to find the associated SigningInfo constructor.

The certificate string was converted from hexadecimal to base64
encoding. This change was made to include public key details in
SigningInfo, as Java requires PEM data to be loaded from a byte array.
The Android SDK provides a built-in base64 package for decoding
base64-encoded strings, which simplifies handling compared to
hexadecimal strings. Additionally, base64-encoded PEM certificates are
more commonly used.

See also:

[1] https://cs.android.com/android/platform/superproject/+/1c19b376095446666df2b2d9290dac3ef71da846:frameworks/base/core/java/android/content/pm/SigningDetails.java

@gilbsgilbs gilbsgilbs force-pushed the fix-siginfo branch 6 times, most recently from 7c9a847 to cda79e4 Compare December 13, 2024 18:20
// should be android.content.pm.SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V3
// but it is not exported.
final int SIGNING_BLOCK_V3 = 3;
pi.signingInfo = new SigningInfo(SIGNING_BLOCK_V3, Arrays.asList(new Signature(sig)), null, null);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the LineageOS patch scheme version is the second field and it also has signingkeys.
Look here: https://review.lineageos.org/c/LineageOS/android_frameworks_base/+/411386/4/services/core/java/com/android/server/pm/ComputerEngine.java

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is because they use a constructor for SigningInfo which is not publicly available from the SDK. I used this constructor in my original patch (see https://github.com/whew-inc/FakeGApps/compare/f4c434a7022bce659d5ccb29ea66276512a82000..7c9a84743949a324687c51cb81b7da386b77359e), so it is possible if needed, but I think it's preferable if it works without reflection.

I think the public keys could be added though. On it.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I admit that I'm not expert about this, but I have seen that reflection can be avoided by using aidl files that define the missing things.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm interested if there is some docs about that or another XPosed module that implements this. IIUC AIDLs are primarily used for bindings. This is not really what we want here because we need direct-calls to methods that are just hidden from typings. And even so, I'm unsure if adding the stubs would actually be less cumbersome than using reflection in the end.

I updated my PR to reuse reflection because I realized the constructor I was using wasn't available before SDK 35 which is very recent. I also added the certificate's public key to signing details, so we should be very close to LineageOS implementation.

Thanks for the review.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reflection code here is quite readable for anyone familiar with Java so my personal preference lies in this approach over something possibly more complex like AIDLs (I'm not familiar with it myself).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK the only alternative to reflection here is having a stub module used as a compileOnly dependency that declares the missing types/members.
I've found an example of this pattern in this repository made by LSPosed authors. You can see that there is a stub module that declares the hidden SDK classes and members they use with a placeholder implementation (which is not included in the APK).
If it works, I think it has the potential of being a cleaner approach. Btw great job @gilbsgilbs

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Fs00 that's a neat idea directly applicable to SigningDetails. However it gets more tricky for SigningInfo because the class exists in the SDK. We need the compiler to understand that we want to compile against our stub classes, but not actually include them in the resulting APK. I made some attempts but always failed to achieve either of these properties. With a compileOnly subproject, I am unable to supersede SDK's SigningInfo so it won't compile. Without subproject and -Xprefer:source, I am unable to exclude the classes from the resulting APK so it compiles but doesn't work.

It may be doable, but it is simply beyond my current knowledge of Gradle and highly exceeds the amount of time I am ready to spend on that. Unless somebody provides an actual proof of concept that doesn't rely on cursed gradle tricks or post-process, I'm going to stick to reflection which at least works in a remotely "standard" way.

Copy link
Author

@gilbsgilbs gilbsgilbs Dec 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up finding a way that works with stubs and doesn't look too cumbersome:

no-reflection.patch
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 52a3e35..afa606a 100755
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -6,6 +6,10 @@ plugins {
     id("com.android.application")
 }
 
+interface InjectedFsOps {
+    @get:Inject val fs: FileSystemOperations
+}
+
 android {
     namespace = "inc.whew.android.fakegapps"
     compileSdk = 35
@@ -66,9 +70,25 @@ android {
     tasks.withType<PackageAndroidArtifact> {
         doFirst { appMetadata.asFile.orNull?.writeText("") }
     }
+
+    sourceSets.getByName("main") {
+        java.srcDir("src/stub/java")
+    }
+
+    tasks.withType<JavaCompile> {
+        // don't include Android SDK stubs in APK
+        val fsOps = project.objects.newInstance<InjectedFsOps>()
+        val destDir = destinationDirectory.dir("android")
+
+        doLast {
+            fsOps.fs.delete {
+                delete(destDir)
+            }
+        }
+    }
 }
 
 dependencies {
     compileOnly("de.robv.android.xposed:api:82")
-    compileOnly("de.robv.android.xposed:api:82:sources")
+    // compileOnly("de.robv.android.xposed:api:82:sources")
 }
diff --git a/app/src/main/java/inc/whew/android/fakegapps/FakeSignatures.java b/app/src/main/java/inc/whew/android/fakegapps/FakeSignatures.java
index 46a42ab..43929a7 100755
--- a/app/src/main/java/inc/whew/android/fakegapps/FakeSignatures.java
+++ b/app/src/main/java/inc/whew/android/fakegapps/FakeSignatures.java
@@ -1,19 +1,16 @@
 package inc.whew.android.fakegapps;
 
-import android.annotation.TargetApi;
 import android.content.pm.PackageInfo;
 import android.content.pm.Signature;
+import android.content.pm.SigningDetails;
 import android.content.pm.SigningInfo;
 import android.os.Build;
-import android.util.ArraySet;
 import android.util.Base64;
 
 import java.io.ByteArrayInputStream;
-import java.lang.reflect.Constructor;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
-import java.security.PublicKey;
 
 import de.robv.android.xposed.IXposedHookLoadPackage;
 import de.robv.android.xposed.XC_MethodHook;
@@ -22,7 +19,6 @@ import de.robv.android.xposed.XposedHelpers;
 import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
 
 public class FakeSignatures implements IXposedHookLoadPackage {
-    private static final String TAG = "FakeGApps";
     private static final String _x509cert = "MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK";
 
     @Override
@@ -31,23 +27,26 @@ public class FakeSignatures implements IXposedHookLoadPackage {
             return;
 
         final byte[] certBytes = Base64.decode(_x509cert, Base64.DEFAULT);
-        final CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
-        final Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
 
         XC_MethodHook hook = new XC_MethodHook() {
             @Override
-            protected void afterHookedMethod(MethodHookParam param) {
+            protected void afterHookedMethod(MethodHookParam param) throws CertificateException {
                 PackageInfo pi = (PackageInfo) param.getResult();
                 if (pi != null) {
                     String packageName = pi.packageName;
                     if (packageName.equals("com.google.android.gms") || packageName.equals("com.android.vending")) {
-                        pi.signatures = new Signature[]{new Signature(certBytes)};
+                        final Signature[] sigs = new Signature[]{new Signature(certBytes)};
+                        pi.signatures = sigs;
 
                         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
-                            SigningInfo signingInfo = createSigningInfo(new Signature(certBytes), cert.getPublicKey());
-                            if (signingInfo != null) {
-                                pi.signingInfo = signingInfo;
-                            }
+                            pi.signingInfo = new SigningInfo(
+                                    new SigningDetails(
+                                            sigs,
+                                            SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V3,
+                                            SigningDetails.toSigningKeys(sigs),
+                                            null
+                                    )
+                            );
                         }
 
                         param.setResult(pi);
@@ -104,37 +103,4 @@ public class FakeSignatures implements IXposedHookLoadPackage {
         final Class<?> hookedClass = XposedHelpers.findClass(classToHook, loadedPackage.classLoader);
         XposedBridge.hookAllMethods(hookedClass, "generatePackageInfo", hook);
     }
-
-    @TargetApi(android.os.Build.VERSION_CODES.P)
-    private SigningInfo createSigningInfo(Signature sig, PublicKey publicKey) {
-        final int SIGNING_BLOCK_V3 = 3;
-        final Signature[] sigs = new Signature[]{sig};
-        final ArraySet<PublicKey> pks = new ArraySet<>();
-        pks.add(publicKey);
-
-        // Unfortunately, SigningDetails is not exported in SDK, so we have to rely on reflection.
-        // Also, public SigningInfo constructor is only available from API 35, so we can't use it.
-        try {
-            Class<?> signingDetailsClass = Class.forName("android.content.pm.SigningDetails");
-            // https://cs.android.com/android/platform/superproject/+/1c19b376095446666df2b2d9290dac3ef71da846:frameworks/base/core/java/android/content/pm/SigningDetails.java;l=146
-            Constructor<?> signingDetailsConstructor = signingDetailsClass.getDeclaredConstructor(
-                Signature[].class, // signatures
-                int.class, // signatureSchemeVersion
-                ArraySet.class, // keys
-                Signature[].class // pastSigningCertificates
-            );
-            Constructor<SigningInfo> signingInfoConstructor = SigningInfo.class.getDeclaredConstructor(signingDetailsClass);
-
-            signingDetailsConstructor.setAccessible(true);
-            signingInfoConstructor.setAccessible(true);
-
-            Object signingDetails = signingDetailsConstructor.newInstance(sigs, SIGNING_BLOCK_V3, pks, null);
-            return signingInfoConstructor.newInstance(signingDetails);
-        } catch (Exception e) {
-            XposedBridge.log(String.format("%s failed to create signingInfo", TAG));
-            XposedBridge.log(e);
-        }
-
-        return null;
-    }
 }
diff --git a/app/src/stub/java/android/content/pm/SigningDetails.java b/app/src/stub/java/android/content/pm/SigningDetails.java
new file mode 100755
index 0000000..df2bffe
--- /dev/null
+++ b/app/src/stub/java/android/content/pm/SigningDetails.java
@@ -0,0 +1,25 @@
+package android.content.pm;
+
+import android.util.ArraySet;
+
+import java.security.PublicKey;
+import java.security.cert.CertificateException;
+
+public class SigningDetails {
+    public @interface SignatureSchemeVersion {
+        int SIGNING_BLOCK_V3 = 3;
+    }
+
+    public SigningDetails(
+            Signature[] signatures,
+            @SignatureSchemeVersion int signatureSchemeVersion,
+            ArraySet<PublicKey> keys,
+            Signature[] pastSigningCertificates
+    ) {
+        throw new RuntimeException("Stub!");
+    }
+
+    public static ArraySet<PublicKey> toSigningKeys(Signature[] signatures) throws CertificateException {
+        throw new RuntimeException("Stub!");
+    }
+}
diff --git a/app/src/stub/java/android/content/pm/SigningInfo.java b/app/src/stub/java/android/content/pm/SigningInfo.java
new file mode 100755
index 0000000..d4ef157
--- /dev/null
+++ b/app/src/stub/java/android/content/pm/SigningInfo.java
@@ -0,0 +1,7 @@
+package android.content.pm;
+
+public class SigningInfo {
+    public SigningInfo(SigningDetails signingDetails) {
+        throw new RuntimeException("Stub!");
+    }
+}

In summary, this approach builds the stubs but hooks into the compilation task to remove the stub class files just after. This ensures that the stubs are not part of the final output.

While this method seems more convenient and slightly more performant, I’ve decided not to update this PR with it. I believe reflection remains more robust: if Android ever changes or removes these private APIs (which they are entitled to), this stub-based approach would no longer work, and we’d have to revert back to using reflection. This app should remain decoupled from a specific SDK implementation and instead detect the correct behavior at runtime, so reflection just fundamentally makes more sense IMHO.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Impressive attempt! After reading your first reply I thought it wasn't possible to use only stubs in this case, but you've found a way nevertheless (although quite hacky). After all, it's probably better to stick to reflection than using black Gradle magic to alter the build process.
I'm not too convinced about the difference in robustness between the two approaches, but at this point I think we can all agree that both have their drawbacks.

@gilbsgilbs gilbsgilbs force-pushed the fix-siginfo branch 2 times, most recently from c31e2b1 to d297cd1 Compare December 13, 2024 20:33
@whew-inc
Copy link
Owner

Thank you so much @gilbsgilbs for your hard work. I'm unavailable today so I'll try to review your change request tomorrow.

@gilbsgilbs
Copy link
Author

gilbsgilbs commented Dec 13, 2024

I'm unavailable today so I'll try to review your change request tomorrow.

No worries 🙂. Thanks for your time.

If anyone wants to test this out, you can download the APKs from my CI job.

@ale5000-git
Copy link

The "things" returned by reflection can be just requested the first time and cached instead of doing everything at every request?

@gilbsgilbs
Copy link
Author

The "things" returned by reflection can be just requested the first time and cached instead of doing everything at every request?

It could, but I believe that would be premature optimization. More importantly, it would harm readability by adding one level of indirection: instead of seeing all the reflected fields in one place, you'd need to trace how each cached field is initialized.

Some major Google Apps stopped relying on the deprecated
`packageInfo.signature` field and started using
`packageInfo.signingInfo` instead. This commit introduces spoofing for
the `signingInfo` field to maintain compatibility.

Unfortunately, spoofing the `signingInfo` field is a little more
involved than spoofing the `signature` field, because the former relies
on a `SigningDetails` class which is not part of the public SDK API [1].
Also, there is no public constructor for `SigningInfo` before SDK 35.
Therefore we rely on reflection to instantiate the `SigningDetails`
object, and to find the associated `SigningInfo` constructor.

The certificate string was converted from hexadecimal to base64
encoding. This change was made to include public key details in
`SigningInfo`, as Java requires PEM data to be loaded from a byte array.
The Android SDK provides a built-in base64 package for decoding
base64-encoded strings, which simplifies handling compared to
hexadecimal strings. Additionally, base64-encoded PEM certificates are
more commonly used.

See also:

- microg/GmsCore#2680
- https://gitlab.e.foundation/e/os/android_frameworks_base/-/commit/2b9c74a4409f41924905c4b28aa900904e442992
- https://review.lineageos.org/c/LineageOS/android_frameworks_base/+/411374
@whew-inc whew-inc merged commit e9a88b4 into whew-inc:master Dec 15, 2024
@gilbsgilbs gilbsgilbs deleted the fix-siginfo branch December 15, 2024 15:26
@gilbsgilbs
Copy link
Author

Thanks for the prompt review and release @whew-inc . Much appreciated.

@Fs00
Copy link

Fs00 commented Dec 15, 2024

I've just tested the new version on Android 11 but unfortunately it didn't work.
The error I get in the logcat is the following (excerpt):

java.lang.ClassNotFoundException: android.content.pm.SigningDetails
	at java.lang.Class.classForName(Native Method)
	at java.lang.Class.forName(Class.java:454)
	at java.lang.Class.forName(Class.java:379)
	at inc.whew.android.fakegapps.FakeSignatures.createSigningInfo(FakeSignatures.java:118)
...

It turns out that on Android 9-12 the SigningDetails class is nested into the PackageParser class, so on those SDK levels the correct class name to use should be android.content.pm.PackageParser.SigningDetails (you can see the difference in A12L SigningInfo class vs A13 one).

@gilbsgilbs
Copy link
Author

Well, I should probably have checked older Android versions earlier, thanks for the detailed report and references. At least it justifies usage of reflection, because I don't think we would be able to stub that (this is exactly the case I described here). I attempted a fix here: #25 , any test would be highly appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants