Generate class mapping at runtime

This commit is contained in:
topjohnwu 2022-02-13 06:22:42 -08:00
parent a4aa4a91a3
commit c09b4dabc4
7 changed files with 199 additions and 125 deletions

View File

@ -74,13 +74,6 @@ fun genKeyData(keysDir: File, outSrc: File) {
} }
fun genStubManifest(srcDir: File, outDir: File): String { fun genStubManifest(srcDir: File, outDir: File): String {
class Component(
val real: String,
val stub: String,
val xml: String,
val genClass: Boolean = false
)
outDir.deleteRecursively() outDir.deleteRecursively()
val mainPkgDir = File(outDir, "com/topjohnwu/magisk") val mainPkgDir = File(outDir, "com/topjohnwu/magisk")
@ -88,25 +81,9 @@ fun genStubManifest(srcDir: File, outDir: File): String {
fun String.ind(level: Int) = replaceIndentByMargin(" ".repeat(level)) fun String.ind(level: Int) = replaceIndentByMargin(" ".repeat(level))
val cmpList = mutableListOf<Component>() val cmpList = mutableListOf<String>()
cmpList.add(Component( cmpList.add(
"androidx.core.app.CoreComponentFactory",
"DelegateComponentFactory",
"",
true
))
cmpList.add(Component(
"com.topjohnwu.magisk.core.App",
"DelegateApplication",
"",
true
))
cmpList.add(Component(
"com.topjohnwu.magisk.core.Provider",
"dummy.DummyProvider",
""" """
|<provider |<provider
| android:name="%s" | android:name="%s"
@ -114,11 +91,9 @@ fun genStubManifest(srcDir: File, outDir: File): String {
| android:directBootAware="true" | android:directBootAware="true"
| android:exported="false" | android:exported="false"
| android:grantUriPermissions="true" />""".ind(2) | android:grantUriPermissions="true" />""".ind(2)
)) )
cmpList.add(Component( cmpList.add(
"com.topjohnwu.magisk.core.Receiver",
"dummy.DummyReceiver",
""" """
|<receiver |<receiver
| android:name="%s" | android:name="%s"
@ -135,11 +110,9 @@ fun genStubManifest(srcDir: File, outDir: File): String {
| <data android:scheme="package" /> | <data android:scheme="package" />
| </intent-filter> | </intent-filter>
|</receiver>""".ind(2) |</receiver>""".ind(2)
)) )
cmpList.add(Component( cmpList.add(
"com.topjohnwu.magisk.ui.MainActivity",
"DownloadActivity",
""" """
|<activity |<activity
| android:name="%s" | android:name="%s"
@ -149,11 +122,9 @@ fun genStubManifest(srcDir: File, outDir: File): String {
| <category android:name="android.intent.category.LAUNCHER" /> | <category android:name="android.intent.category.LAUNCHER" />
| </intent-filter> | </intent-filter>
|</activity>""".ind(2) |</activity>""".ind(2)
)) )
cmpList.add(Component( cmpList.add(
"com.topjohnwu.magisk.ui.surequest.SuRequestActivity",
"",
""" """
|<activity |<activity
| android:name="%s" | android:name="%s"
@ -167,26 +138,22 @@ fun genStubManifest(srcDir: File, outDir: File): String {
| <category android:name="android.intent.category.DEFAULT"/> | <category android:name="android.intent.category.DEFAULT"/>
| </intent-filter> | </intent-filter>
|</activity>""".ind(2) |</activity>""".ind(2)
)) )
cmpList.add(Component( cmpList.add(
"com.topjohnwu.magisk.core.download.DownloadService",
"",
""" """
|<service |<service
| android:name="%s" | android:name="%s"
| android:exported="false" />""".trimIndent().ind(2) | android:exported="false" />""".ind(2)
)) )
cmpList.add(Component( cmpList.add(
"com.topjohnwu.magisk.core.JobService",
"",
""" """
|<service |<service
| android:name="%s" | android:name="%s"
| android:exported="false" | android:exported="false"
| android:permission="android.permission.BIND_JOB_SERVICE" />""".ind(2) | android:permission="android.permission.BIND_JOB_SERVICE" />""".ind(2)
)) )
val names = mutableListOf<String>() val names = mutableListOf<String>()
names.addAll(c1) names.addAll(c1)
@ -194,33 +161,38 @@ fun genStubManifest(srcDir: File, outDir: File): String {
names.addAll(c3.subList(0, 10)) names.addAll(c3.subList(0, 10))
names.shuffle(RANDOM) names.shuffle(RANDOM)
val pkgNames = names.subList(0, 50) val pkgNames = names
// Distinct by lower case to support case insensitive file systems // Distinct by lower case to support case insensitive file systems
.distinctBy { it.toLowerCase(Locale.ROOT) } .distinctBy { it.toLowerCase(Locale.ROOT) }
// Old Android does not support capitalized package names // Old Android does not support capitalized package names
// Check Android 7.0.0 PackageParser#buildClassName // Check Android 7.0.0 PackageParser#buildClassName
.map { it.decapitalize(Locale.ROOT) } .map { it.decapitalize(Locale.ROOT) }
var idx = 0
fun isJavaKeyword(name: String) = when (name) { fun isJavaKeyword(name: String) = when (name) {
"do", "if", "for", "int", "new", "try" -> true "do", "if", "for", "int", "new", "try" -> true
else -> false else -> false
} }
fun genCmpName() : String { val cmps = mutableListOf<String>()
var pkgName : String val usedNames = mutableListOf<String>()
fun genCmpName(): String {
var pkgName: String
do { do {
pkgName = pkgNames[idx++] pkgName = pkgNames.random(kRANDOM)
} while (isJavaKeyword(pkgName)) } while (isJavaKeyword(pkgName))
var clzName : String var clzName: String
do { do {
clzName = names.random(kRANDOM) clzName = names.random(kRANDOM)
} while (isJavaKeyword(clzName)) } while (isJavaKeyword(clzName))
return "${pkgName}.${clzName}" val cmp = "${pkgName}.${clzName}"
usedNames.add(cmp)
return cmp
} }
fun genClass(clzName: String, type: String) { fun genClass(type: String) {
val clzName = genCmpName()
val (pkg, name) = clzName.split('.') val (pkg, name) = clzName.split('.')
val pkgDir = File(outDir, pkg) val pkgDir = File(outDir, pkg)
pkgDir.mkdir() pkgDir.mkdir()
@ -230,41 +202,20 @@ fun genStubManifest(srcDir: File, outDir: File): String {
} }
} }
val cmps = mutableListOf<String>() // Generate 2 non redirect-able classes
val usedNames = mutableListOf<String>() genClass("DelegateComponentFactory")
val maps = StringBuilder() genClass("DelegateApplication")
for (gen in cmpList) { for (gen in cmpList) {
val name = genCmpName() val name = genCmpName()
usedNames.add(name) cmps.add(gen.format(name))
maps.append("|map.put(\"$name\", \"${gen.real}\");".ind(2))
maps.append('\n')
if (gen.stub.isNotEmpty()) {
if (gen.stub != "DelegateComponentFactory") {
maps.append("|internalMap.put(\"$name\", com.topjohnwu.magisk.${gen.stub}.class);".ind(2))
maps.append('\n')
}
if (gen.genClass) {
genClass(name, gen.stub)
}
}
if (gen.xml.isNotEmpty()) {
cmps.add(gen.xml.format(name))
}
} }
// Shuffle the order of the components // Shuffle the order of the components
cmps.shuffle(RANDOM) cmps.shuffle(RANDOM)
val xml = File(srcDir, "AndroidManifest.xml").readText() val xml = File(srcDir, "AndroidManifest.xml").readText()
val genXml = xml.format(usedNames[0], usedNames[1], cmps.joinToString("\n\n")) return xml.format(usedNames[0], usedNames[1], cmps.joinToString("\n\n"))
// Write mapping information to code
val mapping = File(srcDir, "Mapping.java").readText().format(maps)
PrintStream(File(mainPkgDir, "Mapping.java")).use {
it.print(mapping)
}
return genXml
} }
fun genEncryptedResources(res: InputStream, outDir: File) { fun genEncryptedResources(res: InputStream, outDir: File) {

View File

@ -3,6 +3,7 @@ package com.topjohnwu.magisk;
import com.topjohnwu.magisk.utils.DynamicClassLoader; import com.topjohnwu.magisk.utils.DynamicClassLoader;
import java.io.File; import java.io.File;
import java.util.Map;
// Wrap the actual classloader as we only want to resolve classname // Wrap the actual classloader as we only want to resolve classname
// mapping when loading from platform (via LoadedApk.mClassLoader) // mapping when loading from platform (via LoadedApk.mClassLoader)
@ -14,19 +15,24 @@ class InjectedClassLoader extends ClassLoader {
@Override @Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
return super.loadClass(Mapping.get(name), resolve); String clz = DynLoad.componentMap.get(name);
name = clz != null ? clz : name;
return super.loadClass(name, resolve);
} }
} }
class RedirectClassLoader extends ClassLoader { class RedirectClassLoader extends ClassLoader {
RedirectClassLoader() { private final Map<String, Class<?>> mapping;
RedirectClassLoader(Map<String, Class<?>> m) {
super(RedirectClassLoader.class.getClassLoader()); super(RedirectClassLoader.class.getClassLoader());
mapping = m;
} }
@Override @Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> clz = Mapping.internalMap.get(name); Class<?> clz = mapping.get(name);
return clz == null ? super.loadClass(name, resolve) : clz; return clz == null ? super.loadClass(name, resolve) : clz;
} }
} }

View File

@ -11,6 +11,10 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.os.Process; import android.os.Process;
import com.topjohnwu.magisk.dummy.DummyProvider;
import com.topjohnwu.magisk.dummy.DummyReceiver;
import com.topjohnwu.magisk.dummy.DummyService;
@SuppressLint("NewApi") @SuppressLint("NewApi")
public class DelegateComponentFactory extends AppComponentFactory { public class DelegateComponentFactory extends AppComponentFactory {
@ -40,7 +44,7 @@ public class DelegateComponentFactory extends AppComponentFactory {
throws ClassNotFoundException, IllegalAccessException, InstantiationException { throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (receiver != null) if (receiver != null)
return receiver.instantiateActivity(DynLoad.loader, className, intent); return receiver.instantiateActivity(DynLoad.loader, className, intent);
return create(className); return create(className, DownloadActivity.class);
} }
@Override @Override
@ -48,7 +52,7 @@ public class DelegateComponentFactory extends AppComponentFactory {
throws ClassNotFoundException, IllegalAccessException, InstantiationException { throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (receiver != null) if (receiver != null)
return receiver.instantiateReceiver(DynLoad.loader, className, intent); return receiver.instantiateReceiver(DynLoad.loader, className, intent);
return create(className); return create(className, DummyReceiver.class);
} }
@Override @Override
@ -56,7 +60,7 @@ public class DelegateComponentFactory extends AppComponentFactory {
throws ClassNotFoundException, IllegalAccessException, InstantiationException { throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (receiver != null) if (receiver != null)
return receiver.instantiateService(DynLoad.loader, className, intent); return receiver.instantiateService(DynLoad.loader, className, intent);
return create(className); return create(className, DummyService.class);
} }
@Override @Override
@ -64,12 +68,17 @@ public class DelegateComponentFactory extends AppComponentFactory {
throws ClassNotFoundException, IllegalAccessException, InstantiationException { throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (receiver != null) if (receiver != null)
return receiver.instantiateProvider(DynLoad.loader, className); return receiver.instantiateProvider(DynLoad.loader, className);
return create(className); return create(className, DummyProvider.class);
} }
private <T> T create(String name) private <T> T create(String name, Class<T> fallback)
throws ClassNotFoundException, IllegalAccessException, InstantiationException{ throws IllegalAccessException, InstantiationException {
return (T) DynLoad.loader.loadClass(name).newInstance(); try {
// noinspection unchecked
return (T) DynLoad.loader.loadClass(name).newInstance();
} catch (ClassNotFoundException e) {
return fallback.newInstance();
}
} }
} }

View File

@ -49,6 +49,14 @@ public class DownloadActivity extends Activity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (DynLoad.isDynLoader()) {
// For some reason activity is created before Application.attach(),
// relaunch the activity using the same intent
finishAffinity();
startActivity(getIntent());
return;
}
themed = new ContextThemeWrapper(this, android.R.style.Theme_DeviceDefault); themed = new ContextThemeWrapper(this, android.R.style.Theme_DeviceDefault);
// Only download and dynamic load full APK if hidden // Only download and dynamic load full APK if hidden

View File

@ -4,14 +4,21 @@ import static com.topjohnwu.magisk.BuildConfig.APPLICATION_ID;
import android.app.AppComponentFactory; import android.app.AppComponentFactory;
import android.app.Application; import android.app.Application;
import android.app.job.JobService;
import android.content.Context; import android.content.Context;
import android.content.ContextWrapper; import android.content.ContextWrapper;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.os.Build; import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.util.Log; import android.util.Log;
import com.topjohnwu.magisk.dummy.DummyProvider;
import com.topjohnwu.magisk.dummy.DummyReceiver;
import com.topjohnwu.magisk.dummy.DummyService;
import com.topjohnwu.magisk.utils.APKInstall; import com.topjohnwu.magisk.utils.APKInstall;
import java.io.File; import java.io.File;
@ -20,6 +27,8 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import io.michaelrocks.paranoid.Obfuscate; import io.michaelrocks.paranoid.Obfuscate;
@ -28,15 +37,20 @@ import io.michaelrocks.paranoid.Obfuscate;
public class DynLoad { public class DynLoad {
// The current active classloader // The current active classloader
static ClassLoader loader = new RedirectClassLoader(); static ClassLoader loader = DynLoad.class.getClassLoader();
static Object componentFactory; static Object componentFactory;
static Map<String, String> componentMap = new HashMap<>();
private static boolean loadedApk = false; private static boolean loadedApk = false;
static StubApk.Data createApkData() { static StubApk.Data createApkData() {
var data = new StubApk.Data(); var data = new StubApk.Data();
data.setVersion(BuildConfig.STUB_VERSION); data.setVersion(BuildConfig.STUB_VERSION);
data.setClassToComponent(Mapping.inverseMap); Map<String, String> map = new HashMap<>();
for (var e : componentMap.entrySet()) {
map.put(e.getValue(), e.getKey());
}
data.setClassToComponent(map);
data.setRootService(DelegateRootService.class); data.setRootService(DelegateRootService.class);
return data; return data;
} }
@ -135,23 +149,40 @@ public class DynLoad {
if (Build.VERSION.SDK_INT < 29) if (Build.VERSION.SDK_INT < 29)
replaceClassLoader(context); replaceClassLoader(context);
if (!loadApk(context)) int flags = PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES
| PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS;
final PackageInfo info;
try {
info = context.getPackageManager()
.getPackageInfo(context.getPackageName(), flags);
} catch (PackageManager.NameNotFoundException e) {
// Impossible
throw new RuntimeException(e);
}
if (!loadApk(context)) {
loader = new RedirectClassLoader(createInternalMap(info));
return null; return null;
}
File apk = StubApk.current(context); File apk = StubApk.current(context);
PackageManager pm = context.getPackageManager(); PackageManager pm = context.getPackageManager();
try { try {
var info = pm.getPackageArchiveInfo(apk.getPath(), 0).applicationInfo; var pkgInfo = pm.getPackageArchiveInfo(apk.getPath(), flags);
var appInfo = pkgInfo.applicationInfo;
updateComponentMap(info, pkgInfo);
// Create the receiver Application // Create the receiver Application
var data = createApkData(); var data = createApkData();
var app = (Application) loader.loadClass(info.className) var app = (Application) loader.loadClass(appInfo.className)
.getConstructor(Object.class) .getConstructor(Object.class)
.newInstance(data.getObject()); .newInstance(data.getObject());
// Create the receiver component factory // Create the receiver component factory
if (Build.VERSION.SDK_INT >= 28 && componentFactory != null) { if (Build.VERSION.SDK_INT >= 28 && componentFactory != null) {
Object factory = loader.loadClass(info.appComponentFactory).newInstance(); Object factory = loader.loadClass(appInfo.appComponentFactory).newInstance();
var delegate = (DelegateComponentFactory) componentFactory; var delegate = (DelegateComponentFactory) componentFactory;
delegate.receiver = (AppComponentFactory) factory; delegate.receiver = (AppComponentFactory) factory;
} }
@ -168,7 +199,90 @@ public class DynLoad {
return null; return null;
} }
private static boolean isDynLoader() { private static Map<String, Class<?>> createInternalMap(PackageInfo info) {
Map<String, Class<?>> map = new HashMap<>();
for (var c : info.activities) {
map.put(c.name, DownloadActivity.class);
}
for (var c : info.services) {
map.put(c.name, DummyService.class);
}
for (var c : info.providers) {
map.put(c.name, DummyProvider.class);
}
for (var c : info.receivers) {
map.put(c.name, DummyReceiver.class);
}
return map;
}
private static void updateComponentMap(PackageInfo from, PackageInfo to) {
{
var src = from.activities;
var dest = to.activities;
final ActivityInfo sa;
final ActivityInfo da;
final ActivityInfo sb;
final ActivityInfo db;
if (src[0].exported) {
sa = src[0];
sb = src[1];
} else {
sa = src[1];
sb = src[0];
}
if (dest[0].exported) {
da = dest[0];
db = dest[1];
} else {
da = dest[1];
db = dest[0];
}
componentMap.put(sa.name, da.name);
componentMap.put(sb.name, db.name);
}
{
var src = from.services;
var dest = to.services;
final ServiceInfo sa;
final ServiceInfo da;
final ServiceInfo sb;
final ServiceInfo db;
if (JobService.PERMISSION_BIND.equals(src[0].permission)) {
sa = src[0];
sb = src[1];
} else {
sa = src[1];
sb = src[0];
}
if (JobService.PERMISSION_BIND.equals(dest[0].permission)) {
da = dest[0];
db = dest[1];
} else {
da = dest[1];
db = dest[0];
}
componentMap.put(sa.name, da.name);
componentMap.put(sb.name, db.name);
}
{
var src = from.receivers;
var dest = to.receivers;
componentMap.put(src[0].name, dest[0].name);
}
{
var src = from.providers;
var dest = to.providers;
componentMap.put(src[0].name, dest[0].name);
}
}
static boolean isDynLoader() {
return loader instanceof InjectedClassLoader; return loader instanceof InjectedClassLoader;
} }

View File

@ -0,0 +1,12 @@
package com.topjohnwu.magisk.dummy;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class DummyService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
}

View File

@ -1,26 +0,0 @@
package com.topjohnwu.magisk;
import java.util.HashMap;
import java.util.Map;
import io.michaelrocks.paranoid.Obfuscate;
@Obfuscate
public class Mapping {
private static final Map<String, String> map = new HashMap<>();
public static final Map<String, Class<?>> internalMap = new HashMap<>();
public static final Map<String, String> inverseMap = new HashMap<>();
static {
%s
for (Map.Entry<String, String> e : map.entrySet()) {
inverseMap.put(e.getValue(), e.getKey());
}
}
public static String get(String name) {
String n = map.get(name);
return n != null ? n : name;
}
}