refactor: use PatchConfig and optimize module loading
[feat: Local mode runs without background manager](https://github.com/7723mod/NPatch/pulls/2)
This commit is contained in:
parent
5bbef84a43
commit
ed202bbb10
|
|
@ -6,40 +6,46 @@ import static org.lsposed.lspatch.share.Constants.ORIGINAL_APK_ASSET_PATH;
|
|||
import android.app.ActivityThread;
|
||||
import android.app.LoadedApk;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.res.CompatibilityInfo;
|
||||
import android.os.Build;
|
||||
import android.os.RemoteException;
|
||||
import android.system.Os;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.lsposed.lspatch.loader.util.FileUtils;
|
||||
import org.lsposed.lspatch.loader.util.XLog;
|
||||
import org.lsposed.lspatch.service.LocalApplicationService;
|
||||
import org.lsposed.lspatch.service.RemoteApplicationService;
|
||||
import org.lsposed.lspatch.share.PatchConfig;
|
||||
import org.lsposed.lspd.core.Startup;
|
||||
import org.lsposed.lspd.models.Module;
|
||||
import org.lsposed.lspd.service.ILSPApplicationService;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import dalvik.system.DexFile;
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.XposedHelpers;
|
||||
import hidden.HiddenApiBridge;
|
||||
|
||||
|
|
@ -57,7 +63,7 @@ public class LSPApplication {
|
|||
private static LoadedApk stubLoadedApk;
|
||||
private static LoadedApk appLoadedApk;
|
||||
|
||||
private static JSONObject config;
|
||||
private static PatchConfig config;
|
||||
|
||||
public static boolean isIsolated() {
|
||||
return (android.os.Process.myUid() % PER_USER_RANGE) >= FIRST_APP_ZYGOTE_ISOLATED_UID;
|
||||
|
|
@ -77,29 +83,47 @@ public class LSPApplication {
|
|||
|
||||
Log.d(TAG, "Initialize service client");
|
||||
ILSPApplicationService service;
|
||||
if (config.optBoolean("useManager")) {
|
||||
if (config.useManager) {
|
||||
try {
|
||||
service = new RemoteApplicationService(context);
|
||||
List<Module> m = service.getLegacyModulesList();
|
||||
JSONArray moduleArr = new JSONArray();
|
||||
for (Module module : m) {
|
||||
JSONObject moduleObj = new JSONObject();
|
||||
moduleObj.put("path",module.apkPath);
|
||||
moduleObj.put("packageName",module.packageName);
|
||||
moduleArr.put(moduleObj);
|
||||
}
|
||||
SharedPreferences shared = context.getSharedPreferences("npatch", Context.MODE_PRIVATE);
|
||||
shared.edit().putString("modules",moduleArr.toString()).commit();
|
||||
Log.e(TAG, "Success update module scope");
|
||||
}catch (Exception e){
|
||||
Log.e(TAG, "Failed to connect to manager, fallback to fixed local service");
|
||||
service = new LocalApplicationService(context);
|
||||
}
|
||||
|
||||
} else {
|
||||
service = new LocalApplicationService(context);
|
||||
}
|
||||
|
||||
disableProfile(context);
|
||||
Startup.initXposed(false, ActivityThread.currentProcessName(), context.getApplicationInfo().dataDir, service);
|
||||
Startup.bootstrapXposed();
|
||||
// WARN: Since it uses `XResource`, the following class should not be initialized
|
||||
// before forkPostCommon is invoke. Otherwise, you will get failure of XResources
|
||||
|
||||
Log.i(TAG, "Load modules");
|
||||
LSPLoader.initModules(appLoadedApk);
|
||||
Log.i(TAG, "Modules initialized");
|
||||
|
||||
switchAllClassLoader();
|
||||
SigBypass.doSigBypass(context, config.optInt("sigBypassLevel"));
|
||||
SigBypass.doSigBypass(context, config.sigBypassLevel);
|
||||
|
||||
Log.i(TAG, "LSPatch bootstrap completed");
|
||||
}
|
||||
|
||||
private static Context createLoadedApkWithContext() {
|
||||
try {
|
||||
var timeStart = System.currentTimeMillis();
|
||||
var mBoundApplication = XposedHelpers.getObjectField(activityThread, "mBoundApplication");
|
||||
|
||||
stubLoadedApk = (LoadedApk) XposedHelpers.getObjectField(mBoundApplication, "info");
|
||||
|
|
@ -109,13 +133,13 @@ public class LSPApplication {
|
|||
|
||||
try (var is = baseClassLoader.getResourceAsStream(CONFIG_ASSET_PATH)) {
|
||||
BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
|
||||
config = new JSONObject(streamReader.lines().collect(Collectors.joining()));
|
||||
} catch (Throwable e) {
|
||||
Log.e(TAG, "Failed to parse config file", e);
|
||||
config = new Gson().fromJson(streamReader, PatchConfig.class);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to load config file");
|
||||
return null;
|
||||
}
|
||||
Log.i(TAG, "Use manager: " + config.optBoolean("useManager"));
|
||||
Log.i(TAG, "Signature bypass level: " + config.optInt("sigBypassLevel"));
|
||||
Log.i(TAG, "Use manager: " + config.useManager);
|
||||
Log.i(TAG, "Signature bypass level: " + config.sigBypassLevel);
|
||||
|
||||
Path originPath = Paths.get(appInfo.dataDir, "cache/lspatch/origin/");
|
||||
Path cacheApkPath;
|
||||
|
|
@ -125,9 +149,7 @@ public class LSPApplication {
|
|||
|
||||
appInfo.sourceDir = cacheApkPath.toString();
|
||||
appInfo.publicSourceDir = cacheApkPath.toString();
|
||||
if (config.has("appComponentFactory")) {
|
||||
appInfo.appComponentFactory = config.optString("appComponentFactory");
|
||||
}
|
||||
appInfo.appComponentFactory = config.appComponentFactory;
|
||||
|
||||
if (!Files.exists(cacheApkPath)) {
|
||||
Log.i(TAG, "Extract original apk");
|
||||
|
|
@ -137,10 +159,13 @@ public class LSPApplication {
|
|||
Files.copy(is, cacheApkPath);
|
||||
}
|
||||
}
|
||||
|
||||
cacheApkPath.toFile().setWritable(false);
|
||||
|
||||
var mPackages = (Map<?, ?>) XposedHelpers.getObjectField(activityThread, "mPackages");
|
||||
mPackages.remove(appInfo.packageName);
|
||||
appLoadedApk = activityThread.getPackageInfoNoCheck(appInfo, compatInfo);
|
||||
|
||||
XposedHelpers.setObjectField(mBoundApplication, "info", appLoadedApk);
|
||||
|
||||
var activityClientRecordClass = XposedHelpers.findClass("android.app.ActivityThread$ActivityClientRecord", ActivityThread.class.getClassLoader());
|
||||
|
|
@ -165,14 +190,16 @@ public class LSPApplication {
|
|||
Log.i(TAG, "hooked app initialized: " + appLoadedApk);
|
||||
|
||||
var context = (Context) XposedHelpers.callStaticMethod(Class.forName("android.app.ContextImpl"), "createAppContext", activityThread, stubLoadedApk);
|
||||
if (config.has("appComponentFactory")) {
|
||||
if (config.appComponentFactory != null) {
|
||||
try {
|
||||
context.getClassLoader().loadClass(appInfo.appComponentFactory);
|
||||
} catch (Throwable e) { // 捕捉更廣泛的類載入錯誤, 可能可以兼容部分加固如 360
|
||||
Log.w(TAG, "Original AppComponentFactory not found: " + appInfo.appComponentFactory, e);
|
||||
context.getClassLoader().loadClass(config.appComponentFactory);
|
||||
} catch (Throwable e) {
|
||||
Log.w(TAG, "Original AppComponentFactory not found: " + config.appComponentFactory, e);
|
||||
appInfo.appComponentFactory = null;
|
||||
}
|
||||
}
|
||||
Log.i(TAG,"createLoadedApkWithContext cost: " + (System.currentTimeMillis() - timeStart) + "ms");
|
||||
|
||||
return context;
|
||||
} catch (Throwable e) {
|
||||
Log.e(TAG, "createLoadedApk", e);
|
||||
|
|
@ -180,55 +207,6 @@ public class LSPApplication {
|
|||
}
|
||||
}
|
||||
|
||||
public static void disableProfile(Context context) {
|
||||
final ArrayList<String> codePaths = new ArrayList<>();
|
||||
var appInfo = context.getApplicationInfo();
|
||||
var pkgName = context.getPackageName();
|
||||
if (appInfo == null) return;
|
||||
if ((appInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0) {
|
||||
codePaths.add(appInfo.sourceDir);
|
||||
}
|
||||
if (appInfo.splitSourceDirs != null) {
|
||||
Collections.addAll(codePaths, appInfo.splitSourceDirs);
|
||||
}
|
||||
|
||||
if (codePaths.isEmpty()) {
|
||||
// If there are no code paths there's no need to setup a profile file and register with
|
||||
// the runtime,
|
||||
return;
|
||||
}
|
||||
|
||||
var profileDir = HiddenApiBridge.Environment_getDataProfilesDePackageDirectory(appInfo.uid / PER_USER_RANGE, pkgName);
|
||||
|
||||
var attrs = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("r--------"));
|
||||
|
||||
for (int i = codePaths.size() - 1; i >= 0; i--) {
|
||||
String splitName = i == 0 ? null : appInfo.splitNames[i - 1];
|
||||
File curProfileFile = new File(profileDir, splitName == null ? "primary.prof" : splitName + ".split.prof").getAbsoluteFile();
|
||||
Log.d(TAG, "Processing " + curProfileFile.getAbsolutePath());
|
||||
try {
|
||||
if (!curProfileFile.exists()) {
|
||||
Files.createFile(curProfileFile.toPath(), attrs);
|
||||
continue;
|
||||
}
|
||||
if (!curProfileFile.canWrite() && Files.size(curProfileFile.toPath()) == 0) {
|
||||
Log.d(TAG, "Skip profile " + curProfileFile.getAbsolutePath());
|
||||
continue;
|
||||
}
|
||||
if (curProfileFile.exists() && !curProfileFile.delete()) {
|
||||
try (var writer = new FileOutputStream(curProfileFile)) {
|
||||
Log.d(TAG, "Failed to delete, try to clear content " + curProfileFile.getAbsolutePath());
|
||||
} catch (Throwable e) {
|
||||
Log.e(TAG, "Failed to delete and clear profile file " + curProfileFile.getAbsolutePath(), e);
|
||||
}
|
||||
Os.chmod(curProfileFile.getAbsolutePath(), 00400);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Log.e(TAG, "Failed to disable profile file " + curProfileFile.getAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void switchAllClassLoader() {
|
||||
var fields = LoadedApk.class.getDeclaredFields();
|
||||
for (Field field : fields) {
|
||||
|
|
|
|||
|
|
@ -1,84 +1,75 @@
|
|||
package org.lsposed.lspatch.service;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import org.lsposed.lspatch.loader.util.FileUtils;
|
||||
import org.lsposed.lspatch.share.Constants;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.lsposed.lspatch.util.ModuleLoader;
|
||||
import org.lsposed.lspd.models.Module;
|
||||
import org.lsposed.lspd.service.ILSPApplicationService;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public class LocalApplicationService extends ILSPApplicationService.Stub {
|
||||
|
||||
private static final String TAG = "NPatch";
|
||||
|
||||
private final List<Module> modules = new ArrayList<>();
|
||||
|
||||
private final List<Module> cachedModule;
|
||||
public LocalApplicationService(Context context){
|
||||
SharedPreferences shared = context.getSharedPreferences("npatch", Context.MODE_PRIVATE);
|
||||
cachedModule = new ArrayList<>();
|
||||
try {
|
||||
for (var name : context.getAssets().list("lspatch/modules")) {
|
||||
String packageName = name.substring(0, name.length() - 4);
|
||||
String modulePath = context.getCacheDir() + "/lspatch/" + packageName + "/";
|
||||
String cacheApkPath;
|
||||
try (ZipFile sourceFile = new ZipFile(context.getPackageResourcePath())) {
|
||||
cacheApkPath = modulePath + sourceFile.getEntry(Constants.EMBEDDED_MODULES_ASSET_PATH + name).getCrc() + ".apk";
|
||||
}
|
||||
|
||||
if (!Files.exists(Paths.get(cacheApkPath))) {
|
||||
Log.i(TAG, "Extract module apk: " + packageName);
|
||||
FileUtils.deleteFolderIfExists(Paths.get(modulePath));
|
||||
Files.createDirectories(Paths.get(modulePath));
|
||||
try (var is = context.getAssets().open("lspatch/modules/" + name)) {
|
||||
Files.copy(is, Paths.get(cacheApkPath));
|
||||
JSONArray mArr = new JSONArray(shared.getString("modules", "{}"));
|
||||
Log.i(TAG,"use fixed local application service:"+shared.getString("modules", "{}"));
|
||||
for (int i = 0; i < mArr.length(); i++) {
|
||||
JSONObject mObj = mArr.getJSONObject(i);
|
||||
Module m = new Module();
|
||||
String path = mObj.getString("path");
|
||||
String packageName = mObj.getString("packageName");
|
||||
m.apkPath = path;
|
||||
m.packageName = packageName;
|
||||
if (!new File(m.apkPath).exists()){
|
||||
Log.i("NPatch","Module:" + m.packageName + " path not available, reset.");
|
||||
try {
|
||||
ApplicationInfo info = context.getPackageManager().getApplicationInfo(m.packageName, 0);
|
||||
m.apkPath = info.sourceDir;
|
||||
Log.i("NPatch","Module:" + m.packageName + " path reset to " + m.apkPath);
|
||||
}catch (Exception e){
|
||||
Log.e("NPatch",Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
|
||||
var module = new Module();
|
||||
module.apkPath = cacheApkPath;
|
||||
module.packageName = packageName;
|
||||
module.file = ModuleLoader.loadModule(cacheApkPath);
|
||||
modules.add(module);
|
||||
m.file = ModuleLoader.loadModule(m.apkPath);
|
||||
cachedModule.add(m);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error when initializing LocalApplicationServiceClient", e);
|
||||
}catch (Exception e){
|
||||
Log.e(TAG,Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLogMuted() throws RemoteException {
|
||||
return false;
|
||||
public List<Module> getLegacyModulesList() throws RemoteException {
|
||||
return cachedModule;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Module> getLegacyModulesList() {
|
||||
return modules;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Module> getModulesList() {
|
||||
public List<Module> getModulesList() throws RemoteException {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPrefsPath(String packageName) {
|
||||
public String getPrefsPath(String packageName) throws RemoteException {
|
||||
return new File(Environment.getDataDirectory(), "data/" + packageName + "/shared_prefs/").getAbsolutePath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor requestInjectedManagerBinder(List<IBinder> binder) {
|
||||
public ParcelFileDescriptor requestInjectedManagerBinder(List<IBinder> binder) throws RemoteException {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -86,4 +77,9 @@ public class LocalApplicationService extends ILSPApplicationService.Stub {
|
|||
public IBinder asBinder() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLogMuted() throws RemoteException {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import android.os.ParcelFileDescriptor;
|
|||
import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.lsposed.lspatch.share.Constants;
|
||||
import org.lsposed.lspd.models.Module;
|
||||
|
|
@ -48,7 +47,7 @@ public class RemoteApplicationService implements ILSPApplicationService {
|
|||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder binder) {
|
||||
Log.i(TAG, "Manager binder received");
|
||||
service = Stub.asInterface(binder);
|
||||
service = ILSPApplicationService.Stub.asInterface(binder);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +75,6 @@ public class RemoteApplicationService implements ILSPApplicationService {
|
|||
if (!success) throw new TimeoutException("Bind service timeout");
|
||||
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException |
|
||||
InterruptedException | TimeoutException e) {
|
||||
Toast.makeText(context, "Unable to connect to Manager", Toast.LENGTH_SHORT).show();
|
||||
var r = new RemoteException("Failed to get manager binder");
|
||||
r.initCause(e);
|
||||
throw r;
|
||||
|
|
|
|||
Loading…
Reference in New Issue