Update stub implementation

This commit is contained in:
topjohnwu 2024-07-09 21:49:48 -07:00
parent 88e8e15607
commit 6b81716440
11 changed files with 131 additions and 147 deletions

View File

@ -21,7 +21,9 @@ class RootUtils(stub: Any?) : RootService() {
private val className: String = stub?.javaClass?.name ?: javaClass.name private val className: String = stub?.javaClass?.name ?: javaClass.name
private lateinit var am: ActivityManager private lateinit var am: ActivityManager
constructor() : this(null) { constructor() : this(null)
init {
Timber.plant(object : Timber.DebugTree() { Timber.plant(object : Timber.DebugTree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
super.log(priority, "Magisk", message, t) super.log(priority, "Magisk", message, t)

View File

@ -29,4 +29,4 @@
-allowaccessmodification -allowaccessmodification
-keepclassmembers class com.topjohnwu.magisk.dummy.* { <init>(); } -keepclassmembers class com.topjohnwu.magisk.dummy.* { <init>(); }
-keepclassmembers class com.topjohnwu.magisk.DownloadActivity { <init>(); } -keepclassmembers class com.topjohnwu.magisk.DownloadActivity { <init>(); }
-keepclassmembers class com.topjohnwu.magisk.DelegateRootService { <init>(); } -keepclassmembers class com.topjohnwu.magisk.StubRootService { <init>(); }

View File

@ -1,26 +1,23 @@
package com.topjohnwu.magisk; package com.topjohnwu.magisk;
import android.app.job.JobService;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.ServiceInfo;
import com.topjohnwu.magisk.dummy.DummyProvider; import com.topjohnwu.magisk.dummy.DummyProvider;
import com.topjohnwu.magisk.dummy.DummyReceiver; import com.topjohnwu.magisk.dummy.DummyReceiver;
import com.topjohnwu.magisk.dummy.DummyService; import com.topjohnwu.magisk.dummy.DummyService;
import com.topjohnwu.magisk.utils.DynamicClassLoader;
import java.io.File;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
// Wrap the actual classloader as we only want to resolve classname // Wrap the actual classloader as we only want to resolve classname
// mapping when loading from platform (via LoadedApk.mClassLoader) // mapping when loading from platform (via LoadedApk.mClassLoader)
class AppClassLoader extends ClassLoader { class MappingClassLoader extends ClassLoader {
final Map<String, String> mapping = new HashMap<>();
AppClassLoader(File apk) { private final Map<String, String> mapping;
super(new DynamicClassLoader(apk));
MappingClassLoader(ClassLoader parent, Map<String, String> m) {
super(parent);
mapping = m;
} }
@Override @Override
@ -29,72 +26,6 @@ class AppClassLoader extends ClassLoader {
name = clz != null ? clz : name; name = clz != null ? clz : name;
return super.loadClass(name, resolve); return super.loadClass(name, resolve);
} }
void updateComponentMap(PackageInfo stub, PackageInfo app) {
{
var src = stub.activities;
var dest = app.activities;
final ActivityInfo sa;
final ActivityInfo da;
final ActivityInfo sb;
final ActivityInfo db;
if (src[0].exported) {
sa = src[0];
sb = src[1];
} else {
sa = src[1];
sb = src[0];
}
if (dest[0].exported) {
da = dest[0];
db = dest[1];
} else {
da = dest[1];
db = dest[0];
}
mapping.put(sa.name, da.name);
mapping.put(sb.name, db.name);
}
{
var src = stub.services;
var dest = app.services;
final ServiceInfo sa;
final ServiceInfo da;
final ServiceInfo sb;
final ServiceInfo db;
if (JobService.PERMISSION_BIND.equals(src[0].permission)) {
sa = src[0];
sb = src[1];
} else {
sa = src[1];
sb = src[0];
}
if (JobService.PERMISSION_BIND.equals(dest[0].permission)) {
da = dest[0];
db = dest[1];
} else {
da = dest[1];
db = dest[0];
}
mapping.put(sa.name, da.name);
mapping.put(sb.name, db.name);
}
{
var src = stub.receivers;
var dest = app.receivers;
mapping.put(src[0].name, dest[0].name);
}
{
var src = stub.providers;
var dest = app.providers;
mapping.put(src[0].name, dest[0].name);
}
}
} }
class StubClassLoader extends ClassLoader { class StubClassLoader extends ClassLoader {

View File

@ -1,30 +0,0 @@
package com.topjohnwu.magisk;
import android.app.Application;
import android.content.Context;
import android.content.res.Configuration;
public class DelegateApplication extends Application {
private Application receiver;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
receiver = DynLoad.createAndSetupApp(this);
}
@Override
public void onCreate() {
super.onCreate();
if (receiver != null)
receiver.onCreate();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (receiver != null)
receiver.onConfigurationChanged(newConfig);
}
}

View File

@ -30,7 +30,7 @@ public class DelegateComponentFactory extends AppComponentFactory {
@Override @Override
public Application instantiateApplication(ClassLoader cl, String className) { public Application instantiateApplication(ClassLoader cl, String className) {
return new DelegateApplication(); return new StubApplication();
} }
@Override @Override

View File

@ -60,14 +60,6 @@ public class DownloadActivity extends Activity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (DynLoad.activeClassLoader instanceof AppClassLoader) {
// For some reason activity is created before Application.attach(),
// relaunch the activity using the same intent
finishAffinity();
startActivity(getIntent());
return;
}
themed = new ContextThemeWrapper(this, android.R.style.Theme_DeviceDefault); themed = new ContextThemeWrapper(this, android.R.style.Theme_DeviceDefault);
// Only download and dynamic load full APK if hidden // Only download and dynamic load full APK if hidden
@ -187,7 +179,7 @@ public class DownloadActivity extends Activity {
} else { } else {
File res = new File(getCodeCacheDir(), "res.apk"); File res = new File(getCodeCacheDir(), "res.apk");
try (var out = new ZipOutputStream(new FileOutputStream(res))) { try (var out = new ZipOutputStream(new FileOutputStream(res))) {
// AndroidManifest.xml is reuqired on Android 6-, and directory support is broken on Android 9-10 // AndroidManifest.xml is required on Android 6-, and directory support is broken on Android 9-10
out.putNextEntry(new ZipEntry("AndroidManifest.xml")); out.putNextEntry(new ZipEntry("AndroidManifest.xml"));
try (var stubApk = new ZipFile(getPackageCodePath())) { try (var stubApk = new ZipFile(getPackageCodePath())) {
APKInstall.transfer(stubApk.getInputStream(stubApk.getEntry("AndroidManifest.xml")), out); APKInstall.transfer(stubApk.getInputStream(stubApk.getEntry("AndroidManifest.xml")), out);

View File

@ -4,14 +4,18 @@ import static com.topjohnwu.magisk.BuildConfig.APPLICATION_ID;
import android.app.AppComponentFactory; import android.app.AppComponentFactory;
import android.app.Application; import android.app.Application;
import android.app.job.JobService;
import android.content.Context; import android.content.Context;
import android.content.ContextWrapper; import android.content.ContextWrapper;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.os.Build; import android.os.Build;
import android.util.Log; import android.util.Log;
import com.topjohnwu.magisk.utils.APKInstall; import com.topjohnwu.magisk.utils.APKInstall;
import com.topjohnwu.magisk.utils.DynamicClassLoader;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -20,6 +24,7 @@ import java.io.IOException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map;
@SuppressWarnings("ResultOfMethodCallIgnored") @SuppressWarnings("ResultOfMethodCallIgnored")
public class DynLoad { public class DynLoad {
@ -31,7 +36,7 @@ public class DynLoad {
var data = new StubApk.Data(); var data = new StubApk.Data();
data.setVersion(BuildConfig.STUB_VERSION); data.setVersion(BuildConfig.STUB_VERSION);
data.setClassToComponent(new HashMap<>()); data.setClassToComponent(new HashMap<>());
data.setRootService(DelegateRootService.class); data.setRootService(StubRootService.class);
return data; return data;
} }
@ -46,7 +51,7 @@ public class DynLoad {
} }
// Dynamically load APK from internal, external storage, or previous app // Dynamically load APK from internal, external storage, or previous app
static AppClassLoader loadApk(Context context) { static DynamicClassLoader loadApk(Context context) {
File apk = StubApk.current(context); File apk = StubApk.current(context);
File update = StubApk.update(context); File update = StubApk.update(context);
@ -82,7 +87,7 @@ public class DynLoad {
if (apk.exists()) { if (apk.exists()) {
apk.setReadOnly(); apk.setReadOnly();
return new AppClassLoader(apk); return new DynamicClassLoader(apk);
} }
// If no APK is loaded, attempt to copy from previous app // If no APK is loaded, attempt to copy from previous app
@ -96,7 +101,7 @@ public class DynLoad {
try (src; out) { try (src; out) {
APKInstall.transfer(src, out); APKInstall.transfer(src, out);
} }
return new AppClassLoader(apk); return new DynamicClassLoader(apk);
} catch (PackageManager.NameNotFoundException ignored) { } catch (PackageManager.NameNotFoundException ignored) {
} catch (IOException e) { } catch (IOException e) {
Log.e(DynLoad.class.getSimpleName(), "", e); Log.e(DynLoad.class.getSimpleName(), "", e);
@ -107,8 +112,8 @@ public class DynLoad {
return null; return null;
} }
// Dynamically load APK and create the Application instance from the loaded APK // Dynamically load APK and initialize the application
static Application createAndSetupApp(Application context) { static void loadAndInitializeApp(Application context) {
// On API >= 29, AppComponentFactory will replace the ClassLoader for us // On API >= 29, AppComponentFactory will replace the ClassLoader for us
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
replaceClassLoader(context); replaceClassLoader(context);
@ -120,10 +125,10 @@ public class DynLoad {
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE; | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
var pm = context.getPackageManager(); var pm = context.getPackageManager();
final PackageInfo info; final PackageInfo stubInfo;
try { try {
// noinspection WrongConstant // noinspection WrongConstant
info = pm.getPackageInfo(context.getPackageName(), flags); stubInfo = pm.getPackageInfo(context.getPackageName(), flags);
} catch (PackageManager.NameNotFoundException e) { } catch (PackageManager.NameNotFoundException e) {
// Impossible // Impossible
throw new RuntimeException(e); throw new RuntimeException(e);
@ -134,20 +139,19 @@ public class DynLoad {
final var cl = loadApk(context); final var cl = loadApk(context);
if (cl != null) try { if (cl != null) try {
// noinspection WrongConstant // noinspection WrongConstant
var pkgInfo = pm.getPackageArchiveInfo(apk.getPath(), flags); var apkInfo = pm.getPackageArchiveInfo(apk.getPath(), flags);
cl.updateComponentMap(info, pkgInfo); var mapping = generateMapping(stubInfo, apkInfo);
var appInfo = pkgInfo.applicationInfo;
var data = createApkData(); var data = createApkData();
var map = data.getClassToComponent(); var map = data.getClassToComponent();
// Create the inverse mapping (class to component name) // Create the inverse mapping (class to component name)
for (var e : cl.mapping.entrySet()) { for (var e : mapping.entrySet()) {
map.put(e.getValue(), e.getKey()); map.put(e.getValue(), e.getKey());
} }
// Create the receiver Application var appInfo = apkInfo.applicationInfo;
var app = (Application) cl.loadClass(appInfo.className) // Create the receiver Application with proper constructor
var app = cl.loadClass(appInfo.className)
.getConstructor(Object.class) .getConstructor(Object.class)
.newInstance(data.getObject()); .newInstance(data.getObject());
@ -162,19 +166,17 @@ public class DynLoad {
} }
} }
activeClassLoader = cl; activeClassLoader = new MappingClassLoader(cl, mapping);
// Send real application to attachBaseContext // Call Application.attachBaseContext
attachContext(app, context); attachContext(app, context);
return app;
} catch (Exception e) { } catch (Exception e) {
Log.e(DynLoad.class.getSimpleName(), "", e); Log.e(DynLoad.class.getSimpleName(), "", e);
apk.delete(); apk.delete();
} else {
// Dynamic loading failed, use normal stub classloader
activeClassLoader = new StubClassLoader(stubInfo);
} }
activeClassLoader = new StubClassLoader(info);
return null;
} }
// Replace LoadedApk mClassLoader // Replace LoadedApk mClassLoader
@ -198,4 +200,72 @@ public class DynLoad {
Log.e(DynLoad.class.getSimpleName(), "", e); Log.e(DynLoad.class.getSimpleName(), "", e);
} }
} }
private static Map<String, String> generateMapping(PackageInfo stub, PackageInfo app) {
var mapping = new HashMap<String, String>();
{
var src = stub.activities;
var dest = app.activities;
final ActivityInfo sa;
final ActivityInfo da;
final ActivityInfo sb;
final ActivityInfo db;
if (src[0].exported) {
sa = src[0];
sb = src[1];
} else {
sa = src[1];
sb = src[0];
}
if (dest[0].exported) {
da = dest[0];
db = dest[1];
} else {
da = dest[1];
db = dest[0];
}
mapping.put(sa.name, da.name);
mapping.put(sb.name, db.name);
}
{
var src = stub.services;
var dest = app.services;
final ServiceInfo sa;
final ServiceInfo da;
final ServiceInfo sb;
final ServiceInfo db;
if (JobService.PERMISSION_BIND.equals(src[0].permission)) {
sa = src[0];
sb = src[1];
} else {
sa = src[1];
sb = src[0];
}
if (JobService.PERMISSION_BIND.equals(dest[0].permission)) {
da = dest[0];
db = dest[1];
} else {
da = dest[1];
db = dest[0];
}
mapping.put(sa.name, da.name);
mapping.put(sb.name, db.name);
}
{
var src = stub.receivers;
var dest = app.receivers;
mapping.put(src[0].name, dest[0].name);
}
{
var src = stub.providers;
var dest = app.providers;
mapping.put(src[0].name, dest[0].name);
}
return mapping;
}
} }

View File

@ -0,0 +1,12 @@
package com.topjohnwu.magisk;
import android.app.Application;
import android.content.Context;
public class StubApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
DynLoad.loadAndInitializeApp(this);
}
}

View File

@ -9,9 +9,9 @@ import android.util.Log;
import java.io.File; import java.io.File;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
public class DelegateRootService extends ContextWrapper { public class StubRootService extends ContextWrapper {
public DelegateRootService() { public StubRootService() {
super(null); super(null);
} }
@ -37,7 +37,7 @@ public class DelegateRootService extends ContextWrapper {
Object service = ctor.newInstance(this); Object service = ctor.newInstance(this);
DynLoad.attachContext(service, base); DynLoad.attachContext(service, base);
} catch (Exception e) { } catch (Exception e) {
Log.e(DelegateRootService.class.getSimpleName(), "", e); Log.e(StubRootService.class.getSimpleName(), "", e);
} }
} }
} }

View File

@ -2,13 +2,20 @@ import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.tasks.* import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.PrintStream import java.io.PrintStream
import java.security.SecureRandom import java.security.SecureRandom
import java.util.* import java.util.Random
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherOutputStream import javax.crypto.CipherOutputStream
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
@ -169,7 +176,7 @@ abstract class ManifestUpdater: DefaultTask() {
|<application |<application
| android:appComponentFactory="$factoryPkg.$factoryClass" | android:appComponentFactory="$factoryPkg.$factoryClass"
| android:name="$appPkg.$appClass"""".ind(1) | android:name="$appPkg.$appClass"""".ind(1)
).replace(Regex(".*\\<\\/application"), components + "\n </application") ).replace(Regex(".*\\<\\/application"), "$components\n </application")
outputManifest.get().asFile.writeText(manifest) outputManifest.get().asFile.writeText(manifest)
} }
} }
@ -218,7 +225,7 @@ fun genStubClasses(factoryOutDir: File, appOutDir: File) {
} }
genClass("DelegateComponentFactory", factoryOutDir) genClass("DelegateComponentFactory", factoryOutDir)
genClass("DelegateApplication", appOutDir) genClass("StubApplication", appOutDir)
} }
fun genEncryptedResources(res: ByteArray, outDir: File) { fun genEncryptedResources(res: ByteArray, outDir: File) {

View File

@ -26,6 +26,6 @@ android.injected.testOnly=false
android.nonFinalResIds=false android.nonFinalResIds=false
# Magisk # Magisk
magisk.stubVersion=39 magisk.stubVersion=40
magisk.versionCode=27003 magisk.versionCode=27003
magisk.ondkVersion=r27.2 magisk.ondkVersion=r27.2