Use AppComponentFactory to replace ClassLoader

This commit is contained in:
topjohnwu 2022-02-01 22:43:44 -08:00
parent 7f65f7d3ca
commit 355341f0ab
7 changed files with 139 additions and 83 deletions

View File

@ -3,6 +3,7 @@ package com.topjohnwu.magisk;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import android.content.Context; import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import java.io.File; import java.io.File;
@ -16,24 +17,35 @@ public class DynAPK {
private static File dynDir; private static File dynDir;
private static Method addAssetPath; private static Method addAssetPath;
private static File getDynDir(Context c) { private static File getDynDir(ApplicationInfo info) {
if (dynDir == null) { if (dynDir == null) {
final String dataDir;
if (SDK_INT >= 24) { if (SDK_INT >= 24) {
// Use protected context to allow directBootAware // Use device protected path to allow directBootAware
c = c.createDeviceProtectedStorageContext(); dataDir = info.deviceProtectedDataDir;
} else {
dataDir = info.dataDir;
} }
dynDir = new File(c.getFilesDir().getParent(), "dyn"); dynDir = new File(dataDir, "dyn");
dynDir.mkdir(); dynDir.mkdirs();
} }
return dynDir; return dynDir;
} }
public static File current(Context c) { 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) { 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) { public static void addAssetPath(AssetManager asset, String path) {

View File

@ -30,3 +30,15 @@ class RedirectClassLoader extends ClassLoader {
return clz == null ? super.loadClass(name, resolve) : clz; 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);
}
}

View File

@ -2,11 +2,8 @@ package com.topjohnwu.magisk;
import android.app.Application; import android.app.Application;
import android.content.Context; import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.Configuration; import android.content.res.Configuration;
import java.lang.reflect.Method;
import io.michaelrocks.paranoid.Obfuscate; import io.michaelrocks.paranoid.Obfuscate;
@Obfuscate @Obfuscate
@ -17,14 +14,7 @@ public class DelegateApplication extends Application {
@Override @Override
protected void attachBaseContext(Context base) { protected void attachBaseContext(Context base) {
super.attachBaseContext(base); super.attachBaseContext(base);
receiver = DynLoad.createAndSetupApp(this);
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 */ }
} }
@Override @Override

View File

@ -8,20 +8,25 @@ import android.app.Service;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ContentProvider; import android.content.ContentProvider;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ApplicationInfo;
@SuppressLint("NewApi") @SuppressLint("NewApi")
public class DelegateComponentFactory extends AppComponentFactory { public class DelegateComponentFactory extends AppComponentFactory {
ClassLoader loader;
AppComponentFactory receiver; AppComponentFactory receiver;
public DelegateComponentFactory() { public DelegateComponentFactory() {
DynLoad.componentFactory = this; DynLoad.componentFactory = this;
} }
@Override
public ClassLoader instantiateClassLoader(ClassLoader cl, ApplicationInfo info) {
DynLoad.loadAPK(info);
return new DelegateClassLoader();
}
@Override @Override
public Application instantiateApplication(ClassLoader cl, String className) { public Application instantiateApplication(ClassLoader cl, String className) {
if (loader == null) loader = cl;
return new DelegateApplication(); return new DelegateApplication();
} }
@ -29,7 +34,7 @@ public class DelegateComponentFactory extends AppComponentFactory {
public Activity instantiateActivity(ClassLoader cl, String className, Intent intent) public Activity instantiateActivity(ClassLoader cl, String className, Intent intent)
throws ClassNotFoundException, IllegalAccessException, InstantiationException { throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (receiver != null) if (receiver != null)
return receiver.instantiateActivity(loader, className, intent); return receiver.instantiateActivity(DynLoad.loader, className, intent);
return create(className); return create(className);
} }
@ -37,7 +42,7 @@ public class DelegateComponentFactory extends AppComponentFactory {
public BroadcastReceiver instantiateReceiver(ClassLoader cl, String className, Intent intent) public BroadcastReceiver instantiateReceiver(ClassLoader cl, String className, Intent intent)
throws ClassNotFoundException, IllegalAccessException, InstantiationException { throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (receiver != null) if (receiver != null)
return receiver.instantiateReceiver(loader, className, intent); return receiver.instantiateReceiver(DynLoad.loader, className, intent);
return create(className); return create(className);
} }
@ -45,7 +50,7 @@ public class DelegateComponentFactory extends AppComponentFactory {
public Service instantiateService(ClassLoader cl, String className, Intent intent) public Service instantiateService(ClassLoader cl, String className, Intent intent)
throws ClassNotFoundException, IllegalAccessException, InstantiationException { throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (receiver != null) if (receiver != null)
return receiver.instantiateService(loader, className, intent); return receiver.instantiateService(DynLoad.loader, className, intent);
return create(className); return create(className);
} }
@ -53,13 +58,13 @@ public class DelegateComponentFactory extends AppComponentFactory {
public ContentProvider instantiateProvider(ClassLoader cl, String className) public ContentProvider instantiateProvider(ClassLoader cl, String className)
throws ClassNotFoundException, IllegalAccessException, InstantiationException { throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (receiver != null) if (receiver != null)
return receiver.instantiateProvider(loader, className); return receiver.instantiateProvider(DynLoad.loader, className);
return create(className); return create(className);
} }
private <T> T create(String name) private <T> T create(String name)
throws ClassNotFoundException, IllegalAccessException, InstantiationException{ throws ClassNotFoundException, IllegalAccessException, InstantiationException{
return (T) loader.loadClass(name).newInstance(); return (T) DynLoad.loader.loadClass(name).newInstance();
} }
} }

View File

@ -5,7 +5,6 @@ import android.content.ContextWrapper;
import android.util.Log; import android.util.Log;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import io.michaelrocks.paranoid.Obfuscate; import io.michaelrocks.paranoid.Obfuscate;
@ -18,7 +17,7 @@ public class DelegateRootService extends ContextWrapper {
@Override @Override
protected void attachBaseContext(Context base) { protected void attachBaseContext(Context base) {
if (DynLoad.inject(base) == null) if (DynLoad.createApp(base) == null)
return; return;
// Create the actual RootService and call its attachBaseContext // 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); Constructor<?> ctor = DynLoad.apkData.getRootService().getConstructor(Object.class);
ctor.setAccessible(true); ctor.setAccessible(true);
Object service = ctor.newInstance(this); Object service = ctor.newInstance(this);
Method m = ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class); DynLoad.attachContext(service, base);
m.setAccessible(true);
m.invoke(service, base);
} catch (Exception e) { } catch (Exception e) {
Log.e(DelegateRootService.class.getSimpleName(), "", e); Log.e(DelegateRootService.class.getSimpleName(), "", e);
} }

View File

@ -118,8 +118,7 @@ public class DownloadActivity extends Activity {
File apk = dynLoad ? DynAPK.current(this) : new File(getCacheDir(), "manager.apk"); File apk = dynLoad ? DynAPK.current(this) : new File(getCacheDir(), "manager.apk");
request(apkLink).setExecutor(AsyncTask.THREAD_POOL_EXECUTOR).getAsFile(apk, file -> { request(apkLink).setExecutor(AsyncTask.THREAD_POOL_EXECUTOR).getAsFile(apk, file -> {
if (dynLoad) { if (dynLoad) {
DynLoad.setup(this); // TODO
runOnUiThread(onSuccess);
} else { } else {
var receiver = APKInstall.register(this, BuildConfig.APPLICATION_ID, onSuccess); var receiver = APKInstall.register(this, BuildConfig.APPLICATION_ID, onSuccess);
APKInstall.installapk(this, file); APKInstall.installapk(this, file);

View File

@ -2,7 +2,6 @@ package com.topjohnwu.magisk;
import android.app.AppComponentFactory; import android.app.AppComponentFactory;
import android.app.Application; import android.app.Application;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.ContextWrapper; import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
@ -10,6 +9,7 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Environment;
import android.util.Log; import android.util.Log;
import com.topjohnwu.magisk.utils.APKInstall; import com.topjohnwu.magisk.utils.APKInstall;
@ -20,6 +20,7 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method;
import io.michaelrocks.paranoid.Obfuscate; import io.michaelrocks.paranoid.Obfuscate;
@ -27,23 +28,52 @@ import io.michaelrocks.paranoid.Obfuscate;
@SuppressWarnings("ResultOfMethodCallIgnored") @SuppressWarnings("ResultOfMethodCallIgnored")
public class DynLoad { public class DynLoad {
// The current active classloader
static ClassLoader loader = new RedirectClassLoader();
static Object componentFactory; static Object componentFactory;
static final DynAPK.Data apkData = createApkData(); static final DynAPK.Data apkData = createApkData();
// Dynamically load APK, inject ClassLoader into ContextImpl, then private static boolean loadedApk = false;
// create the actual Application instance from the loaded APK
static Application inject(Context context) { static void attachContext(Object o, Context context) {
File apk = DynAPK.current(context); if (!(o instanceof ContextWrapper))
File update = DynAPK.update(context); 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()) { if (update.exists()) {
// Rename from update // Rename from update
update.renameTo(apk); update.renameTo(apk);
} }
if (BuildConfig.DEBUG) {
// Copy from external for easier development // Copy from external for easier development
File external = new File(context.getExternalFilesDir(null), "magisk.apk"); 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()) { if (external.exists()) {
try { try {
var in = new FileInputStream(external); var in = new FileInputStream(external);
@ -60,19 +90,33 @@ public class DynLoad {
} }
} }
if (!apk.exists() && !context.getPackageName().equals(BuildConfig.APPLICATION_ID)) { if (apk.exists()) {
// Copy from previous app 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") Uri uri = new Uri.Builder().scheme("content")
.authority("com.topjohnwu.magisk.provider") .authority("com.topjohnwu.magisk.provider")
.encodedPath("apk_file").build(); .encodedPath("apk_file").build();
ContentResolver resolver = context.getContentResolver();
try { try {
InputStream src = resolver.openInputStream(uri); InputStream src = context.getContentResolver().openInputStream(uri);
if (src != null) { if (src != null) {
var out = new FileOutputStream(apk); var out = new FileOutputStream(apk);
try (src; out) { try (src; out) {
APKInstall.transfer(src, out); APKInstall.transfer(src, out);
} }
loader = new InjectedClassLoader(apk);
} }
} catch (IOException e) { } catch (IOException e) {
Log.e(DynLoad.class.getSimpleName(), "", e); Log.e(DynLoad.class.getSimpleName(), "", e);
@ -80,12 +124,11 @@ public class DynLoad {
} }
} }
if (apk.exists()) { if (isDynLoader()) {
ClassLoader cl = new InjectedClassLoader(apk);
PackageManager pm = context.getPackageManager(); PackageManager pm = context.getPackageManager();
PackageInfo pkgInfo = pm.getPackageArchiveInfo(apk.getPath(), 0); PackageInfo pkgInfo = pm.getPackageArchiveInfo(apk.getPath(), 0);
try { try {
return createApp(context, cl, pkgInfo.applicationInfo); return newApp(pkgInfo.applicationInfo);
} catch (ReflectiveOperationException e) { } catch (ReflectiveOperationException e) {
Log.e(DynLoad.class.getSimpleName(), "", e); Log.e(DynLoad.class.getSimpleName(), "", e);
apk.delete(); apk.delete();
@ -95,65 +138,63 @@ public class DynLoad {
return null; return null;
} }
// Inject and create Application, or setup redirections for the current app // Stub app setup entry
static Application setup(Context context) { static Application createAndSetupApp(Application context) {
Application app = inject(context); // On API >= 29, AppComponentFactory will replace the ClassLoader
if (Build.VERSION.SDK_INT < 29)
replaceClassLoader(context);
Application app = createApp(context);
if (app != null) { if (app != null) {
// Send real application to attachBaseContext
attachContext(app, context);
}
return app; return app;
} }
ClassLoader cl = new RedirectClassLoader(); private static boolean isDynLoader() {
try { return loader instanceof InjectedClassLoader;
setClassLoader(context, cl);
if (Build.VERSION.SDK_INT >= 28) {
((DelegateComponentFactory) componentFactory).loader = cl;
}
} catch (Exception e) {
Log.e(DynLoad.class.getSimpleName(), "", e);
} }
return null; private static Application newApp(ApplicationInfo info) throws ReflectiveOperationException {
}
private static Application createApp(Context context, ClassLoader cl, ApplicationInfo info)
throws ReflectiveOperationException {
// Create the receiver Application // Create the receiver Application
Object app = cl.loadClass(info.className) var app = (Application) loader.loadClass(info.className)
.getConstructor(Object.class) .getConstructor(Object.class)
.newInstance(apkData.getObject()); .newInstance(apkData.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 = cl.loadClass(info.appComponentFactory).newInstance(); Object factory = loader.loadClass(info.appComponentFactory).newInstance();
DelegateComponentFactory delegate = (DelegateComponentFactory) componentFactory; var delegate = (DelegateComponentFactory) componentFactory;
delegate.loader = cl;
delegate.receiver = (AppComponentFactory) factory; delegate.receiver = (AppComponentFactory) factory;
} }
setClassLoader(context, cl); return app;
return (Application) app;
} }
// Replace LoadedApk mClassLoader // Replace LoadedApk mClassLoader
private static void setClassLoader(Context context, ClassLoader cl) private static void replaceClassLoader(Context context) {
throws NoSuchFieldException, IllegalAccessException {
// Get ContextImpl // Get ContextImpl
while (context instanceof ContextWrapper) { while (context instanceof ContextWrapper) {
context = ((ContextWrapper) context).getBaseContext(); context = ((ContextWrapper) context).getBaseContext();
} }
try {
Field mInfo = context.getClass().getDeclaredField("mPackageInfo"); Field mInfo = context.getClass().getDeclaredField("mPackageInfo");
mInfo.setAccessible(true); mInfo.setAccessible(true);
Object loadedApk = mInfo.get(context); Object loadedApk = mInfo.get(context);
assert loadedApk != null;
Field mcl = loadedApk.getClass().getDeclaredField("mClassLoader"); Field mcl = loadedApk.getClass().getDeclaredField("mClassLoader");
mcl.setAccessible(true); mcl.setAccessible(true);
mcl.set(loadedApk, cl); 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() { private static DynAPK.Data createApkData() {
DynAPK.Data data = new DynAPK.Data(); var data = new DynAPK.Data();
data.setVersion(BuildConfig.STUB_VERSION); data.setVersion(BuildConfig.STUB_VERSION);
data.setClassToComponent(Mapping.inverseMap); data.setClassToComponent(Mapping.inverseMap);
data.setRootService(DelegateRootService.class); data.setRootService(DelegateRootService.class);