-
Notifications
You must be signed in to change notification settings - Fork 19
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
Conversation
7c9a847
to
cda79e4
Compare
// 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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
c31e2b1
to
d297cd1
Compare
Thank you so much @gilbsgilbs for your hard work. 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. |
d297cd1
to
42ca139
Compare
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
42ca139
to
7689158
Compare
Thanks for the prompt review and release @whew-inc . Much appreciated. |
I've just tested the new version on Android 11 but unfortunately it didn't work.
It turns out that on Android 9-12 the |
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. |
Some major Google Apps stopped relying on the deprecated
packageInfo.signature
field and started usingpackageInfo.signingInfo
instead. This commit introduces spoofing for thesigningInfo
field to maintain compatibility.Unfortunately, spoofing the
signingInfo
field is a little moreinvolved than spoofing the
signature
field, because the former relieson 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