Generate class mapping at runtime

This commit is contained in:
topjohnwu 2022-02-13 06:22:42 -08:00
parent a4aa4a91a3
commit c09b4dabc4
7 changed files with 199 additions and 125 deletions

View File

@ -74,13 +74,6 @@ fun genKeyData(keysDir: File, outSrc: File) {
}
fun genStubManifest(srcDir: File, outDir: File): String {
class Component(
val real: String,
val stub: String,
val xml: String,
val genClass: Boolean = false
)
outDir.deleteRecursively()
val mainPkgDir = File(outDir, "com/topjohnwu/magisk")
@ -88,25 +81,9 @@ fun genStubManifest(srcDir: File, outDir: File): String {
fun String.ind(level: Int) = replaceIndentByMargin(" ".repeat(level))
val cmpList = mutableListOf<Component>()
val cmpList = mutableListOf<String>()
cmpList.add(Component(
"androidx.core.app.CoreComponentFactory",
"DelegateComponentFactory",
"",
true
))
cmpList.add(Component(
"com.topjohnwu.magisk.core.App",
"DelegateApplication",
"",
true
))
cmpList.add(Component(
"com.topjohnwu.magisk.core.Provider",
"dummy.DummyProvider",
cmpList.add(
"""
|<provider
| android:name="%s"
@ -114,11 +91,9 @@ fun genStubManifest(srcDir: File, outDir: File): String {
| android:directBootAware="true"
| android:exported="false"
| android:grantUriPermissions="true" />""".ind(2)
))
)
cmpList.add(Component(
"com.topjohnwu.magisk.core.Receiver",
"dummy.DummyReceiver",
cmpList.add(
"""
|<receiver
| android:name="%s"
@ -135,11 +110,9 @@ fun genStubManifest(srcDir: File, outDir: File): String {
| <data android:scheme="package" />
| </intent-filter>
|</receiver>""".ind(2)
))
)
cmpList.add(Component(
"com.topjohnwu.magisk.ui.MainActivity",
"DownloadActivity",
cmpList.add(
"""
|<activity
| android:name="%s"
@ -149,11 +122,9 @@ fun genStubManifest(srcDir: File, outDir: File): String {
| <category android:name="android.intent.category.LAUNCHER" />
| </intent-filter>
|</activity>""".ind(2)
))
)
cmpList.add(Component(
"com.topjohnwu.magisk.ui.surequest.SuRequestActivity",
"",
cmpList.add(
"""
|<activity
| android:name="%s"
@ -167,26 +138,22 @@ fun genStubManifest(srcDir: File, outDir: File): String {
| <category android:name="android.intent.category.DEFAULT"/>
| </intent-filter>
|</activity>""".ind(2)
))
)
cmpList.add(Component(
"com.topjohnwu.magisk.core.download.DownloadService",
"",
cmpList.add(
"""
|<service
| android:name="%s"
| android:exported="false" />""".trimIndent().ind(2)
))
| android:exported="false" />""".ind(2)
)
cmpList.add(Component(
"com.topjohnwu.magisk.core.JobService",
"",
cmpList.add(
"""
|<service
| android:name="%s"
| android:exported="false"
| android:permission="android.permission.BIND_JOB_SERVICE" />""".ind(2)
))
)
val names = mutableListOf<String>()
names.addAll(c1)
@ -194,33 +161,38 @@ fun genStubManifest(srcDir: File, outDir: File): String {
names.addAll(c3.subList(0, 10))
names.shuffle(RANDOM)
val pkgNames = names.subList(0, 50)
val pkgNames = names
// Distinct by lower case to support case insensitive file systems
.distinctBy { it.toLowerCase(Locale.ROOT) }
// Old Android does not support capitalized package names
// Check Android 7.0.0 PackageParser#buildClassName
.map { it.decapitalize(Locale.ROOT) }
var idx = 0
fun isJavaKeyword(name: String) = when (name) {
"do", "if", "for", "int", "new", "try" -> true
else -> false
}
fun genCmpName() : String {
var pkgName : String
val cmps = mutableListOf<String>()
val usedNames = mutableListOf<String>()
fun genCmpName(): String {
var pkgName: String
do {
pkgName = pkgNames[idx++]
pkgName = pkgNames.random(kRANDOM)
} while (isJavaKeyword(pkgName))
var clzName : String
var clzName: String
do {
clzName = names.random(kRANDOM)
} while (isJavaKeyword(clzName))
return "${pkgName}.${clzName}"
val cmp = "${pkgName}.${clzName}"
usedNames.add(cmp)
return cmp
}
fun genClass(clzName: String, type: String) {
fun genClass(type: String) {
val clzName = genCmpName()
val (pkg, name) = clzName.split('.')
val pkgDir = File(outDir, pkg)
pkgDir.mkdir()
@ -230,41 +202,20 @@ fun genStubManifest(srcDir: File, outDir: File): String {
}
}
val cmps = mutableListOf<String>()
val usedNames = mutableListOf<String>()
val maps = StringBuilder()
// Generate 2 non redirect-able classes
genClass("DelegateComponentFactory")
genClass("DelegateApplication")
for (gen in cmpList) {
val name = genCmpName()
usedNames.add(name)
maps.append("|map.put(\"$name\", \"${gen.real}\");".ind(2))
maps.append('\n')
if (gen.stub.isNotEmpty()) {
if (gen.stub != "DelegateComponentFactory") {
maps.append("|internalMap.put(\"$name\", com.topjohnwu.magisk.${gen.stub}.class);".ind(2))
maps.append('\n')
}
if (gen.genClass) {
genClass(name, gen.stub)
}
}
if (gen.xml.isNotEmpty()) {
cmps.add(gen.xml.format(name))
}
cmps.add(gen.format(name))
}
// Shuffle the order of the components
cmps.shuffle(RANDOM)
val xml = File(srcDir, "AndroidManifest.xml").readText()
val genXml = xml.format(usedNames[0], usedNames[1], cmps.joinToString("\n\n"))
// Write mapping information to code
val mapping = File(srcDir, "Mapping.java").readText().format(maps)
PrintStream(File(mainPkgDir, "Mapping.java")).use {
it.print(mapping)
}
return genXml
return xml.format(usedNames[0], usedNames[1], cmps.joinToString("\n\n"))
}
fun genEncryptedResources(res: InputStream, outDir: File) {

View File

@ -3,6 +3,7 @@ package com.topjohnwu.magisk;
import com.topjohnwu.magisk.utils.DynamicClassLoader;
import java.io.File;
import java.util.Map;
// Wrap the actual classloader as we only want to resolve classname
// mapping when loading from platform (via LoadedApk.mClassLoader)
@ -14,19 +15,24 @@ class InjectedClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
return super.loadClass(Mapping.get(name), resolve);
String clz = DynLoad.componentMap.get(name);
name = clz != null ? clz : name;
return super.loadClass(name, resolve);
}
}
class RedirectClassLoader extends ClassLoader {
RedirectClassLoader() {
private final Map<String, Class<?>> mapping;
RedirectClassLoader(Map<String, Class<?>> m) {
super(RedirectClassLoader.class.getClassLoader());
mapping = m;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> clz = Mapping.internalMap.get(name);
Class<?> clz = mapping.get(name);
return clz == null ? super.loadClass(name, resolve) : clz;
}
}

View File

@ -11,6 +11,10 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.os.Process;
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 {
@ -40,7 +44,7 @@ public class DelegateComponentFactory extends AppComponentFactory {
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (receiver != null)
return receiver.instantiateActivity(DynLoad.loader, className, intent);
return create(className);
return create(className, DownloadActivity.class);
}
@Override
@ -48,7 +52,7 @@ public class DelegateComponentFactory extends AppComponentFactory {
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (receiver != null)
return receiver.instantiateReceiver(DynLoad.loader, className, intent);
return create(className);
return create(className, DummyReceiver.class);
}
@Override
@ -56,7 +60,7 @@ public class DelegateComponentFactory extends AppComponentFactory {
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (receiver != null)
return receiver.instantiateService(DynLoad.loader, className, intent);
return create(className);
return create(className, DummyService.class);
}
@Override
@ -64,12 +68,17 @@ public class DelegateComponentFactory extends AppComponentFactory {
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (receiver != null)
return receiver.instantiateProvider(DynLoad.loader, className);
return create(className);
return create(className, DummyProvider.class);
}
private <T> T create(String name)
throws ClassNotFoundException, IllegalAccessException, InstantiationException{
return (T) DynLoad.loader.loadClass(name).newInstance();
private <T> T create(String name, Class<T> fallback)
throws IllegalAccessException, InstantiationException {
try {
// noinspection unchecked
return (T) DynLoad.loader.loadClass(name).newInstance();
} catch (ClassNotFoundException e) {
return fallback.newInstance();
}
}
}

View File

@ -49,6 +49,14 @@ public class DownloadActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (DynLoad.isDynLoader()) {
// 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);
// Only download and dynamic load full APK if hidden

View File

@ -4,14 +4,21 @@ import static com.topjohnwu.magisk.BuildConfig.APPLICATION_ID;
import android.app.AppComponentFactory;
import android.app.Application;
import android.app.job.JobService;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import com.topjohnwu.magisk.dummy.DummyProvider;
import com.topjohnwu.magisk.dummy.DummyReceiver;
import com.topjohnwu.magisk.dummy.DummyService;
import com.topjohnwu.magisk.utils.APKInstall;
import java.io.File;
@ -20,6 +27,8 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import io.michaelrocks.paranoid.Obfuscate;
@ -28,15 +37,20 @@ import io.michaelrocks.paranoid.Obfuscate;
public class DynLoad {
// The current active classloader
static ClassLoader loader = new RedirectClassLoader();
static ClassLoader loader = DynLoad.class.getClassLoader();
static Object componentFactory;
static Map<String, String> componentMap = new HashMap<>();
private static boolean loadedApk = false;
static StubApk.Data createApkData() {
var data = new StubApk.Data();
data.setVersion(BuildConfig.STUB_VERSION);
data.setClassToComponent(Mapping.inverseMap);
Map<String, String> map = new HashMap<>();
for (var e : componentMap.entrySet()) {
map.put(e.getValue(), e.getKey());
}
data.setClassToComponent(map);
data.setRootService(DelegateRootService.class);
return data;
}
@ -135,23 +149,40 @@ public class DynLoad {
if (Build.VERSION.SDK_INT < 29)
replaceClassLoader(context);
if (!loadApk(context))
int flags = PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES
| PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS;
final PackageInfo info;
try {
info = context.getPackageManager()
.getPackageInfo(context.getPackageName(), flags);
} catch (PackageManager.NameNotFoundException e) {
// Impossible
throw new RuntimeException(e);
}
if (!loadApk(context)) {
loader = new RedirectClassLoader(createInternalMap(info));
return null;
}
File apk = StubApk.current(context);
PackageManager pm = context.getPackageManager();
try {
var info = pm.getPackageArchiveInfo(apk.getPath(), 0).applicationInfo;
var pkgInfo = pm.getPackageArchiveInfo(apk.getPath(), flags);
var appInfo = pkgInfo.applicationInfo;
updateComponentMap(info, pkgInfo);
// Create the receiver Application
var data = createApkData();
var app = (Application) loader.loadClass(info.className)
var app = (Application) loader.loadClass(appInfo.className)
.getConstructor(Object.class)
.newInstance(data.getObject());
// Create the receiver component factory
if (Build.VERSION.SDK_INT >= 28 && componentFactory != null) {
Object factory = loader.loadClass(info.appComponentFactory).newInstance();
Object factory = loader.loadClass(appInfo.appComponentFactory).newInstance();
var delegate = (DelegateComponentFactory) componentFactory;
delegate.receiver = (AppComponentFactory) factory;
}
@ -168,7 +199,90 @@ public class DynLoad {
return null;
}
private static boolean isDynLoader() {
private static Map<String, Class<?>> createInternalMap(PackageInfo info) {
Map<String, Class<?>> map = new HashMap<>();
for (var c : info.activities) {
map.put(c.name, DownloadActivity.class);
}
for (var c : info.services) {
map.put(c.name, DummyService.class);
}
for (var c : info.providers) {
map.put(c.name, DummyProvider.class);
}
for (var c : info.receivers) {
map.put(c.name, DummyReceiver.class);
}
return map;
}
private static void updateComponentMap(PackageInfo from, PackageInfo to) {
{
var src = from.activities;
var dest = to.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];
}
componentMap.put(sa.name, da.name);
componentMap.put(sb.name, db.name);
}
{
var src = from.services;
var dest = to.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];
}
componentMap.put(sa.name, da.name);
componentMap.put(sb.name, db.name);
}
{
var src = from.receivers;
var dest = to.receivers;
componentMap.put(src[0].name, dest[0].name);
}
{
var src = from.providers;
var dest = to.providers;
componentMap.put(src[0].name, dest[0].name);
}
}
static boolean isDynLoader() {
return loader instanceof InjectedClassLoader;
}

View File

@ -0,0 +1,12 @@
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) {
return null;
}
}

View File

@ -1,26 +0,0 @@
package com.topjohnwu.magisk;
import java.util.HashMap;
import java.util.Map;
import io.michaelrocks.paranoid.Obfuscate;
@Obfuscate
public class Mapping {
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 = new HashMap<>();
static {
%s
for (Map.Entry<String, String> e : map.entrySet()) {
inverseMap.put(e.getValue(), e.getKey());
}
}
public static String get(String name) {
String n = map.get(name);
return n != null ? n : name;
}
}