diff --git a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt index 15e6d3544..25e8a121a 100644 --- a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt +++ b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt @@ -34,6 +34,7 @@ object Patcher { add("-m"); add(it) } if(injectDex) add("--injectdex") + if (config.useMicroG) add("--useMicroG") if (!MyKeyStore.useDefault) { addAll(arrayOf("-k", MyKeyStore.file.path, Configs.keyStorePassword, Configs.keyStoreAlias, Configs.keyStoreAliasPassword)) } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt index d298fe16f..c3a5870b4 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt @@ -346,6 +346,14 @@ private fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) { desc = stringResource(R.string.patch_inject_dex_desc) ) + SettingsCheckBox( + modifier = Modifier.clickable { viewModel.useMicroG = !viewModel.useMicroG }, + checked = viewModel.useMicroG, + icon = Icons.Outlined.CloudSync, + title = stringResource(R.string.patch_use_microg), + desc = stringResource(R.string.patch_use_microg_desc) + ) + var bypassExpanded by remember { mutableStateOf(false) } AnywhereDropdown( expanded = bypassExpanded, diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt index be17f4b57..c8c381f8c 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt @@ -39,6 +39,7 @@ class NewPatchViewModel : ViewModel() { var overrideVersionCode by mutableStateOf(false) var sigBypassLevel by mutableStateOf(2) var injectDex by mutableStateOf(false) + var useMicroG by mutableStateOf(false) var embeddedModules = emptyList() lateinit var patchApp: AppInfo @@ -92,7 +93,7 @@ class NewPatchViewModel : ViewModel() { if (useManager) embeddedModules = emptyList() patchOptions = Patcher.Options( injectDex = injectDex, - config = PatchConfig(useManager, debuggable, overrideVersionCode, sigBypassLevel, null, null), + config = PatchConfig(useManager, debuggable, overrideVersionCode, sigBypassLevel, null, null, useMicroG), apkPaths = listOf(patchApp.app.sourceDir) + (patchApp.app.splitSourceDirs ?: emptyArray()), embeddedModules = embeddedModules.flatMap { listOf(it.app.sourceDir) + (it.app.splitSourceDirs ?: emptyArray()) } ) diff --git a/manager/src/main/res/values-zh-rCN/strings.xml b/manager/src/main/res/values-zh-rCN/strings.xml index fa115e031..f835f4f12 100644 --- a/manager/src/main/res/values-zh-rCN/strings.xml +++ b/manager/src/main/res/values-zh-rCN/strings.xml @@ -62,6 +62,10 @@ lv2: 绕过 PM + openat (libc) 覆写版本号 将修补的 App 版本号重写为 1\n这将允许后续降级安装,并且通常来说这不会影响应用实际感知到的版本号 + 注入加载器 Dex + 对那些需要孤立服务进程的应用程序,譬如说浏览器的渲染引擎,请勾选此选项以确保他们正常运行 + 强制启用 MicroG 支持 + 重新导向 GMS 请求至社区版 MicroG。适用于 YouTube 等 Google 应用程序,需自行安装对应的 MicroG 服务。 开始修补 返回 由于签名不同,安装修补的应用前需要先卸载原应用。\n确保您已备份好个人数据。 @@ -86,6 +90,4 @@ 别名错误 别名密码错误 详细修补日志 - 注入加载器 Dex - 对那些需要孤立服务进程的应用程序,譬如说浏览器的渲染引擎,请勾选此选项以确保他们正常运行 diff --git a/manager/src/main/res/values-zh-rTW/strings.xml b/manager/src/main/res/values-zh-rTW/strings.xml index 0ad886b8e..55b4efbcd 100644 --- a/manager/src/main/res/values-zh-rTW/strings.xml +++ b/manager/src/main/res/values-zh-rTW/strings.xml @@ -62,6 +62,10 @@ lv2: 繞過 PM + openat (libc) 覆蓋版本編號 將打包應用程式的版本編號改成 1\n允許以後降級安裝,一般來說,這不會影響應用程式實際感知的版本編號。 + 注入載入器 Dex + 對那些需要孤立服務程序的應用程式,譬如說瀏覽器的渲染引擎,請勾選此選項以確保他們正常執行 + 強制啟用 MicroG 支援 + 重新導向 GMS 請求至社群版 MicroG。適用於 YouTube 等 Google 應用程式,需自行安裝對應的 MicroG 服務。 開始打包 返回 由於簽名不同,安裝前需要先解除安裝原程式。\n確保您已備份好個人資料。 @@ -86,6 +90,4 @@ 別名錯誤 別名密碼錯誤 詳細打包日誌 - 注入加載器 Dex - 對那些需要孤立服務進程的應用程序,譬如說瀏覽器的渲染引擎,請勾選此選項以確保他們正常運行 diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index 27393557a..7d9628b1d 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -64,6 +64,10 @@ lv2: Bypass PM + openat (libc) Override version code Override the patched app\'s version code to 1\nThis allows downgrade installation in the future, and generally this will not affect the version code actually perceived by the application + Inject loader dex + For applications with isolated services, such as the render engines of browsers, please turn on this option to ensure that they work properly. + Force enable MicroG support + Redirect GMS requests to the community version of MicroG (such as ReVanced GmsCore). Applicable to Google apps like YouTube, requires manually installing the corresponding MicroG service. Start Patch Return Due to different signatures, you need to uninstall the original app before installing the patched one.\nMake sure you have backed up personal data. @@ -90,6 +94,4 @@ Wrong alias name Wrong alias password Detail patch logs - Inject loader dex - For applications with isolated services, such as the render engines of browsers, please turn on this option to ensure that they work properly. diff --git a/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java b/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java new file mode 100644 index 000000000..f99526e2e --- /dev/null +++ b/patch-loader/src/main/java/org/lsposed/lspatch/loader/GmsRedirector.java @@ -0,0 +1,256 @@ +package org.lsposed.lspatch.loader; + +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.net.Uri; +import android.util.Log; + +import org.lsposed.lspatch.share.Constants; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; + +public class GmsRedirector { + private static final String TAG = "LSPatch-GmsRedirect"; + private static final String REAL_GMS = Constants.REAL_GMS_PACKAGE_NAME; + + // 鎖定社群主流的 MicroG 套件名稱 + private static final String[] MICROG_PACKAGES = { + "app.revanced.android.gms", // ReVanced GmsCore (推薦) + "org.microg.gms", // Original MicroG + }; + + private static String targetGms = null; + private static String originalSignature; + + public static void activate(Context context, String origSig) { + originalSignature = origSig; + + targetGms = findInstalledMicroG(context); + if (targetGms == null) { + Log.w(TAG, "No MicroG/GmsCore found! GMS redirect disabled."); + return; + } + + Log.i(TAG, "Activating GMS redirect: " + REAL_GMS + " -> " + targetGms); + + hookIntentSetPackage(); + hookIntentSetComponent(); + hookIntentResolve(); + hookContentResolverAcquire(); + hookPackageManagerGetPackageInfo(context); + + Log.i(TAG, "GMS redirect hooks installed"); + } + + private static String findInstalledMicroG(Context context) { + PackageManager pm = context.getPackageManager(); + for (String pkg : MICROG_PACKAGES) { + try { + pm.getPackageInfo(pkg, 0); + return pkg; + } catch (PackageManager.NameNotFoundException ignored) {} + } + return null; + } + + private static String redirectPackage(String pkg) { + if (REAL_GMS.equals(pkg) || "com.google.android.gsf".equals(pkg)) { + return targetGms; + } + return null; + } + + private static String redirectAuthority(String authority) { + if (authority == null) return null; + if (authority.startsWith(REAL_GMS + ".")) { + return targetGms + authority.substring(REAL_GMS.length()); + } + if (authority.equals(REAL_GMS)) { + return targetGms; + } + if (authority.startsWith("com.google.android.gsf")) { + return authority.replace("com.google.android.gsf", targetGms); + } + return null; + } + + private static void hookIntentSetPackage() { + try { + XposedBridge.hookAllMethods(Intent.class, "setPackage", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + String pkg = (String) param.args[0]; + String redirected = redirectPackage(pkg); + if (redirected != null) param.args[0] = redirected; + } + }); + } catch (Throwable t) { + Log.e(TAG, "Failed to hook Intent.setPackage", t); + } + } + + private static void hookIntentSetComponent() { + try { + XposedBridge.hookAllMethods(Intent.class, "setComponent", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + ComponentName cn = (ComponentName) param.args[0]; + if (cn != null) { + String redirected = redirectPackage(cn.getPackageName()); + if (redirected != null) { + param.args[0] = new ComponentName(redirected, cn.getClassName()); + } + } + } + }); + } catch (Throwable t) { + Log.e(TAG, "Failed to hook Intent.setComponent", t); + } + } + + private static void hookIntentResolve() { + try { + XposedBridge.hookAllConstructors(Intent.class, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + Intent intent = (Intent) param.thisObject; + ComponentName cn = intent.getComponent(); + if (cn != null) { + String redirected = redirectPackage(cn.getPackageName()); + if (redirected != null) { + intent.setComponent(new ComponentName(redirected, cn.getClassName())); + } + } + String pkg = intent.getPackage(); + if (pkg != null) { + String redirected = redirectPackage(pkg); + if (redirected != null) { + intent.setPackage(redirected); + } + } + } + }); + } catch (Throwable t) { + Log.e(TAG, "Failed to hook Intent constructors", t); + } + } + + private static void hookContentResolverAcquire() { + try { + for (String method : new String[]{ + "acquireProvider", "acquireContentProviderClient", + "acquireUnstableProvider", "acquireUnstableContentProviderClient" + }) { + try { + XposedBridge.hookAllMethods(ContentResolver.class, method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + if (param.args[0] instanceof Uri) { + Uri uri = (Uri) param.args[0]; + String newAuth = redirectAuthority(uri.getAuthority()); + if (newAuth != null) { + param.args[0] = uri.buildUpon().authority(newAuth).build(); + } + } else if (param.args[0] instanceof String) { + String newAuth = redirectAuthority((String) param.args[0]); + if (newAuth != null) { + param.args[0] = newAuth; + } + } + } + }); + } catch (Throwable ignored) {} + } + + // 攔截 ContentResolver.call,遇到 SecurityException 則自動重試 + try { + XposedBridge.hookAllMethods(ContentResolver.class, "call", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + for (int i = 0; i < param.args.length; i++) { + if (param.args[i] instanceof Uri) { + Uri uri = (Uri) param.args[i]; + String newAuth = redirectAuthority(uri.getAuthority()); + if (newAuth != null) { + param.args[i] = uri.buildUpon().authority(newAuth).build(); + } + } else if (param.args[i] instanceof String && i == 0) { + String newAuth = redirectAuthority((String) param.args[i]); + if (newAuth != null) { + param.args[i] = newAuth; + } + } + } + } + + @Override + protected void afterHookedMethod(MethodHookParam param) { + if (param.getThrowable() instanceof SecurityException) { + String msg = param.getThrowable().getMessage(); + if (msg != null && (msg.contains("GoogleCertificatesRslt") || + msg.contains("not allowed") || + msg.contains("Access denied"))) { + Log.i(TAG, "GMS rejected call, retrying with MicroG"); + for (int i = 0; i < param.args.length; i++) { + if (param.args[i] instanceof Uri) { + Uri uri = (Uri) param.args[i]; + String authority = uri.getAuthority(); + if (authority != null && authority.contains(REAL_GMS)) { + param.args[i] = uri.buildUpon() + .authority(authority.replace(REAL_GMS, targetGms)) + .build(); + } + } else if (param.args[i] instanceof String && i == 0) { + String s = (String) param.args[i]; + if (s.contains(REAL_GMS)) { + param.args[i] = s.replace(REAL_GMS, targetGms); + } + } + } + param.setThrowable(null); + param.setResult(null); + } + } + } + }); + } catch (Throwable ignored) {} + } catch (Throwable t) { + Log.e(TAG, "Failed to hook ContentResolver", t); + } + } + + private static void hookPackageManagerGetPackageInfo(Context context) { + try { + XposedHelpers.findAndHookMethod( + context.getPackageManager().getClass(), + "getPackageInfo", + String.class, int.class, + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + PackageInfo pi = (PackageInfo) param.getResult(); + if (pi != null && targetGms != null) { + if (targetGms.equals(pi.packageName) && (((int) param.args[1]) & PackageManager.GET_SIGNATURES) != 0) { + if (originalSignature != null && !originalSignature.isEmpty()) { + try { + byte[] sigBytes = android.util.Base64.decode(originalSignature, android.util.Base64.DEFAULT); + pi.signatures = new Signature[]{new Signature(sigBytes)}; + } catch (Throwable ignored) {} + } + } + } + } + } + ); + } catch (Throwable t) { + Log.e(TAG, "Failed to hook PackageManager.getPackageInfo", t); + } + } +} diff --git a/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java b/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java index ced217ff9..799ff6847 100644 --- a/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java +++ b/patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java @@ -95,6 +95,11 @@ public static void onLoad() throws RemoteException, IOException { switchAllClassLoader(); SigBypass.doSigBypass(context, config.optInt("sigBypassLevel")); + if (config.optBoolean("useMicroG")) { + String originalSignature = config.optString("originalSignature"); + GmsRedirector.activate(context, originalSignature); + } + Log.i(TAG, "LSPatch bootstrap completed"); } diff --git a/patch/src/main/java/org/lsposed/patch/LSPatch.java b/patch/src/main/java/org/lsposed/patch/LSPatch.java index c7907a84d..1d0b31ab9 100644 --- a/patch/src/main/java/org/lsposed/patch/LSPatch.java +++ b/patch/src/main/java/org/lsposed/patch/LSPatch.java @@ -96,6 +96,9 @@ public PatchError(String message, Throwable cause) { @Parameter(names = {"-m", "--embed"}, description = "Embed provided modules to apk") private List modules = new ArrayList<>(); + @Parameter(names = {"--useMicroG"}, description = "Redirect GMS calls to community MicroG") + private boolean useMicroG = false; + private static final String ANDROID_MANIFEST_XML = "AndroidManifest.xml"; private static final HashSet ARCHES = new HashSet<>(Arrays.asList( "armeabi-v7a", @@ -250,10 +253,10 @@ public void patch(File srcApkFile, File outputFile) throws PatchError, IOExcepti logger.i("Patching apk..."); // modify manifest - final var config = new PatchConfig(useManager, debuggableFlag, overrideVersionCode, sigbypassLevel, originalSignature, appComponentFactory); + final var config = new PatchConfig(useManager, debuggableFlag, overrideVersionCode, sigbypassLevel, originalSignature, appComponentFactory, useMicroG); final var configBytes = new Gson().toJson(config).getBytes(StandardCharsets.UTF_8); final var metadata = Base64.getEncoder().encodeToString(configBytes); - try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open(), metadata, minSdkVersion))) { + try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open(), metadata, minSdkVersion, originalSignature))) { dstZFile.add(ANDROID_MANIFEST_XML, is); } catch (Throwable e) { throw new PatchError("Error when modifying manifest", e); @@ -344,7 +347,7 @@ private void embedModules(ZFile zFile) { } } - private byte[] modifyManifestFile(InputStream is, String metadata, int minSdkVersion) throws IOException { + private byte[] modifyManifestFile(InputStream is, String metadata, int minSdkVersion, String originalSignature) throws IOException { ModificationProperty property = new ModificationProperty(); if (overrideVersionCode) @@ -354,6 +357,23 @@ private byte[] modifyManifestFile(InputStream is, String metadata, int minSdkVer property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggableFlag)); property.addApplicationAttribute(new AttributeItem("appComponentFactory", PROXY_APP_COMPONENT_FACTORY)); property.addMetaData(new ModificationProperty.MetaData("lspatch", metadata)); + + // 注入 MicroG 偽裝簽名與權限 + if (useMicroG && originalSignature != null && !originalSignature.isEmpty()) { + try { + byte[] sigBytes = Base64.getDecoder().decode(originalSignature); + StringBuilder hex = new StringBuilder(); + for (byte b : sigBytes) { + hex.append(String.format("%02x", b)); + } + property.addMetaData(new ModificationProperty.MetaData("fake-signature", hex.toString())); + property.addUsesPermission("android.permission.FAKE_PACKAGE_SIGNATURE"); + logger.d("Added fake-signature metadata for MicroG compatibility"); + } catch (Exception e) { + logger.e("Failed to add fake-signature: " + e.getMessage()); + } + } + // TODO: replace query_all with queries -> manager if (useManager) property.addUsesPermission("android.permission.QUERY_ALL_PACKAGES"); diff --git a/share/java/src/main/java/org/lsposed/lspatch/share/Constants.java b/share/java/src/main/java/org/lsposed/lspatch/share/Constants.java index f8a8f7f09..fdf16c957 100644 --- a/share/java/src/main/java/org/lsposed/lspatch/share/Constants.java +++ b/share/java/src/main/java/org/lsposed/lspatch/share/Constants.java @@ -11,6 +11,7 @@ public class Constants { final static public String PATCH_FILE_SUFFIX = "-lspatched.apk"; final static public String PROXY_APP_COMPONENT_FACTORY = "org.lsposed.lspatch.metaloader.LSPAppComponentFactoryStub"; final static public String MANAGER_PACKAGE_NAME = "org.lsposed.lspatch"; + final static public String REAL_GMS_PACKAGE_NAME = "com.google.android.gms"; final static public int MIN_ROLLING_VERSION_CODE = 348; final static public int SIGBYPASS_LV_DISABLE = 0; diff --git a/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java b/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java index 2cf9dc0df..9a00dcf20 100644 --- a/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java +++ b/share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java @@ -9,6 +9,7 @@ public class PatchConfig { public final String originalSignature; public final String appComponentFactory; public final LSPConfig lspConfig; + public final boolean useMicroG; public PatchConfig( boolean useManager, @@ -16,7 +17,8 @@ public PatchConfig( boolean overrideVersionCode, int sigBypassLevel, String originalSignature, - String appComponentFactory + String appComponentFactory, + boolean useMicroG ) { this.useManager = useManager; this.debuggable = debuggable; @@ -25,5 +27,6 @@ public PatchConfig( this.originalSignature = originalSignature; this.appComponentFactory = appComponentFactory; this.lspConfig = LSPConfig.instance; + this.useMicroG = useMicroG; } }