diff --git a/app/src/main/java/dev/rushii/xspoofsignatures/Main.java b/app/src/main/java/dev/rushii/xspoofsignatures/Main.java index a0149ce..e825a23 100644 --- a/app/src/main/java/dev/rushii/xspoofsignatures/Main.java +++ b/app/src/main/java/dev/rushii/xspoofsignatures/Main.java @@ -1,8 +1,16 @@ package dev.rushii.xspoofsignatures; import android.content.pm.*; +import android.content.res.AssetManager; +import android.content.res.Resources; import android.os.Build; +import android.os.Bundle; import android.util.Log; +import android.util.TypedValue; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; import de.robv.android.xposed.*; import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam; @@ -26,22 +34,101 @@ private static Signature[] copySignatures(Signature[] orig, Signature extra) { return signatures; } - private static String getHookClassName() { + // Values of manifest tags may be resource references + // (e.g. microG declares android:value="@string/fake_signature"), in which case + // the framework stores an Integer resource ID instead of the resolved value. + private static final Map sResourcesCache = new HashMap<>(); + + private static Resources getResourcesForApplication(PackageInfo pi) { + if (pi.applicationInfo == null) return null; + + String path = pi.applicationInfo.publicSourceDir != null + ? pi.applicationInfo.publicSourceDir : pi.applicationInfo.sourceDir; + if (path == null) return null; + + synchronized (sResourcesCache) { + if (sResourcesCache.containsKey(path)) return sResourcesCache.get(path); + + Resources resources = null; + try { + AssetManager assets = AssetManager.class.newInstance(); + Method addAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); + addAssetPath.setAccessible(true); + addAssetPath.invoke(assets, path); + resources = new Resources(assets, + Resources.getSystem().getDisplayMetrics(), + Resources.getSystem().getConfiguration()); + } catch (Throwable t) { + Log.w(TAG, "Failed to load resources of " + path, t); + } + + sResourcesCache.put(path, resources); + return resources; + } + } + + private static Object getMetaDataValue(PackageInfo pi, String key) { + Bundle metaData = pi.applicationInfo.metaData; + Object value = metaData.get(key); + + if (!(value instanceof Integer) || (Integer) value == 0) return value; + + Resources resources = getResourcesForApplication(pi); + if (resources == null) return null; + + try { + TypedValue typedValue = new TypedValue(); + resources.getValue((Integer) value, typedValue, true); + + switch (typedValue.type) { + case TypedValue.TYPE_STRING: + return resources.getString((Integer) value); + case TypedValue.TYPE_INT_BOOLEAN: + return typedValue.data != 0; + case TypedValue.TYPE_INT_DEC: + case TypedValue.TYPE_INT_HEX: + return typedValue.data; + default: + return typedValue.coerceToString(); + } + } catch (Throwable t) { + Log.w(TAG, "Failed to resolve meta-data " + key + " of " + pi.packageName, t); + return null; + } + } + + // Classes that can hand out a PackageInfo to callers. The first entry of each + // group is the one that builds the PackageInfo, the second one (when present) + // is the binder entry point that returns it to other processes. Both have to + // be hooked: on some ROMs the result produced by the "engine" class is not the + // object that the caller receives. + private static String[] getHookClassNames() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // https://cs.android.com/android/platform/superproject/+/android-15.0.0_r1:frameworks/base/services/core/java/com/android/server/pm/ComputerEngine.java;l=1484;drc=d970c566017e2c4a69e545775994fc46e0869247 - return "com.android.server.pm.ComputerEngine"; + // https://cs.android.com/android/platform/superproject/+/android-13.0.0_r1:frameworks/base/services/core/java/com/android/server/pm/IPackageManagerBase.java + return new String[]{ + "com.android.server.pm.ComputerEngine", + "com.android.server.pm.IPackageManagerBase", + }; } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java;l=3302;drc=2cf61babf8de1e5e3a45770632fa067556021291 - return "com.android.server.pm.PackageManagerService$ComputerEngine"; + return new String[]{ + "com.android.server.pm.PackageManagerService$ComputerEngine", + "com.android.server.pm.PackageManagerService$IPackageManagerImpl", + }; } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { // https://cs.android.com/android/platform/superproject/+/android-4.0.1_r1:frameworks/base/services/java/com/android/server/pm/PackageManagerService.java;l=1485;drc=58f42a59bda3bc912d0d2f81dc65a9d31d140eaa - return "com.android.server.pm.PackageManagerService"; + return new String[]{ + "com.android.server.pm.PackageManagerService", + }; } else { // Android SRC is unavailable for Android Honeycomb (3.x) and Android Cupcake (1.5) // but I confirmed this is accurate for those versions by extracting the system image // https://cs.android.com/android/platform/superproject/+/android-2.2.3_r1:frameworks/base/services/java/com/android/server/PackageManagerService.java;l=1316;drc=e2fd45af93178b30e6da97b46fcd31b7d30f5426 - return "com.android.server.PackageManagerService"; + return new String[]{ + "com.android.server.PackageManagerService", + }; } } @@ -81,12 +168,10 @@ protected void afterHookedMethod(MethodHookParam param) { if (!isFetchingSignatures(flags)) return; // Get the declared fake signature from manifest meta-data - String fakeSig; - if (pi.applicationInfo == null || - pi.applicationInfo.metaData == null || - (fakeSig = pi.applicationInfo.metaData.getString("fake-signature")) == null) { - return; - } + if (pi.applicationInfo == null || pi.applicationInfo.metaData == null) return; + Object fakeSigValue = getMetaDataValue(pi, "fake-signature"); + if (!(fakeSigValue instanceof CharSequence)) return; + String fakeSig = fakeSigValue.toString(); // Check if the permission was granted if (pi.requestedPermissions == null || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && @@ -102,7 +187,8 @@ protected void afterHookedMethod(MethodHookParam param) { if (!granted) return; // Check if whether to preserve the other signatures - boolean makeSoleSigner = pi.applicationInfo.metaData.getBoolean("fake-signature-only", true); + Object soleSignerValue = getMetaDataValue(pi, "fake-signature-only"); + boolean makeSoleSigner = !(soleSignerValue instanceof Boolean) || (Boolean) soleSignerValue; Log.d(TAG, "Spoofing signature for " + pi.packageName); @@ -118,38 +204,63 @@ protected void afterHookedMethod(MethodHookParam param) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && pi.signingInfo != null) { Signature[] origSignatures = pi.signingInfo.getApkContentsSigners(); Signature sig = new Signature(fakeSig); + Signature[] newSignatures = makeSoleSigner + ? new Signature[]{ sig } + : copySignatures(origSignatures, sig); + + Object signingDetails = XposedHelpers.getObjectField(pi.signingInfo, "mSigningDetails"); - if (makeSoleSigner && origSignatures.length == 1) { - origSignatures[0] = sig; + String signaturesField; + String historyField; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // SigningDetails#mSignatures (Signature[]) + // SigningDetails#mPastSigningCertificates (Signature[]) + signaturesField = "mSignatures"; + historyField = "mPastSigningCertificates"; } else { - Object signingDetails = XposedHelpers.getObjectField(pi.signingInfo, "mSigningDetails"); - Signature[] newSignatures; - - if (makeSoleSigner) { - newSignatures = new Signature[]{ sig }; - } else { - // SigningInfo#mSigningDetails (SigningDetails) - newSignatures = copySignatures(origSignatures, sig); - } - - String targetField; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - // SigningDetails#mSignatures (Signature[]) - targetField = "mSignatures"; - } else { - // SV2 and below - // PackageParser$SigningDetails#signatures (Signature[]) - targetField = "signatures"; - } - XposedHelpers.setObjectField(signingDetails, targetField, newSignatures); + // SV2 and below + // PackageParser$SigningDetails#signatures (Signature[]) + // PackageParser$SigningDetails#pastSigningCertificates (ArraySet) + signaturesField = "signatures"; + historyField = "pastSigningCertificates"; + } + + // SigningInfo#getApkContentsSigners() returns a copy of the underlying array, + // so the field itself has to be overwritten instead of the returned array + XposedHelpers.setObjectField(signingDetails, signaturesField, newSignatures); + + // Keep the signing certificate history in sync, otherwise + // SigningInfo#getSigningCertificateHistory() still exposes the original certificate + Object history = XposedHelpers.getObjectField(signingDetails, historyField); + if (history instanceof Signature[]) { + XposedHelpers.setObjectField(signingDetails, historyField, newSignatures); } } } }; - final String hookClassName = getHookClassName(); - final Class hookClass = XposedHelpers.findClass(hookClassName, lpparam.classLoader); - XposedBridge.hookAllMethods(hookClass, "generatePackageInfo", hook); - Log.d(TAG, String.format("Hooking all %s#generatePackageInfo(...)", hookClassName)); + hookPackageMethods(lpparam.classLoader, hook); + } + + private static void hookPackageMethods(ClassLoader classLoader, XC_MethodHook hook) { + String[] methodNames = {"generatePackageInfo", "getPackageInfo", "getPackageInfoVersioned"}; + + for (String className : getHookClassNames()) { + for (String methodName : methodNames) { + hookAllMethodsSafe(classLoader, className, methodName, hook); + } + } + } + + // Hook every method with the given name, the class and the methods it contains + // differ between Android versions and ROMs + private static void hookAllMethodsSafe(ClassLoader classLoader, String className, String methodName, XC_MethodHook hook) { + try { + Class hookClass = XposedHelpers.findClass(className, classLoader); + XposedBridge.hookAllMethods(hookClass, methodName, hook); + Log.d(TAG, String.format("Hooking all %s#%s(...)", className, methodName)); + } catch (Throwable t) { + Log.d(TAG, String.format("Cannot hook %s#%s(...)", className, methodName)); + } } }