Support configuration
This commit is contained in:
parent
5cd3f73ed1
commit
ce39256f3c
|
|
@ -1,44 +1,58 @@
|
||||||
package org.lsposed.lspatch.loader;
|
package org.lsposed.lspatch.loader;
|
||||||
|
|
||||||
import static android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE;
|
import static android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE;
|
||||||
import static org.lsposed.lspatch.loader.LSPLoader.initAndLoadModules;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.app.ActivityThread;
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import android.app.LoadedApk;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.pm.PackageInfo;
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.content.pm.Signature;
|
import android.content.pm.Signature;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.Environment;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
import org.lsposed.lspatch.loader.util.FileUtils;
|
import org.lsposed.lspatch.loader.util.FileUtils;
|
||||||
import org.lsposed.lspatch.loader.util.XLog;
|
import org.lsposed.lspatch.loader.util.XLog;
|
||||||
import org.lsposed.lspatch.loader.util.XpatchUtils;
|
|
||||||
import org.lsposed.lspatch.share.Constants;
|
import org.lsposed.lspatch.share.Constants;
|
||||||
import org.lsposed.lspd.deopt.PrebuiltMethodsDeopter;
|
import org.lsposed.lspd.config.ApplicationServiceClient;
|
||||||
|
import org.lsposed.lspd.core.Main;
|
||||||
import org.lsposed.lspd.nativebridge.SigBypass;
|
import org.lsposed.lspd.nativebridge.SigBypass;
|
||||||
import org.lsposed.lspd.yahfa.hooker.YahfaHooker;
|
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
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.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Modifier;
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import de.robv.android.xposed.XC_MethodHook;
|
import de.robv.android.xposed.XC_MethodHook;
|
||||||
import de.robv.android.xposed.XposedBridge;
|
import de.robv.android.xposed.XposedBridge;
|
||||||
import de.robv.android.xposed.XposedHelpers;
|
import de.robv.android.xposed.XposedHelpers;
|
||||||
import de.robv.android.xposed.XposedInit;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Windysha
|
* Created by Windysha
|
||||||
*/
|
*/
|
||||||
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
@SuppressWarnings("unused")
|
||||||
public class LSPApplication {
|
public class LSPApplication extends ApplicationServiceClient {
|
||||||
private static final String ORIGINAL_APPLICATION_NAME_ASSET_PATH = "original_application_name.ini";
|
private static final String ORIGINAL_APPLICATION_NAME_ASSET_PATH = "original_application_name.ini";
|
||||||
private static final String ORIGINAL_SIGNATURE_ASSET_PATH = "original_signature_info.ini";
|
private static final String ORIGINAL_SIGNATURE_ASSET_PATH = "original_signature_info.ini";
|
||||||
private static final String TAG = "LSPatch";
|
private static final String TAG = "LSPatch";
|
||||||
|
|
@ -56,26 +70,24 @@ public class LSPApplication {
|
||||||
|
|
||||||
static private LSPApplication instance = null;
|
static private LSPApplication instance = null;
|
||||||
|
|
||||||
|
static private final Map<String, String> modules = new HashMap<>();
|
||||||
|
|
||||||
static public boolean isIsolated() {
|
static public boolean isIsolated() {
|
||||||
return (android.os.Process.myUid() % PER_USER_RANGE) >= FIRST_APP_ZYGOTE_ISOLATED_UID;
|
return (android.os.Process.myUid() % PER_USER_RANGE) >= FIRST_APP_ZYGOTE_ISOLATED_UID;
|
||||||
}
|
}
|
||||||
|
|
||||||
static public void onLoad() {
|
public static void onLoad() {
|
||||||
cacheSigbypassLv = -1;
|
cacheSigbypassLv = -1;
|
||||||
|
|
||||||
if (isIsolated()) {
|
if (isIsolated()) {
|
||||||
XLog.d(TAG, "skip isolated process");
|
XLog.d(TAG, "skip isolated process");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Context context = XpatchUtils.createAppContext();
|
Context context = createAppContext();
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
XLog.e(TAG, "create context err");
|
XLog.e(TAG, "create context err");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
YahfaHooker.init();
|
|
||||||
// XposedBridge.initXResources();
|
|
||||||
// PrebuiltMethodsDeopter.deoptBootMethods(); // do it once for secondary zygote
|
|
||||||
XposedInit.startsSystemServer = false;
|
|
||||||
|
|
||||||
originalApplicationName = FileUtils.readTextFromAssets(context, ORIGINAL_APPLICATION_NAME_ASSET_PATH);
|
originalApplicationName = FileUtils.readTextFromAssets(context, ORIGINAL_APPLICATION_NAME_ASSET_PATH);
|
||||||
originalSignature = FileUtils.readTextFromAssets(context, ORIGINAL_SIGNATURE_ASSET_PATH);
|
originalSignature = FileUtils.readTextFromAssets(context, ORIGINAL_SIGNATURE_ASSET_PATH);
|
||||||
|
|
@ -84,14 +96,107 @@ public class LSPApplication {
|
||||||
XLog.d(TAG, "original signature info " + originalSignature);
|
XLog.d(TAG, "original signature info " + originalSignature);
|
||||||
|
|
||||||
instance = new LSPApplication();
|
instance = new LSPApplication();
|
||||||
|
serviceClient = instance;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
loadModules(context);
|
||||||
|
Main.forkPostCommon(false, context.getDataDir().toString(), ActivityThread.currentProcessName());
|
||||||
doHook(context);
|
doHook(context);
|
||||||
initAndLoadModules(context);
|
// WARN: Since it uses `XResource`, the following class should not be initialized
|
||||||
|
// before forkPostCommon is invoke. Otherwise, you will get failure of XResources
|
||||||
|
LSPLoader.initModules(context);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Log.e(TAG, "Do hook", e);
|
Log.e(TAG, "Do hook", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void loadModules(Context context) {
|
||||||
|
var configFile = new File(context.getExternalFilesDir(null), "lspatch.json");
|
||||||
|
var cacheDir = new File(context.getExternalCacheDir(), "modules");
|
||||||
|
cacheDir.mkdirs();
|
||||||
|
JSONObject moduleConfigs = new JSONObject();
|
||||||
|
try (var is = new FileInputStream(configFile)) {
|
||||||
|
moduleConfigs = new JSONObject(FileUtils.readTextFromInputStream(is));
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
var modules = moduleConfigs.optJSONArray("modules");
|
||||||
|
if (modules == null) {
|
||||||
|
modules = new JSONArray();
|
||||||
|
try {
|
||||||
|
moduleConfigs.put("modules", modules);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HashSet<String> embedded_modules = new HashSet<>();
|
||||||
|
HashSet<String> disabled_modules = new HashSet<>();
|
||||||
|
try {
|
||||||
|
for (var name : context.getAssets().list("modules")) {
|
||||||
|
var target = new File(cacheDir, name + ".apk");
|
||||||
|
if (target.exists()) {
|
||||||
|
embedded_modules.add(name);
|
||||||
|
LSPApplication.modules.put(name, target.getAbsolutePath());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try (var is = context.getAssets().open("modules/" + name)) {
|
||||||
|
Files.copy(is, target.toPath());
|
||||||
|
embedded_modules.add(name);
|
||||||
|
LSPApplication.modules.put(name, target.getAbsolutePath());
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
for (int i = 0; i < modules.length(); ++i) {
|
||||||
|
var module = modules.optJSONObject(i);
|
||||||
|
var name = module.optString("name");
|
||||||
|
var enabled = module.optBoolean("enabled", true);
|
||||||
|
var useEmbed = module.optBoolean("use_embed", false);
|
||||||
|
var apk = module.optString("path");
|
||||||
|
if (!enabled) disabled_modules.add(name);
|
||||||
|
if (embedded_modules.contains(name) && useEmbed) continue;
|
||||||
|
if (apk.isEmpty() || name.isEmpty()) continue;
|
||||||
|
if (!new File(apk).exists()) continue;
|
||||||
|
LSPApplication.modules.put(name, apk);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (PackageInfo pkg : context.getPackageManager().getInstalledPackages(PackageManager.GET_META_DATA)) {
|
||||||
|
ApplicationInfo app = pkg.applicationInfo;
|
||||||
|
if (!app.enabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (app.metaData != null && app.metaData.containsKey("xposedminversion")) {
|
||||||
|
LSPApplication.modules.computeIfAbsent(app.packageName, k -> app.publicSourceDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final var new_modules = new JSONArray();
|
||||||
|
LSPApplication.modules.forEach((k, v) -> {
|
||||||
|
try {
|
||||||
|
var module = new JSONObject();
|
||||||
|
module.put("name", k);
|
||||||
|
module.put("enabled", !disabled_modules.contains(k));
|
||||||
|
module.put("use_embed", embedded_modules.contains(k));
|
||||||
|
module.put("path", v);
|
||||||
|
new_modules.put(module);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
moduleConfigs.put("modules", new_modules);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
try (var is = new ByteArrayInputStream(moduleConfigs.toString(4).getBytes(StandardCharsets.UTF_8))) {
|
||||||
|
Files.copy(is, configFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
for (var module : disabled_modules) {
|
||||||
|
LSPApplication.modules.remove(module);
|
||||||
|
}
|
||||||
|
Log.e(TAG, "DONE LOAD");
|
||||||
|
}
|
||||||
|
|
||||||
public LSPApplication() {
|
public LSPApplication() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
|
@ -331,8 +436,7 @@ public class LSPApplication {
|
||||||
private static Object getActivityThread() {
|
private static Object getActivityThread() {
|
||||||
if (activityThread == null) {
|
if (activityThread == null) {
|
||||||
try {
|
try {
|
||||||
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
|
activityThread = ActivityThread.currentActivityThread();
|
||||||
activityThread = XposedHelpers.callStaticMethod(activityThreadClass, "currentActivityThread");
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Log.e(TAG, "getActivityThread", e);
|
Log.e(TAG, "getActivityThread", e);
|
||||||
}
|
}
|
||||||
|
|
@ -422,4 +526,78 @@ public class LSPApplication {
|
||||||
Log.e(TAG, "modifyApplicationInfoClassName", e);
|
Log.e(TAG, "modifyApplicationInfoClassName", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Context createAppContext() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
ActivityThread activityThreadObj = ActivityThread.currentActivityThread();
|
||||||
|
|
||||||
|
Field boundApplicationField = ActivityThread.class.getDeclaredField("mBoundApplication");
|
||||||
|
boundApplicationField.setAccessible(true);
|
||||||
|
Object mBoundApplication = boundApplicationField.get(activityThreadObj); // AppBindData
|
||||||
|
if (mBoundApplication == null) {
|
||||||
|
Log.e(TAG, "mBoundApplication null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Field infoField = mBoundApplication.getClass().getDeclaredField("info"); // info
|
||||||
|
infoField.setAccessible(true);
|
||||||
|
LoadedApk loadedApkObj = (LoadedApk) infoField.get(mBoundApplication); // LoadedApk
|
||||||
|
if (loadedApkObj == null) {
|
||||||
|
Log.e(TAG, "loadedApkObj null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
|
||||||
|
Method createAppContextMethod = contextImplClass.getDeclaredMethod("createAppContext", ActivityThread.class, LoadedApk.class);
|
||||||
|
createAppContextMethod.setAccessible(true);
|
||||||
|
|
||||||
|
Object context = createAppContextMethod.invoke(null, activityThreadObj, loadedApkObj);
|
||||||
|
|
||||||
|
if (context instanceof Context) {
|
||||||
|
return (Context) context;
|
||||||
|
}
|
||||||
|
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException | NoSuchFieldException e) {
|
||||||
|
Log.e(TAG, "Fail to create app context", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder requestModuleBinder() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder requestManagerBinder(String packageName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isResourcesHookEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map getModulesList(String processName) {
|
||||||
|
return getModulesList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getModulesList() {
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPrefsPath(String packageName) {
|
||||||
|
return new File(Environment.getDataDirectory(), "data/" + packageName + "/shared_prefs/").getAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ParcelFileDescriptor getModuleLogger() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder asBinder() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,324 +1,24 @@
|
||||||
package org.lsposed.lspatch.loader;
|
package org.lsposed.lspatch.loader;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.app.ActivityThread;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.res.XResources;
|
||||||
import android.content.pm.PackageInfo;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.util.Pair;
|
|
||||||
|
|
||||||
import org.lsposed.lspatch.loader.util.FileUtils;
|
|
||||||
import org.lsposed.lspatch.loader.util.XLog;
|
|
||||||
import org.lsposed.lspatch.loader.util.XpatchUtils;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.BufferedWriter;
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
import dalvik.system.DelegateLastClassLoader;
|
|
||||||
import de.robv.android.xposed.IXposedHookInitPackageResources;
|
|
||||||
import de.robv.android.xposed.IXposedHookLoadPackage;
|
|
||||||
import de.robv.android.xposed.IXposedHookZygoteInit;
|
|
||||||
import de.robv.android.xposed.XposedBridge;
|
import de.robv.android.xposed.XposedBridge;
|
||||||
import de.robv.android.xposed.XposedHelper;
|
|
||||||
import de.robv.android.xposed.XposedInit;
|
import de.robv.android.xposed.XposedInit;
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||||
|
|
||||||
public class LSPLoader {
|
public class LSPLoader {
|
||||||
|
public static void initModules(Context context) {
|
||||||
private static final String TAG = LSPLoader.class.getSimpleName();
|
XposedInit.loadedPackagesInProcess.add(context.getPackageName());
|
||||||
private static final String DIR_BASE = Environment.getExternalStorageDirectory().getAbsolutePath();
|
XResources.setPackageNameForResDir(context.getPackageName(), context.getPackageResourcePath());
|
||||||
private static final String XPOSED_MODULE_FILE_PATH = "xpmodules.list";
|
XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(
|
||||||
private static AtomicBoolean hasInited = new AtomicBoolean(false);
|
XposedBridge.sLoadedPackageCallbacks);
|
||||||
private static Context appContext;
|
lpparam.packageName = context.getPackageName();
|
||||||
|
lpparam.processName = ActivityThread.currentProcessName();
|
||||||
|
lpparam.classLoader = context.getClassLoader();
|
||||||
@SuppressLint("DiscouragedPrivateApi")
|
lpparam.appInfo = context.getApplicationInfo();
|
||||||
public static boolean loadModule(final String moduleApkPath, String moduleLibPath, final ApplicationInfo currentApplicationInfo, ClassLoader appClassLoader) {
|
lpparam.isFirstApplication = true;
|
||||||
|
XC_LoadPackage.callAll(lpparam);
|
||||||
XLog.i(TAG, "Loading modules from " + moduleApkPath);
|
|
||||||
|
|
||||||
if (!new File(moduleApkPath).exists()) {
|
|
||||||
XLog.e(TAG, moduleApkPath + " does not exist");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// module can load it's own so
|
|
||||||
StringBuilder nativePath = new StringBuilder();
|
|
||||||
for (String i : Build.SUPPORTED_ABIS) {
|
|
||||||
nativePath.append(moduleApkPath).append("!/lib/").append(i).append(File.pathSeparator);
|
|
||||||
}
|
|
||||||
ClassLoader initLoader = XposedInit.class.getClassLoader();
|
|
||||||
ClassLoader mcl = new DelegateLastClassLoader(moduleApkPath, nativePath.toString(), initLoader);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (mcl.loadClass(XposedBridge.class.getName()).getClassLoader() != initLoader) {
|
|
||||||
Log.e(TAG, "Cannot load module:");
|
|
||||||
Log.e(TAG, "The Xposed API classes are compiled into the module's APK.");
|
|
||||||
Log.e(TAG, "This may cause strange issues and must be fixed by the module developer.");
|
|
||||||
Log.e(TAG, "For details, see: http://api.xposed.info/using.html");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (ClassNotFoundException ignored) {
|
|
||||||
}
|
|
||||||
|
|
||||||
try (InputStream is = mcl.getResourceAsStream("assets/xposed_init")) {
|
|
||||||
if (is == null) {
|
|
||||||
XLog.e(TAG, "assets/xposed_init not found in the APK");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
BufferedReader moduleClassesReader = new BufferedReader(new InputStreamReader(is));
|
|
||||||
String moduleClassName;
|
|
||||||
while ((moduleClassName = moduleClassesReader.readLine()) != null) {
|
|
||||||
moduleClassName = moduleClassName.trim();
|
|
||||||
if (moduleClassName.isEmpty() || moduleClassName.startsWith("#")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
XLog.i(TAG, "Loading class " + moduleClassName);
|
|
||||||
Class<?> moduleClass = mcl.loadClass(moduleClassName);
|
|
||||||
|
|
||||||
if (!XposedHelper.isIXposedMod(moduleClass)) {
|
|
||||||
Log.w(TAG, "This class doesn't implement any sub-interface of IXposedMod, skipping it");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else if (IXposedHookInitPackageResources.class.isAssignableFrom(moduleClass)) {
|
|
||||||
Log.w(TAG, "This class requires resource-related hooks (which are disabled), skipping it.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Object moduleInstance = moduleClass.newInstance();
|
|
||||||
if (moduleInstance instanceof IXposedHookZygoteInit) {
|
|
||||||
XposedHelper.callInitZygote(moduleApkPath, moduleInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moduleInstance instanceof IXposedHookLoadPackage) {
|
|
||||||
// hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));
|
|
||||||
IXposedHookLoadPackage.Wrapper wrapper = new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance, moduleApkPath);
|
|
||||||
XposedBridge.CopyOnWriteSortedSet<XC_LoadPackage> xc_loadPackageCopyOnWriteSortedSet = new XposedBridge.CopyOnWriteSortedSet<>();
|
|
||||||
xc_loadPackageCopyOnWriteSortedSet.add(wrapper);
|
|
||||||
XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(xc_loadPackageCopyOnWriteSortedSet);
|
|
||||||
lpparam.packageName = currentApplicationInfo.packageName;
|
|
||||||
lpparam.processName = (String) Class.forName("android.app.ActivityThread").getDeclaredMethod("currentProcessName").invoke(null);
|
|
||||||
lpparam.classLoader = appClassLoader;
|
|
||||||
lpparam.appInfo = currentApplicationInfo;
|
|
||||||
lpparam.isFirstApplication = true;
|
|
||||||
XC_LoadPackage.callAll(lpparam);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moduleInstance instanceof IXposedHookInitPackageResources) {
|
|
||||||
XLog.e(TAG, "Unsupport resource hook");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Throwable t) {
|
|
||||||
XLog.e(TAG, "", t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
XLog.e(TAG, "", e);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void initAndLoadModules() {
|
|
||||||
Context context = XpatchUtils.createAppContext();
|
|
||||||
initAndLoadModules(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void initAndLoadModules(Context context) {
|
|
||||||
if (!hasInited.compareAndSet(false, true)) {
|
|
||||||
XLog.w(TAG, "Has been init");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context == null) {
|
|
||||||
XLog.e(TAG, "Try to init with context null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
appContext = context;
|
|
||||||
|
|
||||||
initSELinux(context);
|
|
||||||
|
|
||||||
ClassLoader originClassLoader = context.getClassLoader();
|
|
||||||
List<String> modulePathList = loadAllInstalledModule(context);
|
|
||||||
|
|
||||||
for (String modulePath : modulePathList) {
|
|
||||||
if (!TextUtils.isEmpty(modulePath)) {
|
|
||||||
LSPLoader.loadModule(modulePath, null, context.getApplicationInfo(), originClassLoader);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void initSELinux(Context context) {
|
|
||||||
XposedHelper.initSeLinux(context.getApplicationInfo().processName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> loadAllInstalledModule(Context context) {
|
|
||||||
PackageManager pm = context.getPackageManager();
|
|
||||||
List<String> modulePathList = new ArrayList<>();
|
|
||||||
|
|
||||||
List<String> packageNameList = loadPackageNameListFromFile(true);
|
|
||||||
List<Pair<String, String>> installedModuleList = new ArrayList<>();
|
|
||||||
|
|
||||||
boolean configFileExist = configFileExist();
|
|
||||||
|
|
||||||
// todo: Android 11
|
|
||||||
for (PackageInfo pkg : pm.getInstalledPackages(PackageManager.GET_META_DATA)) {
|
|
||||||
ApplicationInfo app = pkg.applicationInfo;
|
|
||||||
if (!app.enabled) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (app.metaData != null && app.metaData.containsKey("xposedminversion")) {
|
|
||||||
String apkPath = pkg.applicationInfo.publicSourceDir;
|
|
||||||
String apkName = context.getPackageManager().getApplicationLabel(pkg.applicationInfo).toString();
|
|
||||||
if (TextUtils.isEmpty(apkPath)) {
|
|
||||||
apkPath = pkg.applicationInfo.sourceDir;
|
|
||||||
}
|
|
||||||
if (!TextUtils.isEmpty(apkPath) && (!configFileExist || packageNameList == null || packageNameList.contains(app.packageName))) {
|
|
||||||
XLog.d(TAG, "query installed module path " + apkPath);
|
|
||||||
modulePathList.add(apkPath);
|
|
||||||
}
|
|
||||||
installedModuleList.add(Pair.create(pkg.applicationInfo.packageName, apkName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<Pair<String, String>> installedModuleListFinal = installedModuleList;
|
|
||||||
|
|
||||||
new Thread(() -> {
|
|
||||||
List<String> savedPackageNameList = loadPackageNameListFromFile(false);
|
|
||||||
if (savedPackageNameList == null) {
|
|
||||||
savedPackageNameList = new ArrayList<>();
|
|
||||||
}
|
|
||||||
List<Pair<String, String>> addPackageList = new ArrayList<>();
|
|
||||||
for (Pair<String, String> packgagePair : installedModuleListFinal) {
|
|
||||||
if (!savedPackageNameList.contains(packgagePair.first)) {
|
|
||||||
XLog.d(TAG, "append " + packgagePair + " to " + XPOSED_MODULE_FILE_PATH);
|
|
||||||
addPackageList.add(packgagePair);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
appendPackageNameToFile(addPackageList);
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
return modulePathList;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从 sd 卡中加载指定文件,以加载指定的 xposed module
|
|
||||||
private static List<String> loadPackageNameListFromFile(boolean loadActivedPackages) {
|
|
||||||
File moduleFile = new File(DIR_BASE, XPOSED_MODULE_FILE_PATH);
|
|
||||||
if (!moduleFile.exists()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
List<String> modulePackageList = new ArrayList<>();
|
|
||||||
|
|
||||||
try (FileInputStream fileInputStream = new FileInputStream(moduleFile);
|
|
||||||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream))) {
|
|
||||||
String modulePackageName;
|
|
||||||
while ((modulePackageName = bufferedReader.readLine()) != null) {
|
|
||||||
modulePackageName = modulePackageName.trim();
|
|
||||||
if (modulePackageName.isEmpty() || (modulePackageName.startsWith("#") && loadActivedPackages)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modulePackageName.startsWith("#")) {
|
|
||||||
modulePackageName = modulePackageName.substring(1);
|
|
||||||
}
|
|
||||||
int index = modulePackageName.indexOf("#");
|
|
||||||
if (index > 0) {
|
|
||||||
modulePackageName = modulePackageName.substring(0, index);
|
|
||||||
}
|
|
||||||
XLog.d(TAG, "load " + XPOSED_MODULE_FILE_PATH + " file result, modulePackageName " + modulePackageName);
|
|
||||||
modulePackageList.add(modulePackageName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException ignore) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return modulePackageList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void appendPackageNameToFile(List<Pair<String, String>> packageNameList) throws IOException {
|
|
||||||
|
|
||||||
if (isEmpty(packageNameList)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
File moduleFile = new File(DIR_BASE, XPOSED_MODULE_FILE_PATH);
|
|
||||||
if (!moduleFile.exists()) {
|
|
||||||
if (!moduleFile.createNewFile()) {
|
|
||||||
throw new IllegalStateException("create " + XPOSED_MODULE_FILE_PATH + " err");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try (FileOutputStream outputStream = new FileOutputStream(moduleFile, true);
|
|
||||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream))) {
|
|
||||||
for (Pair<String, String> packageInfo : packageNameList) {
|
|
||||||
String packageName = packageInfo.first;
|
|
||||||
String appName = packageInfo.second;
|
|
||||||
writer.write(packageName + "#" + appName);
|
|
||||||
writer.write("\n");
|
|
||||||
XLog.d(TAG, "append new pkg to " + XPOSED_MODULE_FILE_PATH);
|
|
||||||
}
|
|
||||||
writer.flush();
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException ignore) {
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean configFileExist() {
|
|
||||||
File moduleConfigFile = new File(DIR_BASE, XPOSED_MODULE_FILE_PATH);
|
|
||||||
return moduleConfigFile.exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void closeStream(Closeable closeable) {
|
|
||||||
if (closeable != null) {
|
|
||||||
try {
|
|
||||||
closeable.close();
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isEmpty(Collection<?> collection) {
|
|
||||||
if (collection == null || collection.size() == 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Context getAppContext() {
|
|
||||||
return appContext;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
package org.lsposed.lspatch.loader.util;
|
package org.lsposed.lspatch.loader.util;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.Process;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
@ -12,19 +9,6 @@ import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
public class FileUtils {
|
public class FileUtils {
|
||||||
|
|
||||||
//读写权限
|
|
||||||
private static String[] PERMISSIONS_STORAGE = {
|
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
||||||
};
|
|
||||||
|
|
||||||
public static boolean isSdcardPermissionGranted(Context context) {
|
|
||||||
int pid = android.os.Process.myPid();
|
|
||||||
int uid = Process.myUid();
|
|
||||||
return context.checkPermission(PERMISSIONS_STORAGE[0], pid, uid) == PackageManager.PERMISSION_GRANTED && context.checkPermission(PERMISSIONS_STORAGE[1], pid,
|
|
||||||
uid) == PackageManager.PERMISSION_GRANTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String readTextFromAssets(Context context, String assetsFileName) {
|
public static String readTextFromAssets(Context context, String assetsFileName) {
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
throw new IllegalStateException("context null");
|
throw new IllegalStateException("context null");
|
||||||
|
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
package org.lsposed.lspatch.loader.util;
|
|
||||||
|
|
||||||
import android.app.ActivityThread;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
public class XpatchUtils {
|
|
||||||
final static String TAG = "LSP" + XpatchUtils.class.getSimpleName();
|
|
||||||
|
|
||||||
public static Context createAppContext() {
|
|
||||||
try {
|
|
||||||
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
|
|
||||||
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
|
|
||||||
currentActivityThreadMethod.setAccessible(true);
|
|
||||||
|
|
||||||
Object activityThreadObj = currentActivityThreadMethod.invoke(null);
|
|
||||||
|
|
||||||
Field boundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
|
|
||||||
boundApplicationField.setAccessible(true);
|
|
||||||
Object mBoundApplication = boundApplicationField.get(activityThreadObj); // AppBindData
|
|
||||||
if (mBoundApplication == null) {
|
|
||||||
Log.e(TAG, "mBoundApplication null");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Field infoField = mBoundApplication.getClass().getDeclaredField("info"); // info
|
|
||||||
infoField.setAccessible(true);
|
|
||||||
Object loadedApkObj = infoField.get(mBoundApplication); // LoadedApk
|
|
||||||
if (loadedApkObj == null) {
|
|
||||||
Log.e(TAG, "loadedApkObj null");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
|
|
||||||
Method createAppContextMethod = contextImplClass.getDeclaredMethod("createAppContext", activityThreadClass, loadedApkObj.getClass());
|
|
||||||
createAppContextMethod.setAccessible(true);
|
|
||||||
|
|
||||||
Object context = createAppContextMethod.invoke(null, (ActivityThread) activityThreadObj, loadedApkObj);
|
|
||||||
|
|
||||||
if (context instanceof Context) {
|
|
||||||
return (Context) context;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException | NoSuchFieldException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isApkDebugable(Context context) {
|
|
||||||
try {
|
|
||||||
ApplicationInfo info = context.getApplicationInfo();
|
|
||||||
return (info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
|
|
||||||
}
|
|
||||||
catch (Exception ignore) {
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2
core
2
core
|
|
@ -1 +1 @@
|
||||||
Subproject commit 9e47e9027c4244317085cce2a95ddcbb7b579ee0
|
Subproject commit 9891fc4760e3ef806e909ca7aed83c7248991bf7
|
||||||
|
|
@ -26,11 +26,12 @@ import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
|
||||||
public class LSPatch {
|
public class LSPatch {
|
||||||
|
|
||||||
|
|
@ -41,7 +42,7 @@ public class LSPatch {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parameter(description = "apks")
|
@Parameter(description = "apks")
|
||||||
private ArrayList<String> apkPaths = new ArrayList<>();
|
private List<String> apkPaths = new ArrayList<>();
|
||||||
|
|
||||||
@Parameter(names = {"-h", "--help"}, help = true, order = 0, description = "Print this message")
|
@Parameter(names = {"-h", "--help"}, help = true, order = 0, description = "Print this message")
|
||||||
private boolean help = false;
|
private boolean help = false;
|
||||||
|
|
@ -72,6 +73,9 @@ public class LSPatch {
|
||||||
@Parameter(names = {"-v", "--verbose"}, description = "Verbose output")
|
@Parameter(names = {"-v", "--verbose"}, description = "Verbose output")
|
||||||
private boolean verbose = false;
|
private boolean verbose = false;
|
||||||
|
|
||||||
|
@Parameter(names = {"-m", "--embed"}, description = "Embed provided modules to apk")
|
||||||
|
private List<String> modules = new ArrayList<>();
|
||||||
|
|
||||||
private int dexFileCount = 0;
|
private int dexFileCount = 0;
|
||||||
|
|
||||||
private static final String APPLICATION_NAME_ASSET_PATH = "assets/original_application_name.ini";
|
private static final String APPLICATION_NAME_ASSET_PATH = "assets/original_application_name.ini";
|
||||||
|
|
@ -195,7 +199,7 @@ public class LSPatch {
|
||||||
try (var is = new ByteArrayInputStream(applicationName.getBytes(StandardCharsets.UTF_8))) {
|
try (var is = new ByteArrayInputStream(applicationName.getBytes(StandardCharsets.UTF_8))) {
|
||||||
zFile.add(APPLICATION_NAME_ASSET_PATH, is);
|
zFile.add(APPLICATION_NAME_ASSET_PATH, is);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
throw new PatchError("Error when saving signature: " + e);
|
throw new PatchError("Error when saving application name: " + e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy so and dex files into the unzipped apk
|
// copy so and dex files into the unzipped apk
|
||||||
|
|
@ -264,9 +268,11 @@ public class LSPatch {
|
||||||
try (var is = new ByteArrayInputStream("42".getBytes(StandardCharsets.UTF_8))) {
|
try (var is = new ByteArrayInputStream("42".getBytes(StandardCharsets.UTF_8))) {
|
||||||
zFile.add("assets/" + Constants.CONFIG_NAME_SIGBYPASSLV + sigbypassLevel, is);
|
zFile.add("assets/" + Constants.CONFIG_NAME_SIGBYPASSLV + sigbypassLevel, is);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
throw new PatchError("Error when saving signature: " + e);
|
throw new PatchError("Error when saving signature bypass level: " + e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
embedModules(zFile);
|
||||||
|
|
||||||
System.out.println("Signing apk...");
|
System.out.println("Signing apk...");
|
||||||
var sign = zFile.get("META-INF/MANIFEST.MF");
|
var sign = zFile.get("META-INF/MANIFEST.MF");
|
||||||
if (sign != null)
|
if (sign != null)
|
||||||
|
|
@ -286,6 +292,46 @@ public class LSPatch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void embedModules(ZFile zFile) {
|
||||||
|
System.out.println("Embedding modules...");
|
||||||
|
for (var module : modules) {
|
||||||
|
var file = new File(module);
|
||||||
|
if (!file.exists()) {
|
||||||
|
System.err.println(file.getAbsolutePath() + " does not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.print("Embedding module ");
|
||||||
|
|
||||||
|
ManifestParser.Pair pair = null;
|
||||||
|
try (JarFile jar = new JarFile(file)) {
|
||||||
|
var manifest = jar.getEntry(ANDROID_MANIFEST_XML);
|
||||||
|
if (manifest == null) {
|
||||||
|
System.out.println();
|
||||||
|
System.err.println(file.getAbsolutePath() + " is not a valid apk file.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pair = ManifestParser.parseManifestFile(jar.getInputStream(manifest));
|
||||||
|
if (pair == null) {
|
||||||
|
System.out.println();
|
||||||
|
System.err.println(file.getAbsolutePath() + " is not a valid apk file.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
System.out.println(pair.packageName);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
System.out.println();
|
||||||
|
System.err.println(e.getMessage());
|
||||||
|
}
|
||||||
|
if (pair != null) {
|
||||||
|
try (var is = new FileInputStream(file)) {
|
||||||
|
zFile.add("assets/modules/" + pair.packageName, is);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
System.err.println("Embed " + pair.packageName + " with error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private byte[] modifyManifestFile(InputStream is) throws IOException {
|
private byte[] modifyManifestFile(InputStream is) throws IOException {
|
||||||
ModificationProperty property = new ModificationProperty();
|
ModificationProperty property = new ModificationProperty();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue