From fba83e2330bbb7e12c2034429f4c19cba02b4cb9 Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Tue, 26 Jan 2021 03:40:25 -0800 Subject: [PATCH] Support stub APK loading down to Android 5.0 --- stub/src/main/AndroidManifest.xml | 6 +- stub/src/main/java/a/ii.java | 5 -- stub/src/main/java/a/r.java | 5 -- stub/src/main/java/a/u7.java | 5 -- .../com/topjohnwu/magisk/ClassLoaders.java | 33 ++++++++ .../topjohnwu/magisk/DelegateApplication.java | 19 +++-- .../magisk/DelegateComponentFactory.java | 49 +++++------- .../java/com/topjohnwu/magisk/InjectAPK.java | 76 +++++++++++++------ .../java/com/topjohnwu/magisk/Mapping.java | 33 +++++--- .../topjohnwu/magisk/dummy/DummyActivity.java | 13 ---- .../topjohnwu/magisk/dummy/DummyService.java | 13 ---- 11 files changed, 141 insertions(+), 116 deletions(-) delete mode 100644 stub/src/main/java/a/ii.java delete mode 100644 stub/src/main/java/a/r.java delete mode 100644 stub/src/main/java/a/u7.java create mode 100644 stub/src/main/java/com/topjohnwu/magisk/ClassLoaders.java delete mode 100644 stub/src/main/java/com/topjohnwu/magisk/dummy/DummyActivity.java delete mode 100644 stub/src/main/java/com/topjohnwu/magisk/dummy/DummyService.java diff --git a/stub/src/main/AndroidManifest.xml b/stub/src/main/AndroidManifest.xml index ca06453c0..c17f33675 100644 --- a/stub/src/main/AndroidManifest.xml +++ b/stub/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ tools:ignore="GoogleAppIndexingWarning,MissingApplicationIcon,UnusedAttribute"> - + @@ -42,7 +42,7 @@ @@ -61,7 +61,7 @@ loadClass(String name, boolean resolve) throws ClassNotFoundException { + return super.loadClass(Mapping.get(name), resolve); + } +} + +class RedirectClassLoader extends ClassLoader { + + RedirectClassLoader() { + super(RedirectClassLoader.class.getClassLoader()); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + Class clz = Mapping.internalMap.get(name); + return clz == null ? super.loadClass(name, resolve) : clz; + } +} diff --git a/stub/src/main/java/com/topjohnwu/magisk/DelegateApplication.java b/stub/src/main/java/com/topjohnwu/magisk/DelegateApplication.java index b9d550e8a..d4c565b42 100644 --- a/stub/src/main/java/com/topjohnwu/magisk/DelegateApplication.java +++ b/stub/src/main/java/com/topjohnwu/magisk/DelegateApplication.java @@ -10,32 +10,31 @@ import java.lang.reflect.Method; public class DelegateApplication extends Application { - private Application delegate; static boolean dynLoad = false; + private Application receiver; + @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); - // Only dynamic load full APK if hidden and possible - dynLoad = Build.VERSION.SDK_INT >= 28 && + // Only dynamic load full APK if hidden and supported + dynLoad = Build.VERSION.SDK_INT >= 21 && !base.getPackageName().equals(BuildConfig.APPLICATION_ID); - if (!dynLoad) - return; - delegate = InjectAPK.setup(this); - if (delegate != null) try { + receiver = InjectAPK.setup(this); + if (receiver != null) try { // Call attachBaseContext without ContextImpl to show it is being wrapped Method m = ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class); m.setAccessible(true); - m.invoke(delegate, this); + m.invoke(receiver, this); } catch (Exception ignored) { /* Impossible */ } } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - if (delegate != null) - delegate.onConfigurationChanged(newConfig); + if (receiver != null) + receiver.onConfigurationChanged(newConfig); } } diff --git a/stub/src/main/java/com/topjohnwu/magisk/DelegateComponentFactory.java b/stub/src/main/java/com/topjohnwu/magisk/DelegateComponentFactory.java index 0eefd0995..05cf0027a 100644 --- a/stub/src/main/java/com/topjohnwu/magisk/DelegateComponentFactory.java +++ b/stub/src/main/java/com/topjohnwu/magisk/DelegateComponentFactory.java @@ -9,22 +9,15 @@ import android.content.BroadcastReceiver; import android.content.ContentProvider; import android.content.Intent; -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 { + static DelegateComponentFactory INSTANCE; ClassLoader loader; - AppComponentFactory delegate; - - interface DummyFactory { - T create(); - } + AppComponentFactory receiver; public DelegateComponentFactory() { - InjectAPK.factory = this; + INSTANCE = this; } @Override @@ -36,45 +29,39 @@ public class DelegateComponentFactory extends AppComponentFactory { @Override public Activity instantiateActivity(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException { - if (delegate != null) - return delegate.instantiateActivity(loader, Mapping.get(className), intent); - return create(className, DownloadActivity::new); + if (receiver != null) + return receiver.instantiateActivity(loader, className, intent); + return create(className); } @Override public BroadcastReceiver instantiateReceiver(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException { - if (delegate != null) - return delegate.instantiateReceiver(loader, Mapping.get(className), intent); - return create(className, DummyReceiver::new); + if (receiver != null) + return receiver.instantiateReceiver(loader, className, intent); + return create(className); } @Override public Service instantiateService(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException { - if (delegate != null) - return delegate.instantiateService(loader, Mapping.get(className), intent); - return create(className, DummyService::new); + if (receiver != null) + return receiver.instantiateService(loader, className, intent); + return create(className); } @Override public ContentProvider instantiateProvider(ClassLoader cl, String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException { if (loader == null) loader = cl; - if (delegate != null) - return delegate.instantiateProvider(loader, Mapping.get(className)); - return create(className, DummyProvider::new); + if (receiver != null) + return receiver.instantiateProvider(loader, className); + return create(className); } - /** - * Create the class or dummy implementation if creation failed - */ - private T create(String name, DummyFactory factory) { - try { - return (T) loader.loadClass(name).newInstance(); - } catch (Exception ignored) { - return factory.create(); - } + private T create(String name) + throws ClassNotFoundException, IllegalAccessException, InstantiationException{ + return (T) loader.loadClass(name).newInstance(); } } diff --git a/stub/src/main/java/com/topjohnwu/magisk/InjectAPK.java b/stub/src/main/java/com/topjohnwu/magisk/InjectAPK.java index 8976f8282..483625064 100644 --- a/stub/src/main/java/com/topjohnwu/magisk/InjectAPK.java +++ b/stub/src/main/java/com/topjohnwu/magisk/InjectAPK.java @@ -4,65 +4,97 @@ import android.app.AppComponentFactory; import android.app.Application; import android.content.ContentResolver; import android.content.Context; +import android.content.ContextWrapper; import android.net.Uri; +import android.os.Build; import android.util.Log; -import com.topjohnwu.magisk.utils.DynamicClassLoader; - import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; +import java.lang.reflect.Field; public class InjectAPK { - static DelegateComponentFactory factory; - + @SuppressWarnings("ResultOfMethodCallIgnored") static Application setup(Context context) { + // Get ContextImpl + while (context instanceof ContextWrapper) { + context = ((ContextWrapper) context).getBaseContext(); + } + File apk = DynAPK.current(context); File update = DynAPK.update(context); if (update.exists()) update.renameTo(apk); - Application delegate = null; + Application result = null; if (!apk.exists()) { // Try copying APK Uri uri = new Uri.Builder().scheme("content") .authority("com.topjohnwu.magisk.provider") .encodedPath("apk_file").build(); ContentResolver resolver = context.getContentResolver(); - try (InputStream is = resolver.openInputStream(uri)) { - if (is != null) { + try (InputStream src = resolver.openInputStream(uri)) { + if (src != null) { try (OutputStream out = new FileOutputStream(apk)) { byte[] buf = new byte[4096]; - for (int read; (read = is.read(buf)) >= 0;) { + for (int read; (read = src.read(buf)) >= 0;) { out.write(buf, 0, read); } } } - } catch (Exception e) { - Log.e(InjectAPK.class.getSimpleName(), "", e); - } + } catch (Exception ignored) {} } if (apk.exists()) { - ClassLoader cl = new DynamicClassLoader(apk, factory.loader); + ClassLoader cl = new InjectedClassLoader(apk); try { - // Create the delegate AppComponentFactory - AppComponentFactory df = (AppComponentFactory) - cl.loadClass("androidx.core.app.CoreComponentFactory").newInstance(); - - // Create the delegate Application - delegate = (Application) cl.loadClass("a.e").getConstructor(Object.class) + // Create the receiver Application + Object app = cl.loadClass(Mapping.get("APP")).getConstructor(Object.class) .newInstance(DynAPK.pack(dynData())); - // If everything went well, set our loader and delegate - factory.delegate = df; - factory.loader = cl; + // Create the receiver component factory + Object factory = null; + if (Build.VERSION.SDK_INT >= 28) { + factory = cl.loadClass(Mapping.get("ACF")).newInstance(); + } + + setClassLoader(context, cl); + + // Finally, set variables + result = (Application) app; + if (Build.VERSION.SDK_INT >= 28) { + DelegateComponentFactory.INSTANCE.loader = cl; + DelegateComponentFactory.INSTANCE.receiver = (AppComponentFactory) factory; + } } catch (Exception e) { Log.e(InjectAPK.class.getSimpleName(), "", e); apk.delete(); } + } else { + ClassLoader cl = new RedirectClassLoader(); + try { + setClassLoader(context, cl); + if (Build.VERSION.SDK_INT >= 28) { + DelegateComponentFactory.INSTANCE.loader = cl; + } + } catch (Exception e) { + // Should never happen + Log.e(InjectAPK.class.getSimpleName(), "", e); + } } - return delegate; + return result; + } + + // Replace LoadedApk mClassLoader + private static void setClassLoader(Context impl, ClassLoader cl) + throws NoSuchFieldException, IllegalAccessException { + Field mInfo = impl.getClass().getDeclaredField("mPackageInfo"); + mInfo.setAccessible(true); + Object loadedApk = mInfo.get(impl); + Field mcl = loadedApk.getClass().getDeclaredField("mClassLoader"); + mcl.setAccessible(true); + mcl.set(loadedApk, cl); } private static DynAPK.Data dynData() { diff --git a/stub/src/main/java/com/topjohnwu/magisk/Mapping.java b/stub/src/main/java/com/topjohnwu/magisk/Mapping.java index 87739920f..8bdbe389f 100644 --- a/stub/src/main/java/com/topjohnwu/magisk/Mapping.java +++ b/stub/src/main/java/com/topjohnwu/magisk/Mapping.java @@ -1,27 +1,42 @@ package com.topjohnwu.magisk; +import com.topjohnwu.magisk.dummy.DummyReceiver; + import java.util.HashMap; import java.util.Map; +/** + * These are just some random class names hardcoded as an example. + * For the actual release builds, these mappings will be auto generated. + */ public class Mapping { - private static Map map = new HashMap<>(); - public static Map inverseMap; + private static final Map map = new HashMap<>(); + public static final Map> internalMap = new HashMap<>(); + public static final Map inverseMap; static { - map.put("a.Qzw", "a.e"); - map.put("a.u7", "a.c"); - map.put("a.ii", "a.p"); - map.put("a.r", "a.h"); - map.put("xt.R", "a.b"); - map.put("lt5.a", "a.m"); - map.put("d.s", "a.j"); + map.put("a.Q", "com.topjohnwu.magisk.core.App"); + map.put("f.u7", "com.topjohnwu.magisk.core.SplashActivity"); + map.put("fxQ.lk", "com.topjohnwu.magisk.core.Provider"); + map.put("yy.E", "com.topjohnwu.magisk.core.Receiver"); + map.put("xt.R", "com.topjohnwu.magisk.ui.MainActivity"); + map.put("lt5.a", "com.topjohnwu.magisk.ui.surequest.SuRequestActivity"); + map.put("d.s", "com.topjohnwu.magisk.core.download.DownloadService"); map.put("w.d", "androidx.work.impl.background.systemjob.SystemJobService"); + internalMap.put("a.Q", DelegateApplication.class); + internalMap.put("f.u7", DownloadActivity.class); + internalMap.put("fxQ.lk", FileProvider.class); + internalMap.put("yy.E", DummyReceiver.class); + inverseMap = new HashMap<>(map.size()); for (Map.Entry e : map.entrySet()) { inverseMap.put(e.getValue(), e.getKey()); } + + map.put("APP", "com.topjohnwu.magisk.core.App"); + map.put("ACF", "androidx.core.app.CoreComponentFactory"); } public static String get(String name) { diff --git a/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyActivity.java b/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyActivity.java deleted file mode 100644 index 7a279ba55..000000000 --- a/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyActivity.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.topjohnwu.magisk.dummy; - -import android.app.Activity; -import android.os.Bundle; - -public class DummyActivity extends Activity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - finish(); - } -} diff --git a/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyService.java b/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyService.java deleted file mode 100644 index 6bce30183..000000000 --- a/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyService.java +++ /dev/null @@ -1,13 +0,0 @@ -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) { - stopSelf(); - return null; - } -}