diff --git a/buildSrc/src/main/java/Codegen.kt b/buildSrc/src/main/java/Codegen.kt index df32b2ff6..017316437 100644 --- a/buildSrc/src/main/java/Codegen.kt +++ b/buildSrc/src/main/java/Codegen.kt @@ -74,13 +74,6 @@ fun genKeyData(keysDir: File, outSrc: File) { } fun genStubManifest(srcDir: File, outDir: File): String { - class Component( - val real: String, - val stub: String, - val xml: String, - val genClass: Boolean = false - ) - outDir.deleteRecursively() 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)) - val cmpList = mutableListOf() + val cmpList = mutableListOf() - cmpList.add(Component( - "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", + cmpList.add( """ |""".ind(2) - )) + ) - cmpList.add(Component( - "com.topjohnwu.magisk.core.Receiver", - "dummy.DummyReceiver", + cmpList.add( """ | | |""".ind(2) - )) + ) - cmpList.add(Component( - "com.topjohnwu.magisk.ui.MainActivity", - "DownloadActivity", + cmpList.add( """ | | |""".ind(2) - )) + ) - cmpList.add(Component( - "com.topjohnwu.magisk.ui.surequest.SuRequestActivity", - "", + cmpList.add( """ | | |""".ind(2) - )) + ) - cmpList.add(Component( - "com.topjohnwu.magisk.core.download.DownloadService", - "", + cmpList.add( """ |""".trimIndent().ind(2) - )) + | android:exported="false" />""".ind(2) + ) - cmpList.add(Component( - "com.topjohnwu.magisk.core.JobService", - "", + cmpList.add( """ |""".ind(2) - )) + ) val names = mutableListOf() names.addAll(c1) @@ -194,33 +161,38 @@ fun genStubManifest(srcDir: File, outDir: File): String { names.addAll(c3.subList(0, 10)) names.shuffle(RANDOM) - val pkgNames = names.subList(0, 50) + val pkgNames = names // Distinct by lower case to support case insensitive file systems .distinctBy { it.toLowerCase(Locale.ROOT) } // Old Android does not support capitalized package names // Check Android 7.0.0 PackageParser#buildClassName .map { it.decapitalize(Locale.ROOT) } - var idx = 0 fun isJavaKeyword(name: String) = when (name) { "do", "if", "for", "int", "new", "try" -> true else -> false } - fun genCmpName() : String { - var pkgName : String + val cmps = mutableListOf() + val usedNames = mutableListOf() + + fun genCmpName(): String { + var pkgName: String do { - pkgName = pkgNames[idx++] + pkgName = pkgNames.random(kRANDOM) } while (isJavaKeyword(pkgName)) - var clzName : String + var clzName: String do { clzName = names.random(kRANDOM) } 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 pkgDir = File(outDir, pkg) pkgDir.mkdir() @@ -230,41 +202,20 @@ fun genStubManifest(srcDir: File, outDir: File): String { } } - val cmps = mutableListOf() - val usedNames = mutableListOf() - val maps = StringBuilder() + // Generate 2 non redirect-able classes + genClass("DelegateComponentFactory") + genClass("DelegateApplication") for (gen in cmpList) { val name = genCmpName() - usedNames.add(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)) - } + cmps.add(gen.format(name)) } // Shuffle the order of the components cmps.shuffle(RANDOM) + val xml = File(srcDir, "AndroidManifest.xml").readText() - val genXml = 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 + return xml.format(usedNames[0], usedNames[1], cmps.joinToString("\n\n")) } fun genEncryptedResources(res: InputStream, outDir: File) { diff --git a/stub/src/main/java/com/topjohnwu/magisk/ClassLoaders.java b/stub/src/main/java/com/topjohnwu/magisk/ClassLoaders.java index e92e3680b..c0afc2753 100644 --- a/stub/src/main/java/com/topjohnwu/magisk/ClassLoaders.java +++ b/stub/src/main/java/com/topjohnwu/magisk/ClassLoaders.java @@ -3,6 +3,7 @@ package com.topjohnwu.magisk; import com.topjohnwu.magisk.utils.DynamicClassLoader; import java.io.File; +import java.util.Map; // Wrap the actual classloader as we only want to resolve classname // mapping when loading from platform (via LoadedApk.mClassLoader) @@ -14,19 +15,24 @@ class InjectedClassLoader extends ClassLoader { @Override 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 { - RedirectClassLoader() { + private final Map> mapping; + + RedirectClassLoader(Map> m) { super(RedirectClassLoader.class.getClassLoader()); + mapping = m; } @Override 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; } } diff --git a/stub/src/main/java/com/topjohnwu/magisk/DelegateComponentFactory.java b/stub/src/main/java/com/topjohnwu/magisk/DelegateComponentFactory.java index 2f6911c8e..b396ae8e7 100644 --- a/stub/src/main/java/com/topjohnwu/magisk/DelegateComponentFactory.java +++ b/stub/src/main/java/com/topjohnwu/magisk/DelegateComponentFactory.java @@ -11,6 +11,10 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.os.Process; +import com.topjohnwu.magisk.dummy.DummyProvider; +import com.topjohnwu.magisk.dummy.DummyReceiver; +import com.topjohnwu.magisk.dummy.DummyService; + @SuppressLint("NewApi") public class DelegateComponentFactory extends AppComponentFactory { @@ -40,7 +44,7 @@ public class DelegateComponentFactory extends AppComponentFactory { throws ClassNotFoundException, IllegalAccessException, InstantiationException { if (receiver != null) return receiver.instantiateActivity(DynLoad.loader, className, intent); - return create(className); + return create(className, DownloadActivity.class); } @Override @@ -48,7 +52,7 @@ public class DelegateComponentFactory extends AppComponentFactory { throws ClassNotFoundException, IllegalAccessException, InstantiationException { if (receiver != null) return receiver.instantiateReceiver(DynLoad.loader, className, intent); - return create(className); + return create(className, DummyReceiver.class); } @Override @@ -56,7 +60,7 @@ public class DelegateComponentFactory extends AppComponentFactory { throws ClassNotFoundException, IllegalAccessException, InstantiationException { if (receiver != null) return receiver.instantiateService(DynLoad.loader, className, intent); - return create(className); + return create(className, DummyService.class); } @Override @@ -64,12 +68,17 @@ public class DelegateComponentFactory extends AppComponentFactory { throws ClassNotFoundException, IllegalAccessException, InstantiationException { if (receiver != null) return receiver.instantiateProvider(DynLoad.loader, className); - return create(className); + return create(className, DummyProvider.class); } - private T create(String name) - throws ClassNotFoundException, IllegalAccessException, InstantiationException{ - return (T) DynLoad.loader.loadClass(name).newInstance(); + private T create(String name, Class fallback) + throws IllegalAccessException, InstantiationException { + try { + // noinspection unchecked + return (T) DynLoad.loader.loadClass(name).newInstance(); + } catch (ClassNotFoundException e) { + return fallback.newInstance(); + } } } diff --git a/stub/src/main/java/com/topjohnwu/magisk/DownloadActivity.java b/stub/src/main/java/com/topjohnwu/magisk/DownloadActivity.java index b305536f1..8272da492 100644 --- a/stub/src/main/java/com/topjohnwu/magisk/DownloadActivity.java +++ b/stub/src/main/java/com/topjohnwu/magisk/DownloadActivity.java @@ -49,6 +49,14 @@ public class DownloadActivity extends Activity { @Override protected void onCreate(Bundle 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); // Only download and dynamic load full APK if hidden diff --git a/stub/src/main/java/com/topjohnwu/magisk/DynLoad.java b/stub/src/main/java/com/topjohnwu/magisk/DynLoad.java index 5ce0679d7..857bacbf6 100644 --- a/stub/src/main/java/com/topjohnwu/magisk/DynLoad.java +++ b/stub/src/main/java/com/topjohnwu/magisk/DynLoad.java @@ -4,14 +4,21 @@ import static com.topjohnwu.magisk.BuildConfig.APPLICATION_ID; import android.app.AppComponentFactory; import android.app.Application; +import android.app.job.JobService; import android.content.Context; import android.content.ContextWrapper; +import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; import android.os.Build; import android.os.Environment; 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 java.io.File; @@ -20,6 +27,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; import io.michaelrocks.paranoid.Obfuscate; @@ -28,15 +37,20 @@ import io.michaelrocks.paranoid.Obfuscate; public class DynLoad { // The current active classloader - static ClassLoader loader = new RedirectClassLoader(); + static ClassLoader loader = DynLoad.class.getClassLoader(); static Object componentFactory; + static Map componentMap = new HashMap<>(); private static boolean loadedApk = false; static StubApk.Data createApkData() { var data = new StubApk.Data(); data.setVersion(BuildConfig.STUB_VERSION); - data.setClassToComponent(Mapping.inverseMap); + Map map = new HashMap<>(); + for (var e : componentMap.entrySet()) { + map.put(e.getValue(), e.getKey()); + } + data.setClassToComponent(map); data.setRootService(DelegateRootService.class); return data; } @@ -135,23 +149,40 @@ public class DynLoad { if (Build.VERSION.SDK_INT < 29) 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; + } File apk = StubApk.current(context); PackageManager pm = context.getPackageManager(); 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 var data = createApkData(); - var app = (Application) loader.loadClass(info.className) + var app = (Application) loader.loadClass(appInfo.className) .getConstructor(Object.class) .newInstance(data.getObject()); // Create the receiver component factory 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; delegate.receiver = (AppComponentFactory) factory; } @@ -168,7 +199,90 @@ public class DynLoad { return null; } - private static boolean isDynLoader() { + private static Map> createInternalMap(PackageInfo info) { + Map> 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; } diff --git a/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyService.java b/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyService.java new file mode 100644 index 000000000..3bd01509c --- /dev/null +++ b/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyService.java @@ -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; + } +} diff --git a/stub/template/Mapping.java b/stub/template/Mapping.java deleted file mode 100644 index 0f6ddcd55..000000000 --- a/stub/template/Mapping.java +++ /dev/null @@ -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 map = new HashMap<>(); - public static final Map> internalMap = new HashMap<>(); - public static final Map inverseMap = new HashMap<>(); - - static { -%s - for (Map.Entry 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; - } -}