Support stub APK loading down to Android 5.0

This commit is contained in:
topjohnwu 2021-01-26 03:40:25 -08:00
parent f1295cb7d6
commit fba83e2330
11 changed files with 141 additions and 116 deletions

View File

@ -13,7 +13,7 @@
tools:ignore="GoogleAppIndexingWarning,MissingApplicationIcon,UnusedAttribute"> tools:ignore="GoogleAppIndexingWarning,MissingApplicationIcon,UnusedAttribute">
<!-- Splash --> <!-- Splash -->
<activity android:name="a.u7"> <activity android:name="f.u7">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@ -42,7 +42,7 @@
<!-- Receiver --> <!-- Receiver -->
<receiver <receiver
android:name="a.r" android:name="yy.E"
android:directBootAware="true" android:directBootAware="true"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
@ -61,7 +61,7 @@
<!-- FileProvider --> <!-- FileProvider -->
<provider <provider
android:name="a.ii" android:name="fxQ.lk"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
android:directBootAware="true" android:directBootAware="true"
android:exported="false" android:exported="false"

View File

@ -1,5 +0,0 @@
package a;
import com.topjohnwu.magisk.FileProvider;
public class ii extends FileProvider {}

View File

@ -1,5 +0,0 @@
package a;
import com.topjohnwu.magisk.dummy.DummyReceiver;
public class r extends DummyReceiver {}

View File

@ -1,5 +0,0 @@
package a;
import com.topjohnwu.magisk.DownloadActivity;
public class u7 extends DownloadActivity {}

View File

@ -0,0 +1,33 @@
package com.topjohnwu.magisk;
import com.topjohnwu.magisk.utils.DynamicClassLoader;
import java.io.File;
class InjectedClassLoader extends DynamicClassLoader {
InjectedClassLoader(File apk) {
super(apk,
/* Use the base classloader as we do not want stub
* APK classes accessible from the main app */
Object.class.getClassLoader());
}
@Override
protected Class<?> 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;
}
}

View File

@ -10,32 +10,31 @@ import java.lang.reflect.Method;
public class DelegateApplication extends Application { public class DelegateApplication extends Application {
private Application delegate;
static boolean dynLoad = false; static boolean dynLoad = false;
private Application receiver;
@Override @Override
protected void attachBaseContext(Context base) { protected void attachBaseContext(Context base) {
super.attachBaseContext(base); super.attachBaseContext(base);
// Only dynamic load full APK if hidden and possible // Only dynamic load full APK if hidden and supported
dynLoad = Build.VERSION.SDK_INT >= 28 && dynLoad = Build.VERSION.SDK_INT >= 21 &&
!base.getPackageName().equals(BuildConfig.APPLICATION_ID); !base.getPackageName().equals(BuildConfig.APPLICATION_ID);
if (!dynLoad)
return;
delegate = InjectAPK.setup(this); receiver = InjectAPK.setup(this);
if (delegate != null) try { if (receiver != null) try {
// Call attachBaseContext without ContextImpl to show it is being wrapped // Call attachBaseContext without ContextImpl to show it is being wrapped
Method m = ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class); Method m = ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class);
m.setAccessible(true); m.setAccessible(true);
m.invoke(delegate, this); m.invoke(receiver, this);
} catch (Exception ignored) { /* Impossible */ } } catch (Exception ignored) { /* Impossible */ }
} }
@Override @Override
public void onConfigurationChanged(Configuration newConfig) { public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig); super.onConfigurationChanged(newConfig);
if (delegate != null) if (receiver != null)
delegate.onConfigurationChanged(newConfig); receiver.onConfigurationChanged(newConfig);
} }
} }

View File

@ -9,22 +9,15 @@ import android.content.BroadcastReceiver;
import android.content.ContentProvider; import android.content.ContentProvider;
import android.content.Intent; import android.content.Intent;
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 {
static DelegateComponentFactory INSTANCE;
ClassLoader loader; ClassLoader loader;
AppComponentFactory delegate; AppComponentFactory receiver;
interface DummyFactory<T> {
T create();
}
public DelegateComponentFactory() { public DelegateComponentFactory() {
InjectAPK.factory = this; INSTANCE = this;
} }
@Override @Override
@ -36,45 +29,39 @@ public class DelegateComponentFactory extends AppComponentFactory {
@Override @Override
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 (delegate != null) if (receiver != null)
return delegate.instantiateActivity(loader, Mapping.get(className), intent); return receiver.instantiateActivity(loader, className, intent);
return create(className, DownloadActivity::new); return create(className);
} }
@Override @Override
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 (delegate != null) if (receiver != null)
return delegate.instantiateReceiver(loader, Mapping.get(className), intent); return receiver.instantiateReceiver(loader, className, intent);
return create(className, DummyReceiver::new); return create(className);
} }
@Override @Override
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 (delegate != null) if (receiver != null)
return delegate.instantiateService(loader, Mapping.get(className), intent); return receiver.instantiateService(loader, className, intent);
return create(className, DummyService::new); return create(className);
} }
@Override @Override
public ContentProvider instantiateProvider(ClassLoader cl, String className) public ContentProvider instantiateProvider(ClassLoader cl, String className)
throws ClassNotFoundException, IllegalAccessException, InstantiationException { throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (loader == null) loader = cl; if (loader == null) loader = cl;
if (delegate != null) if (receiver != null)
return delegate.instantiateProvider(loader, Mapping.get(className)); return receiver.instantiateProvider(loader, className);
return create(className, DummyProvider::new); return create(className);
} }
/** private <T> T create(String name)
* Create the class or dummy implementation if creation failed throws ClassNotFoundException, IllegalAccessException, InstantiationException{
*/ return (T) loader.loadClass(name).newInstance();
private <T> T create(String name, DummyFactory<T> factory) {
try {
return (T) loader.loadClass(name).newInstance();
} catch (Exception ignored) {
return factory.create();
}
} }
} }

View File

@ -4,65 +4,97 @@ import android.app.AppComponentFactory;
import android.app.Application; import android.app.Application;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.ContextWrapper;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.util.Log; import android.util.Log;
import com.topjohnwu.magisk.utils.DynamicClassLoader;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.lang.reflect.Field;
public class InjectAPK { public class InjectAPK {
static DelegateComponentFactory factory; @SuppressWarnings("ResultOfMethodCallIgnored")
static Application setup(Context context) { static Application setup(Context context) {
// Get ContextImpl
while (context instanceof ContextWrapper) {
context = ((ContextWrapper) context).getBaseContext();
}
File apk = DynAPK.current(context); File apk = DynAPK.current(context);
File update = DynAPK.update(context); File update = DynAPK.update(context);
if (update.exists()) if (update.exists())
update.renameTo(apk); update.renameTo(apk);
Application delegate = null; Application result = null;
if (!apk.exists()) { if (!apk.exists()) {
// Try copying APK // Try copying APK
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(); ContentResolver resolver = context.getContentResolver();
try (InputStream is = resolver.openInputStream(uri)) { try (InputStream src = resolver.openInputStream(uri)) {
if (is != null) { if (src != null) {
try (OutputStream out = new FileOutputStream(apk)) { try (OutputStream out = new FileOutputStream(apk)) {
byte[] buf = new byte[4096]; 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); out.write(buf, 0, read);
} }
} }
} }
} catch (Exception e) { } catch (Exception ignored) {}
Log.e(InjectAPK.class.getSimpleName(), "", e);
}
} }
if (apk.exists()) { if (apk.exists()) {
ClassLoader cl = new DynamicClassLoader(apk, factory.loader); ClassLoader cl = new InjectedClassLoader(apk);
try { try {
// Create the delegate AppComponentFactory // Create the receiver Application
AppComponentFactory df = (AppComponentFactory) Object app = cl.loadClass(Mapping.get("APP")).getConstructor(Object.class)
cl.loadClass("androidx.core.app.CoreComponentFactory").newInstance();
// Create the delegate Application
delegate = (Application) cl.loadClass("a.e").getConstructor(Object.class)
.newInstance(DynAPK.pack(dynData())); .newInstance(DynAPK.pack(dynData()));
// If everything went well, set our loader and delegate // Create the receiver component factory
factory.delegate = df; Object factory = null;
factory.loader = cl; 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) { } catch (Exception e) {
Log.e(InjectAPK.class.getSimpleName(), "", e); Log.e(InjectAPK.class.getSimpleName(), "", e);
apk.delete(); 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() { private static DynAPK.Data dynData() {

View File

@ -1,27 +1,42 @@
package com.topjohnwu.magisk; package com.topjohnwu.magisk;
import com.topjohnwu.magisk.dummy.DummyReceiver;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; 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 { public class Mapping {
private static Map<String, String> map = new HashMap<>(); private static final Map<String, String> map = new HashMap<>();
public static Map<String, String> inverseMap; public static final Map<String, Class<?>> internalMap = new HashMap<>();
public static final Map<String, String> inverseMap;
static { static {
map.put("a.Qzw", "a.e"); map.put("a.Q", "com.topjohnwu.magisk.core.App");
map.put("a.u7", "a.c"); map.put("f.u7", "com.topjohnwu.magisk.core.SplashActivity");
map.put("a.ii", "a.p"); map.put("fxQ.lk", "com.topjohnwu.magisk.core.Provider");
map.put("a.r", "a.h"); map.put("yy.E", "com.topjohnwu.magisk.core.Receiver");
map.put("xt.R", "a.b"); map.put("xt.R", "com.topjohnwu.magisk.ui.MainActivity");
map.put("lt5.a", "a.m"); map.put("lt5.a", "com.topjohnwu.magisk.ui.surequest.SuRequestActivity");
map.put("d.s", "a.j"); map.put("d.s", "com.topjohnwu.magisk.core.download.DownloadService");
map.put("w.d", "androidx.work.impl.background.systemjob.SystemJobService"); 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()); inverseMap = new HashMap<>(map.size());
for (Map.Entry<String, String> e : map.entrySet()) { for (Map.Entry<String, String> e : map.entrySet()) {
inverseMap.put(e.getValue(), e.getKey()); 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) { public static String get(String name) {

View File

@ -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();
}
}

View File

@ -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;
}
}