From 355341f0ab8158990f81b94708dc226805483bf1 Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Tue, 1 Feb 2022 22:43:44 -0800 Subject: [PATCH] Use AppComponentFactory to replace ClassLoader --- .../java/com/topjohnwu/magisk/DynAPK.java | 26 +++- .../com/topjohnwu/magisk/ClassLoaders.java | 12 ++ .../topjohnwu/magisk/DelegateApplication.java | 12 +- .../magisk/DelegateComponentFactory.java | 19 ++- .../topjohnwu/magisk/DelegateRootService.java | 7 +- .../topjohnwu/magisk/DownloadActivity.java | 3 +- .../java/com/topjohnwu/magisk/DynLoad.java | 143 +++++++++++------- 7 files changed, 139 insertions(+), 83 deletions(-) diff --git a/app/shared/src/main/java/com/topjohnwu/magisk/DynAPK.java b/app/shared/src/main/java/com/topjohnwu/magisk/DynAPK.java index c688b7339..e05b6d1bb 100644 --- a/app/shared/src/main/java/com/topjohnwu/magisk/DynAPK.java +++ b/app/shared/src/main/java/com/topjohnwu/magisk/DynAPK.java @@ -3,6 +3,7 @@ package com.topjohnwu.magisk; import static android.os.Build.VERSION.SDK_INT; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.res.AssetManager; import java.io.File; @@ -16,24 +17,35 @@ public class DynAPK { private static File dynDir; private static Method addAssetPath; - private static File getDynDir(Context c) { + private static File getDynDir(ApplicationInfo info) { if (dynDir == null) { + final String dataDir; if (SDK_INT >= 24) { - // Use protected context to allow directBootAware - c = c.createDeviceProtectedStorageContext(); + // Use device protected path to allow directBootAware + dataDir = info.deviceProtectedDataDir; + } else { + dataDir = info.dataDir; } - dynDir = new File(c.getFilesDir().getParent(), "dyn"); - dynDir.mkdir(); + dynDir = new File(dataDir, "dyn"); + dynDir.mkdirs(); } return dynDir; } public static File current(Context c) { - return new File(getDynDir(c), "current.apk"); + return new File(getDynDir(c.getApplicationInfo()), "current.apk"); + } + + public static File current(ApplicationInfo info) { + return new File(getDynDir(info), "current.apk"); } public static File update(Context c) { - return new File(getDynDir(c), "update.apk"); + return new File(getDynDir(c.getApplicationInfo()), "update.apk"); + } + + public static File update(ApplicationInfo info) { + return new File(getDynDir(info), "update.apk"); } public static void addAssetPath(AssetManager asset, String path) { diff --git a/stub/src/main/java/com/topjohnwu/magisk/ClassLoaders.java b/stub/src/main/java/com/topjohnwu/magisk/ClassLoaders.java index 381f283b7..e92e3680b 100644 --- a/stub/src/main/java/com/topjohnwu/magisk/ClassLoaders.java +++ b/stub/src/main/java/com/topjohnwu/magisk/ClassLoaders.java @@ -30,3 +30,15 @@ class RedirectClassLoader extends ClassLoader { return clz == null ? super.loadClass(name, resolve) : clz; } } + +class DelegateClassLoader extends ClassLoader { + + DelegateClassLoader() { + super(null); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + return DynLoad.loader.loadClass(name); + } +} diff --git a/stub/src/main/java/com/topjohnwu/magisk/DelegateApplication.java b/stub/src/main/java/com/topjohnwu/magisk/DelegateApplication.java index 542663e46..0ed578b32 100644 --- a/stub/src/main/java/com/topjohnwu/magisk/DelegateApplication.java +++ b/stub/src/main/java/com/topjohnwu/magisk/DelegateApplication.java @@ -2,11 +2,8 @@ package com.topjohnwu.magisk; import android.app.Application; import android.content.Context; -import android.content.ContextWrapper; import android.content.res.Configuration; -import java.lang.reflect.Method; - import io.michaelrocks.paranoid.Obfuscate; @Obfuscate @@ -17,14 +14,7 @@ public class DelegateApplication extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); - - receiver = DynLoad.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(receiver, this); - } catch (Exception ignored) { /* Impossible */ } + receiver = DynLoad.createAndSetupApp(this); } @Override diff --git a/stub/src/main/java/com/topjohnwu/magisk/DelegateComponentFactory.java b/stub/src/main/java/com/topjohnwu/magisk/DelegateComponentFactory.java index e255fa96a..776ab3bd6 100644 --- a/stub/src/main/java/com/topjohnwu/magisk/DelegateComponentFactory.java +++ b/stub/src/main/java/com/topjohnwu/magisk/DelegateComponentFactory.java @@ -8,20 +8,25 @@ import android.app.Service; import android.content.BroadcastReceiver; import android.content.ContentProvider; import android.content.Intent; +import android.content.pm.ApplicationInfo; @SuppressLint("NewApi") public class DelegateComponentFactory extends AppComponentFactory { - ClassLoader loader; AppComponentFactory receiver; public DelegateComponentFactory() { DynLoad.componentFactory = this; } + @Override + public ClassLoader instantiateClassLoader(ClassLoader cl, ApplicationInfo info) { + DynLoad.loadAPK(info); + return new DelegateClassLoader(); + } + @Override public Application instantiateApplication(ClassLoader cl, String className) { - if (loader == null) loader = cl; return new DelegateApplication(); } @@ -29,7 +34,7 @@ public class DelegateComponentFactory extends AppComponentFactory { public Activity instantiateActivity(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException { if (receiver != null) - return receiver.instantiateActivity(loader, className, intent); + return receiver.instantiateActivity(DynLoad.loader, className, intent); return create(className); } @@ -37,7 +42,7 @@ public class DelegateComponentFactory extends AppComponentFactory { public BroadcastReceiver instantiateReceiver(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException { if (receiver != null) - return receiver.instantiateReceiver(loader, className, intent); + return receiver.instantiateReceiver(DynLoad.loader, className, intent); return create(className); } @@ -45,7 +50,7 @@ public class DelegateComponentFactory extends AppComponentFactory { public Service instantiateService(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException { if (receiver != null) - return receiver.instantiateService(loader, className, intent); + return receiver.instantiateService(DynLoad.loader, className, intent); return create(className); } @@ -53,13 +58,13 @@ public class DelegateComponentFactory extends AppComponentFactory { public ContentProvider instantiateProvider(ClassLoader cl, String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException { if (receiver != null) - return receiver.instantiateProvider(loader, className); + return receiver.instantiateProvider(DynLoad.loader, className); return create(className); } private T create(String name) throws ClassNotFoundException, IllegalAccessException, InstantiationException{ - return (T) loader.loadClass(name).newInstance(); + return (T) DynLoad.loader.loadClass(name).newInstance(); } } diff --git a/stub/src/main/java/com/topjohnwu/magisk/DelegateRootService.java b/stub/src/main/java/com/topjohnwu/magisk/DelegateRootService.java index 5be210edc..4683653ae 100644 --- a/stub/src/main/java/com/topjohnwu/magisk/DelegateRootService.java +++ b/stub/src/main/java/com/topjohnwu/magisk/DelegateRootService.java @@ -5,7 +5,6 @@ import android.content.ContextWrapper; import android.util.Log; import java.lang.reflect.Constructor; -import java.lang.reflect.Method; import io.michaelrocks.paranoid.Obfuscate; @@ -18,7 +17,7 @@ public class DelegateRootService extends ContextWrapper { @Override protected void attachBaseContext(Context base) { - if (DynLoad.inject(base) == null) + if (DynLoad.createApp(base) == null) return; // Create the actual RootService and call its attachBaseContext @@ -26,9 +25,7 @@ public class DelegateRootService extends ContextWrapper { Constructor ctor = DynLoad.apkData.getRootService().getConstructor(Object.class); ctor.setAccessible(true); Object service = ctor.newInstance(this); - Method m = ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class); - m.setAccessible(true); - m.invoke(service, base); + DynLoad.attachContext(service, base); } catch (Exception e) { Log.e(DelegateRootService.class.getSimpleName(), "", e); } diff --git a/stub/src/main/java/com/topjohnwu/magisk/DownloadActivity.java b/stub/src/main/java/com/topjohnwu/magisk/DownloadActivity.java index afbe535aa..4ff28a288 100644 --- a/stub/src/main/java/com/topjohnwu/magisk/DownloadActivity.java +++ b/stub/src/main/java/com/topjohnwu/magisk/DownloadActivity.java @@ -118,8 +118,7 @@ public class DownloadActivity extends Activity { File apk = dynLoad ? DynAPK.current(this) : new File(getCacheDir(), "manager.apk"); request(apkLink).setExecutor(AsyncTask.THREAD_POOL_EXECUTOR).getAsFile(apk, file -> { if (dynLoad) { - DynLoad.setup(this); - runOnUiThread(onSuccess); + // TODO } else { var receiver = APKInstall.register(this, BuildConfig.APPLICATION_ID, onSuccess); APKInstall.installapk(this, file); diff --git a/stub/src/main/java/com/topjohnwu/magisk/DynLoad.java b/stub/src/main/java/com/topjohnwu/magisk/DynLoad.java index a921307e9..ba7becee4 100644 --- a/stub/src/main/java/com/topjohnwu/magisk/DynLoad.java +++ b/stub/src/main/java/com/topjohnwu/magisk/DynLoad.java @@ -2,7 +2,6 @@ package com.topjohnwu.magisk; import android.app.AppComponentFactory; import android.app.Application; -import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; @@ -10,6 +9,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; +import android.os.Environment; import android.util.Log; import com.topjohnwu.magisk.utils.APKInstall; @@ -20,6 +20,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; +import java.lang.reflect.Method; import io.michaelrocks.paranoid.Obfuscate; @@ -27,23 +28,52 @@ import io.michaelrocks.paranoid.Obfuscate; @SuppressWarnings("ResultOfMethodCallIgnored") public class DynLoad { + // The current active classloader + static ClassLoader loader = new RedirectClassLoader(); static Object componentFactory; static final DynAPK.Data apkData = createApkData(); - // Dynamically load APK, inject ClassLoader into ContextImpl, then - // create the actual Application instance from the loaded APK - static Application inject(Context context) { - File apk = DynAPK.current(context); - File update = DynAPK.update(context); + private static boolean loadedApk = false; + + static void attachContext(Object o, Context context) { + if (!(o instanceof ContextWrapper)) + return; + try { + Method m = ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class); + m.setAccessible(true); + m.invoke(o, context); + } catch (Exception ignored) { /* Impossible */ } + } + + // Dynamically load APK from internal or external storage + static void loadAPK(ApplicationInfo info) { + if (loadedApk) + return; + loadedApk = true; + + File apk = DynAPK.current(info); + File update = DynAPK.update(info); if (update.exists()) { // Rename from update update.renameTo(apk); } - if (BuildConfig.DEBUG) { - // Copy from external for easier development - File external = new File(context.getExternalFilesDir(null), "magisk.apk"); + // Copy from external for easier development + if (BuildConfig.DEBUG) copy_from_ext: { + final File dir; + try { + var dirs = (File[]) Environment.class + .getMethod("buildExternalStorageAppFilesDirs", String.class) + .invoke(null, info.packageName); + if (dirs == null) + break copy_from_ext; + dir = dirs[0]; + } catch (ReflectiveOperationException e) { + Log.e(DynLoad.class.getSimpleName(), "", e); + break copy_from_ext; + } + File external = new File(dir, "magisk.apk"); if (external.exists()) { try { var in = new FileInputStream(external); @@ -60,19 +90,33 @@ public class DynLoad { } } - if (!apk.exists() && !context.getPackageName().equals(BuildConfig.APPLICATION_ID)) { - // Copy from previous app + if (apk.exists()) { + loader = new InjectedClassLoader(apk); + } + } + + // Dynamically load APK, inject ClassLoader into ContextImpl, then + // create the non-stub Application instance from the loaded APK + static Application createApp(Context context) { + File apk = DynAPK.current(context); + loadAPK(context.getApplicationInfo()); + + // Trigger folder creation + context.getExternalFilesDir(null); + + // If no APK to load, attempt to copy from previous app + if (!isDynLoader() && !context.getPackageName().equals(BuildConfig.APPLICATION_ID)) { Uri uri = new Uri.Builder().scheme("content") .authority("com.topjohnwu.magisk.provider") .encodedPath("apk_file").build(); - ContentResolver resolver = context.getContentResolver(); try { - InputStream src = resolver.openInputStream(uri); + InputStream src = context.getContentResolver().openInputStream(uri); if (src != null) { var out = new FileOutputStream(apk); try (src; out) { APKInstall.transfer(src, out); } + loader = new InjectedClassLoader(apk); } } catch (IOException e) { Log.e(DynLoad.class.getSimpleName(), "", e); @@ -80,12 +124,11 @@ public class DynLoad { } } - if (apk.exists()) { - ClassLoader cl = new InjectedClassLoader(apk); + if (isDynLoader()) { PackageManager pm = context.getPackageManager(); PackageInfo pkgInfo = pm.getPackageArchiveInfo(apk.getPath(), 0); try { - return createApp(context, cl, pkgInfo.applicationInfo); + return newApp(pkgInfo.applicationInfo); } catch (ReflectiveOperationException e) { Log.e(DynLoad.class.getSimpleName(), "", e); apk.delete(); @@ -95,65 +138,63 @@ public class DynLoad { return null; } - // Inject and create Application, or setup redirections for the current app - static Application setup(Context context) { - Application app = inject(context); + // Stub app setup entry + static Application createAndSetupApp(Application context) { + // On API >= 29, AppComponentFactory will replace the ClassLoader + if (Build.VERSION.SDK_INT < 29) + replaceClassLoader(context); + + Application app = createApp(context); if (app != null) { - return app; + // Send real application to attachBaseContext + attachContext(app, context); } - - ClassLoader cl = new RedirectClassLoader(); - try { - setClassLoader(context, cl); - if (Build.VERSION.SDK_INT >= 28) { - ((DelegateComponentFactory) componentFactory).loader = cl; - } - } catch (Exception e) { - Log.e(DynLoad.class.getSimpleName(), "", e); - } - - return null; + return app; } - private static Application createApp(Context context, ClassLoader cl, ApplicationInfo info) - throws ReflectiveOperationException { + private static boolean isDynLoader() { + return loader instanceof InjectedClassLoader; + } + + private static Application newApp(ApplicationInfo info) throws ReflectiveOperationException { // Create the receiver Application - Object app = cl.loadClass(info.className) + var app = (Application) loader.loadClass(info.className) .getConstructor(Object.class) .newInstance(apkData.getObject()); // Create the receiver component factory if (Build.VERSION.SDK_INT >= 28 && componentFactory != null) { - Object factory = cl.loadClass(info.appComponentFactory).newInstance(); - DelegateComponentFactory delegate = (DelegateComponentFactory) componentFactory; - delegate.loader = cl; + Object factory = loader.loadClass(info.appComponentFactory).newInstance(); + var delegate = (DelegateComponentFactory) componentFactory; delegate.receiver = (AppComponentFactory) factory; } - setClassLoader(context, cl); - - return (Application) app; + return app; } // Replace LoadedApk mClassLoader - private static void setClassLoader(Context context, ClassLoader cl) - throws NoSuchFieldException, IllegalAccessException { + private static void replaceClassLoader(Context context) { // Get ContextImpl while (context instanceof ContextWrapper) { context = ((ContextWrapper) context).getBaseContext(); } - Field mInfo = context.getClass().getDeclaredField("mPackageInfo"); - mInfo.setAccessible(true); - Object loadedApk = mInfo.get(context); - assert loadedApk != null; - Field mcl = loadedApk.getClass().getDeclaredField("mClassLoader"); - mcl.setAccessible(true); - mcl.set(loadedApk, cl); + try { + Field mInfo = context.getClass().getDeclaredField("mPackageInfo"); + mInfo.setAccessible(true); + Object loadedApk = mInfo.get(context); + Field mcl = loadedApk.getClass().getDeclaredField("mClassLoader"); + mcl.setAccessible(true); + mcl.set(loadedApk, new DelegateClassLoader()); + } catch (Exception e) { + // Actually impossible as this method is only called on API < 29, + // and API 21 - 28 do not restrict access to these methods/fields. + Log.e(DynLoad.class.getSimpleName(), "", e); + } } private static DynAPK.Data createApkData() { - DynAPK.Data data = new DynAPK.Data(); + var data = new DynAPK.Data(); data.setVersion(BuildConfig.STUB_VERSION); data.setClassToComponent(Mapping.inverseMap); data.setRootService(DelegateRootService.class);