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">
<!-- Splash -->
<activity android:name="a.u7">
<activity android:name="f.u7">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@ -42,7 +42,7 @@
<!-- Receiver -->
<receiver
android:name="a.r"
android:name="yy.E"
android:directBootAware="true"
android:exported="false">
<intent-filter>
@ -61,7 +61,7 @@
<!-- FileProvider -->
<provider
android:name="a.ii"
android:name="fxQ.lk"
android:authorities="${applicationId}.provider"
android:directBootAware="true"
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 {
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);
}
}

View File

@ -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> {
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> T create(String name, DummyFactory<T> factory) {
try {
return (T) loader.loadClass(name).newInstance();
} catch (Exception ignored) {
return factory.create();
}
private <T> T create(String name)
throws ClassNotFoundException, IllegalAccessException, InstantiationException{
return (T) loader.loadClass(name).newInstance();
}
}

View File

@ -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() {

View File

@ -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<String, String> map = new HashMap<>();
public static Map<String, String> inverseMap;
private static final Map<String, String> map = new HashMap<>();
public static final Map<String, Class<?>> internalMap = new HashMap<>();
public static final Map<String, String> 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<String, String> 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) {

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