Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 149 additions & 38 deletions app/src/main/java/dev/rushii/xspoofsignatures/Main.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -26,22 +34,101 @@ private static Signature[] copySignatures(Signature[] orig, Signature extra) {
return signatures;
}

private static String getHookClassName() {
// Values of manifest <meta-data> 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<String, Resources> 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",
};
}
}

Expand Down Expand Up @@ -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 &&
Expand All @@ -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);

Expand All @@ -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));
}
}
}