diff --git a/README.md b/README.md index 6f76dc4..9cd6ac6 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,30 @@ # 简介 -Exposed 致力于在**非Root**环境下实现Xposed的功能。基本思路是使用沙盒机制运行APP,在沙盒环境下HOOK本进程从而实现HOOK任意APP的功能。 +exposed 致力于为App提供 Xposed 运行环境。基本思路是劫持APP进程启动的入口,加载 Xposed 插件以及 hook框架 [epic](https://github.com/tiann/epic) 。 -Exposed本质上是一个Xposed与沙盒之间的兼容层,它给Xposed模块提供运行环境,但不强依赖于双开宿主。不过目前的唯一实现基于[VirtualApp](https://github.com/asLody/VirtualApp),同时HOOK模块 -使用 [epic](https://github.com/tiann/epic) +exposed本质上是一个 Xposed 与 APP进程 之间的兼容层,它给Xposed模块提供运行环境(如插件加载、hook环境等)。 + +目前使用最广泛的实现是 VirtualXposed,它使用 [VirtualApp](https://github.com/asLody/VirtualApp) 来运行APP并提供进程入口劫持。但是,exposed 本身并不依赖双开宿主;甚至不需要双开。 + +另外,基于APP加固的思路,甚至直接修改APK,或者通过magisk注入进程的方式,都可以实现 Xposed。 + +目前有以下几种实现方式: + +- [VirtualXposed](https://github.com/android-hacker/VirtualXposed) :基于双开实现,通过 VirtualApp 运行目标APK,在进程启动入口加载 exposed。优势:免安装,无篡改签名问题。劣势:性能 & 稳定性受限于双开,无法与系统完全交互。 +- [太极](https://www.coolapk.com/apk/me.weishu.exp):基于修改APK实现。在 Application 类的入口织入 exposed 入口代码,从而加载 exposed。优势:可以与系统完全交互,性能好。劣势:签名改变,虽有独特技术可以绕过所有检测,但是依然有风险。部分APP调用其他会检测签名,使得所有APP必须被“太极化”,风险极高。 +- [太极·Magisk](https://mp.weixin.qq.com/s?__biz=MjM5Njg5ODU2NA==&tempkey=OTkwX0JJa0I4ZW9qcmd5bGlJSXlwQjBJOTZsWGc0TllULXVXdGVicTQxcWRyWE9McnZFQVozRGpNS21OaHEySDNHbFlfMUVudk9wbHo0akE4c29hOTZhNGs5UENXQlFISlFvQjZFSS1CT1dCa1hSZWt4XzFKNV9abEZITTJNOEJkVkotVEdrN2owcmxzeU9WVF9oaVUxdlJwd3pkcHZDWXFPOTFNVEhBeUF%2Bfg%3D%3D&chksm=25983cf012efb5e6ac3fe06bd73883139a89912fa37aee74f3b3baca9e358b2c41a260cee682#rd):通过 Magisk 修改系统文件,在 Zygote 进程启动的时候执行 exposed 入口代码,从而加载 exposed。优势:完全体,不存在上述所有问题。劣势:需要解锁Bootloader和刷机。 # 使用 -Exposed 是一个library,是提供给沙盒开发者使用的;如果你需要在非ROOT环境下运行Xposed,请移步项目 [VAExposed](https://github.com/android-hacker/VAExposed) +Exposed 是一个library,是提供给开发者使用的;如果你需要在非ROOT环境下运行Xposed,请移步项目 [VAExposed](https://github.com/android-hacker/VAExposed) -如果需要在沙盒中提供Xposed运行环境,在给启动沙盒进程的时候,执行如下调用即可: +如果需要在APP进程中中提供Xposed运行环境,在进程启动的入口,执行如下调用即可: ```java ExposedBridge.initOnce(context, applicationInfo, appClassLoader); ``` -同时,为了加载沙盒中的Xposed模块,需要在进程启动的时候执行模块加载: +同时,为了加载Xposed模块,需要在进程启动的时候执行模块加载: ```java ExposedBridge.loadModule(moduleApk, apkOdexDir, moduleLibDir, applicationInfo, appClassLoader); diff --git a/exposed-core/build.gradle b/exposed-core/build.gradle index 3133ea3..1c2cb35 100644 --- a/exposed-core/build.gradle +++ b/exposed-core/build.gradle @@ -1,11 +1,11 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 26 - buildToolsVersion "26.0.2" + compileSdkVersion 28 + buildToolsVersion "28.0.3" defaultConfig { - minSdkVersion 14 + minSdkVersion 19 targetSdkVersion 26 versionCode 1 versionName "1.0" @@ -23,13 +23,15 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'me.weishu:epic:0.3.5' // The AOP Framework - compile 'me.weishu.exposed:exposed-xposedapi:0.3.0' // The Xposed API + compile 'me.weishu.exposed:exposed-xposedapi:0.4.5' // The Xposed API + compile ('me.weishu:epic:0.8.0-c') { // The AOP Framework + exclude group: 'me.weishu.exposed', module: 'exposed-xposedapi' + } compile 'com.getkeepsafe.relinker:relinker:1.2.3' // for load library } group = 'me.weishu.exposed' -version = '0.3.5' +version = '0.8.0' apply plugin: 'com.novoda.bintray-release' diff --git a/exposed-core/src/main/java/de/robv/android/xposed/ExposedHelper.java b/exposed-core/src/main/java/de/robv/android/xposed/ExposedHelper.java index c13de28..a39cfe6 100644 --- a/exposed-core/src/main/java/de/robv/android/xposed/ExposedHelper.java +++ b/exposed-core/src/main/java/de/robv/android/xposed/ExposedHelper.java @@ -1,5 +1,7 @@ package de.robv.android.xposed; +import android.util.Log; + import java.lang.reflect.Member; /** @@ -7,22 +9,20 @@ */ public class ExposedHelper { + private static final String TAG = "ExposedHelper"; + public static void initSeLinux(String processName) { SELinuxHelper.initOnce(); SELinuxHelper.initForProcess(processName); } public static boolean isIXposedMod(Class moduleClass) { - return IXposedMod.class.isAssignableFrom(moduleClass); - } + Log.d(TAG, "module's classLoader : " + moduleClass.getClassLoader() + ", super: " + moduleClass.getSuperclass()); + Log.d(TAG, "IXposedMod's classLoader : " + IXposedMod.class.getClassLoader()); - public static void beforeHookedMethod(XC_MethodHook methodHook, XC_MethodHook.MethodHookParam param) throws Throwable { - methodHook.beforeHookedMethod(param); + return IXposedMod.class.isAssignableFrom(moduleClass); } - public static void afterHookedMethod(XC_MethodHook methodHook, XC_MethodHook.MethodHookParam param) throws Throwable { - methodHook.afterHookedMethod(param); - } public static XC_MethodHook.Unhook newUnHook(XC_MethodHook methodHook, Member member) { return methodHook.new Unhook(member); @@ -34,4 +34,12 @@ public static void callInitZygote(String modulePath, Object moduleInstance) thro param.startsSystemServer = false; ((IXposedHookZygoteInit) moduleInstance).initZygote(param); } + + public static void beforeHookedMethod(XC_MethodHook methodHook, XC_MethodHook.MethodHookParam param) throws Throwable{ + methodHook.beforeHookedMethod(param); + } + + public static void afterHookedMethod(XC_MethodHook methodHook, XC_MethodHook.MethodHookParam param) throws Throwable{ + methodHook.afterHookedMethod(param); + } } diff --git a/exposed-core/src/main/java/me/weishu/exposed/CHAHelper.java b/exposed-core/src/main/java/me/weishu/exposed/CHAHelper.java new file mode 100644 index 0000000..e8ef9a3 --- /dev/null +++ b/exposed-core/src/main/java/me/weishu/exposed/CHAHelper.java @@ -0,0 +1,77 @@ +package me.weishu.exposed; + +import android.app.Application; +import android.content.Context; +import android.content.ContextWrapper; +import android.util.Log; + +import java.lang.reflect.Member; +import java.lang.reflect.Method; + +import de.robv.android.xposed.DexposedBridge; +import de.robv.android.xposed.ExposedHelper; +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; + +/** + * @author weishu + * @date 2018/6/19. + */ +public final class CHAHelper { + private static final String TAG = "CHAHelper"; + + static class ApplicationHookProxy extends XC_MethodHook { + + XC_MethodHook original; + + ApplicationHookProxy(XC_MethodHook original) { + this.original = original; + } + + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + super.beforeHookedMethod(param); + if (param.thisObject == null) { + throw new IllegalArgumentException("can not use static method!!"); + } + + if (param.thisObject instanceof Application) { + ExposedHelper.beforeHookedMethod(this.original, param); + } else { + Log.d(TAG, "ignore non-application of ContextWrapper: " + param.thisObject); + } + } + + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + super.afterHookedMethod(param); + if (param.thisObject == null) { + throw new IllegalArgumentException("can not use static method!!"); + } + + if (param.thisObject instanceof Application) { + ExposedHelper.afterHookedMethod(this.original, param); + } else { + Log.d(TAG, "ignore non-application of ContextWrapper: " + param.thisObject); + } + } + } + + static XC_MethodHook.Unhook replaceForCHA(Member member, final XC_MethodHook callback) { + + if (member.getDeclaringClass() == Application.class && "attach".equals(member.getName())) { + XposedBridge.log("replace Application.attach with ContextWrapper.attachBaseContext for CHA"); + Method m = XposedHelpers.findMethodExact(ContextWrapper.class, "attachBaseContext", Context.class); + return DexposedBridge.hookMethod(m, new ApplicationHookProxy(callback)); + } + + if (member.getDeclaringClass() == Application.class && "onCreate".equals(member.getName())) { + XposedBridge.log("replace Application.onCreate with ContextWrapper.attachBaseContext for CHA"); + Method m = XposedHelpers.findMethodExact(ContextWrapper.class, "attachBaseContext", Context.class); + return DexposedBridge.hookMethod(m, new ApplicationHookProxy(callback)); + } + + return null; + } +} diff --git a/exposed-core/src/main/java/me/weishu/exposed/ExposedBridge.java b/exposed-core/src/main/java/me/weishu/exposed/ExposedBridge.java index c584d27..3407363 100644 --- a/exposed-core/src/main/java/me/weishu/exposed/ExposedBridge.java +++ b/exposed-core/src/main/java/me/weishu/exposed/ExposedBridge.java @@ -1,12 +1,17 @@ package me.weishu.exposed; import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.Fragment; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.os.Build; +import android.os.IBinder; +import android.os.Process; import android.text.TextUtils; +import android.util.Base64; import android.util.Log; import android.util.Pair; import android.view.AbsSavedState; @@ -14,7 +19,6 @@ import android.widget.Toast; import com.getkeepsafe.relinker.ReLinker; -import com.taobao.android.dexposed.DexposedBridge; import java.io.BufferedReader; import java.io.Closeable; @@ -28,25 +32,28 @@ import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; -import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; import dalvik.system.DexClassLoader; +import de.robv.android.xposed.DexposedBridge; import de.robv.android.xposed.ExposedHelper; import de.robv.android.xposed.IXposedHookInitPackageResources; import de.robv.android.xposed.IXposedHookLoadPackage; import de.robv.android.xposed.IXposedHookZygoteInit; import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XSharedPreferences; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedHelpers; import de.robv.android.xposed.callbacks.XC_LoadPackage; -import static com.taobao.android.dexposed.DexposedBridge.log; +import static de.robv.android.xposed.XposedBridge.log; public class ExposedBridge { @@ -61,12 +68,32 @@ public class ExposedBridge { public static final String BASE_DIR = Build.VERSION.SDK_INT >= 24 ? "/data/user_de/0/de.robv.android.xposed.installer/" : BASE_DIR_LEGACY; + private static final String WECHAT = decodeFromBase64("Y29tLnRlbmNlbnQubW0="); + private static final String QQ = decodeFromBase64("Y29tLnRlbmNlbnQubW9iaWxlcXE="); + + private static final int FAKE_XPOSED_VERSION = 91; private static final String VERSION_KEY = "version"; + private static boolean SYSTEM_CLASSLOADER_INJECT = false; private static Pair> lastModuleList = Pair.create(null, null); private static Map exposedClassLoaderMap = new HashMap<>(); private static ClassLoader xposedClassLoader; + private static Context appContext; + private static String currentPackage; + + private volatile static boolean wcdbLoaded = false; + private static ModuleLoadListener sModuleLoadListener = new ModuleLoadListener() { + @Override + public void onLoadingModule(String moduleClassName, ApplicationInfo applicationInfo, ClassLoader appClassLoader) { + } + + @Override + public void onModuleLoaded(String moduleName, ApplicationInfo applicationInfo, ClassLoader appClassLoader) { + initForWeChatTranslate(moduleName, applicationInfo, appClassLoader); + } + }; + /** * Module load result */ @@ -75,24 +102,63 @@ enum ModuleLoadResult { NOT_EXIST, INVALID, SUCCESS, - FAILED + FAILED, + IGNORED } public static void initOnce(Context context, ApplicationInfo applicationInfo, ClassLoader appClassLoader) { + // SYSTEM_CLASSLOADER_INJECT = patchSystemClassLoader(); + XposedBridge.XPOSED_BRIDGE_VERSION = FAKE_XPOSED_VERSION; + appContext = context; + initForPackage(context, applicationInfo); + ReLinker.loadLibrary(context, "epic"); ExposedHelper.initSeLinux(applicationInfo.processName); + XSharedPreferences.setPackageBaseDirectory(new File(applicationInfo.dataDir).getParentFile()); + + initForXposedModule(context, applicationInfo, appClassLoader); initForXposedInstaller(context, applicationInfo, appClassLoader); + initForWechat(context, applicationInfo, appClassLoader); + initForQQ(context, applicationInfo, appClassLoader); + } + + private static void initForPackage(Context context, ApplicationInfo applicationInfo) { + currentPackage = applicationInfo.packageName; + + if (currentPackage == null) { + currentPackage = context.getPackageName(); + } + + System.setProperty("vxp", "1"); + System.setProperty("vxp_user_dir", new File(applicationInfo.dataDir).getParent()); } - public static void patchAppClassLoader(Context baseContext) { - final ClassLoader originClassLoader = baseContext.getClassLoader(); - Object mPackageInfo = XposedHelpers.getObjectField(baseContext, "mPackageInfo"); - ClassLoader appClassLoaderWithXposed = getAppClassLoaderWithXposed(originClassLoader); - XposedHelpers.setObjectField(mPackageInfo, "mClassLoader", appClassLoaderWithXposed); - Thread.currentThread().setContextClassLoader(appClassLoaderWithXposed); + private static boolean patchSystemClassLoader() { + // 1. first create XposedClassLoader -> BootstrapClassLoader + ClassLoader xposedClassLoader = new XposedClassLoader(ExposedBridge.class.getClassLoader()); + + // 2. replace the systemclassloader's parent. + ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); + + try { + Field parent = ClassLoader.class.getDeclaredField("parent"); + parent.setAccessible(true); + parent.set(systemClassLoader, xposedClassLoader); + + log("XposedBridge's BootClassLoader: " + XposedBridge.BOOTCLASSLOADER + ", parent: " + XposedBridge.BOOTCLASSLOADER.getParent()); + // SystemClassLoader -> XposedClassLoader -> BootstrapClassLoader + return systemClassLoader.getParent() == xposedClassLoader; + } catch (NoSuchFieldException e) { + // todo no such field ? use unsafe. + log(e); + return false; + } catch (IllegalAccessException e) { + log(e); + return false; + } } - public static synchronized ClassLoader getAppClassLoaderWithXposed(ClassLoader appClassLoader) { + private static synchronized ClassLoader getAppClassLoaderWithXposed(ClassLoader appClassLoader) { if (exposedClassLoaderMap.containsKey(appClassLoader)) { return exposedClassLoaderMap.get(appClassLoader); } else { @@ -112,27 +178,41 @@ public static synchronized ClassLoader getXposedClassLoader(ClassLoader hostClas } public static ModuleLoadResult loadModule(final String moduleApkPath, String moduleOdexDir, String moduleLibPath, - final ApplicationInfo currentApplicationInfo, ClassLoader appClassLoader) { + final ApplicationInfo currentApplicationInfo, ClassLoader appClassLoader) { + + if (filterApplication(currentApplicationInfo)) { + return ModuleLoadResult.IGNORED; + } final String rootDir = new File(currentApplicationInfo.dataDir).getParent(); loadModuleConfig(rootDir, currentApplicationInfo.processName); if (lastModuleList.second == null || !lastModuleList.second.contains(moduleApkPath)) { - log("module:" + moduleApkPath + " is disabled, ignore"); + Log.i(TAG, "module:" + moduleApkPath + " is disabled, ignore"); return ModuleLoadResult.DISABLED; } - log("Loading modules from " + moduleApkPath); + Log.i(TAG, "Loading modules from " + moduleApkPath + " for process: " + currentApplicationInfo.processName + " i s c: " + SYSTEM_CLASSLOADER_INJECT); if (!new File(moduleApkPath).exists()) { log(moduleApkPath + " does not exist"); return ModuleLoadResult.NOT_EXIST; } - ClassLoader hostClassLoader = ExposedBridge.class.getClassLoader(); - ClassLoader appClassLoaderWithXposed = getAppClassLoaderWithXposed(appClassLoader); + ClassLoader appClassLoaderWithXposed; + ClassLoader mcl; + if (SYSTEM_CLASSLOADER_INJECT) { + // we replace the systemclassloader's parent success, go with xposed's way + appClassLoaderWithXposed = appClassLoader; + mcl = new DexClassLoader(moduleApkPath, moduleOdexDir, moduleLibPath, XposedBridge.BOOTCLASSLOADER); + } else { + // replace failed, just wrap. + ClassLoader hostClassLoader = ExposedBridge.class.getClassLoader(); + appClassLoaderWithXposed = getAppClassLoaderWithXposed(appClassLoader); + mcl = new DexClassLoader(moduleApkPath, moduleOdexDir, moduleLibPath, getXposedClassLoader(hostClassLoader)); + // ClassLoader mcl = new DexClassLoader(moduleApkPath, moduleOdexDir, moduleLibPath, hostClassLoader); + } - ClassLoader mcl = new DexClassLoader(moduleApkPath, moduleOdexDir, moduleLibPath, hostClassLoader); InputStream is = mcl.getResourceAsStream("assets/xposed_init"); if (is == null) { log("assets/xposed_init not found in the APK"); @@ -147,10 +227,16 @@ public static ModuleLoadResult loadModule(final String moduleApkPath, String mod if (moduleClassName.isEmpty() || moduleClassName.startsWith("#")) continue; + if (filterModuleForApp(currentApplicationInfo, moduleClassName)) { + XposedBridge.log("ignore module: " + moduleClassName + " for application: " + currentApplicationInfo.packageName); + continue; + } try { - log(" Loading class " + moduleClassName); + Log.i(TAG, " Loading class " + moduleClassName); Class moduleClass = mcl.loadClass(moduleClassName); + sModuleLoadListener.onLoadingModule(moduleClassName, currentApplicationInfo, mcl); + if (!ExposedHelper.isIXposedMod(moduleClass)) { log(" This class doesn't implement any sub-interface of IXposedMod, skipping it"); continue; @@ -183,38 +269,156 @@ public static ModuleLoadResult loadModule(final String moduleApkPath, String mod // TODO: 17/12/1 Support Resource hook } - return ModuleLoadResult.SUCCESS; + sModuleLoadListener.onModuleLoaded(moduleClassName, currentApplicationInfo, mcl); + } catch (Throwable t) { log(t); } + return ModuleLoadResult.SUCCESS; } } catch (IOException e) { log(e); } finally { - try { - is.close(); - } catch (IOException ignored) { - } + closeSliently(is); } return ModuleLoadResult.FAILED; } + private static boolean ignoreHooks(Member member) { + if (member == null) { + return false; + } + + String name = member.getDeclaringClass().getName(); + if (QQ.equals(currentPackage)) { + // TODO, we just ignore this hook to avoid crash, fix it when you figure out it. (getManager) + if (name.contains("QQAppInterface")) { + // Log.i("mylog", "ignore hook for: " + member); + return true; + } + } + + return false; + } + + private static void presetMethod(Member method) { + if (method == null) { + return; + } + + if (WECHAT.equals(currentPackage)) { + Class declaringClass = method.getDeclaringClass(); + if (declaringClass.getName().contains("wcdb")) { + if (!wcdbLoaded) { + ClassLoader loader = declaringClass.getClassLoader(); + Class sqliteDataBaseClass = null; + try { + sqliteDataBaseClass = loader.loadClass("com.tencent.wcdb.database.SQLiteDatabase"); + } catch (ClassNotFoundException ignored) { + XposedBridge.log("preload sqlite class failed!!!"); + } + if (sqliteDataBaseClass == null) { + return; + } + + wcdbLoaded = true; + } + } + } + } + public static XC_MethodHook.Unhook hookMethod(Member method, XC_MethodHook callback) { - final com.taobao.android.dexposed.XC_MethodHook.Unhook unhook = DexposedBridge.hookMethod(method, new XC_MethodHookX2Dx(callback)); + if (ignoreHooks(method)) { + return null; + } + + presetMethod(method); + + XC_MethodHook.Unhook replaceUnhook = CHAHelper.replaceForCHA(method, callback); + if (replaceUnhook != null) { + return ExposedHelper.newUnHook(callback, replaceUnhook.getHookedMethod()); + } + + final XC_MethodHook.Unhook unhook = DexposedBridge.hookMethod(method, callback); return ExposedHelper.newUnHook(callback, unhook.getHookedMethod()); } + public static Object invokeOriginalMethod(Member method, Object thisObject, Object[] args) + throws NullPointerException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + + return DexposedBridge.invokeOriginalMethod(method, thisObject, args); + } + + + + private static void initForXposedModule(Context context, ApplicationInfo applicationInfo, ClassLoader appClassLoader) { + InputStream inputStream = null; + + try { + inputStream = context.getAssets().open("xposed_init"); + System.setProperty("epic.force", "true"); + } catch (IOException e) { + Log.i(TAG, applicationInfo.packageName + " is not a Xposed module, do not init epic.force"); + } finally { + closeSliently(inputStream); + } + } + + private static boolean isXposedInstaller(ApplicationInfo applicationInfo) { + return XPOSED_INSTALL_PACKAGE.equals(applicationInfo.packageName); + } + + private static boolean filterApplication(ApplicationInfo applicationInfo) { + if (applicationInfo == null) { + return true; + } + + if (isXposedInstaller(applicationInfo)) { + return true; + } + + if (decodeFromBase64("Y29tLnRlbmNlbnQubW06cHVzaA==").equalsIgnoreCase(applicationInfo.processName)) { + // com.tencent.mm:push + XposedBridge.log("ignore process for wechat push."); + return true; + } + + return false; + } + + private static boolean filterModuleForApp(ApplicationInfo applicationInfo, String moduleEntry) { + if (applicationInfo == null || applicationInfo.packageName == null) { + return false; + } + + final String WECHAT_JUMP_HELPER = "com.emily.mmjumphelper.xposed.XposedMain"; + + if (WECHAT.equals(applicationInfo.packageName)) { + if (applicationInfo.processName.contains("appbrand")) { + // wechat app brand + if (WECHAT_JUMP_HELPER.equals(moduleEntry)) { + // now only load module for appbrand. + return false; + } else { + return true; + } + } else { + if (WECHAT_JUMP_HELPER.equals(moduleEntry)) { + return true; + } + } + } + + return false; + } private static void initForXposedInstaller(Context context, ApplicationInfo applicationInfo, ClassLoader appClassLoader) { - if (!XPOSED_INSTALL_PACKAGE.equals(applicationInfo.packageName)) { + if (!isXposedInstaller(applicationInfo)) { return; } - Log.i(TAG, "initForXposedInstaller"); - // XposedInstaller - final int fakeXposedVersion = 89; - final String fakeVersionString = String.valueOf(fakeXposedVersion); + final String fakeVersionString = String.valueOf(FAKE_XPOSED_VERSION); final File xposedProp = context.getFileStreamPath("xposed_prop"); if (!xposedProp.exists()) { writeXposedProperty(xposedProp, fakeVersionString, false); @@ -224,98 +428,219 @@ private static void initForXposedInstaller(Context context, ApplicationInfo appl if (!fakeVersionString.equals(oldVersion)) { writeXposedProperty(xposedProp, fakeVersionString, true); } else { - log("xposed version keep same, continue."); + Log.i(TAG, "xposed version keep same, continue."); } } final Class xposedApp = XposedHelpers.findClass("de.robv.android.xposed.installer.XposedApp", appClassLoader); - final Object xposed_prop_files = XposedHelpers.getStaticObjectField(xposedApp, "XPOSED_PROP_FILES"); - final int length = Array.getLength(xposed_prop_files); - String xposedPropPath = xposedProp.getPath(); - for (int i = 0; i < length; i++) { - Array.set(xposed_prop_files, i, xposedPropPath); - } + try { + final Object xposed_prop_files = XposedHelpers.getStaticObjectField(xposedApp, "XPOSED_PROP_FILES"); + final int length = Array.getLength(xposed_prop_files); + String xposedPropPath = xposedProp.getPath(); + for (int i = 0; i < length; i++) { + Array.set(xposed_prop_files, i, xposedPropPath); + } - DexposedBridge.findAndHookMethod(xposedApp, "getActiveXposedVersion", new com.taobao.android.dexposed.XC_MethodHook() { - @Override - protected void beforeHookedMethod(MethodHookParam param) throws Throwable { - super.beforeHookedMethod(param); - param.setResult(fakeXposedVersion); + + XposedHelpers.findAndHookMethod(xposedApp, "getActiveXposedVersion", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + super.beforeHookedMethod(param); + param.setResult(FAKE_XPOSED_VERSION); + } + }); + XposedHelpers.findAndHookMethod(xposedApp, "getInstalledXposedVersion", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable { + super.beforeHookedMethod(param); + param.setResult(FAKE_XPOSED_VERSION); + } + }); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // fix bug on Android O: https://github.com/emilsjolander/StickyListHeaders/issues/477 + Class stickyListHeadersClass = XposedHelpers.findClass("se.emilsjolander.stickylistheaders.StickyListHeadersListView", appClassLoader); + XposedHelpers.findAndHookMethod(stickyListHeadersClass, "onSaveInstanceState", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + super.beforeHookedMethod(param); + param.setResult(AbsSavedState.EMPTY_STATE); + Field mPrivateFlags = XposedHelpers.findField(View.class, "mPrivateFlags"); + int flags = mPrivateFlags.getInt(param.thisObject); + mPrivateFlags.set(param.thisObject, flags | 0x00020000); + } + }); } - }); - DexposedBridge.findAndHookMethod(xposedApp, "getInstalledXposedVersion", new com.taobao.android.dexposed.XC_MethodHook() { - @Override - protected void beforeHookedMethod(MethodHookParam param) throws Throwable { - super.beforeHookedMethod(param); - param.setResult(fakeXposedVersion); + } catch (Throwable ignored) { + // only support 3.1.5 and above. + try { + Toast.makeText(context, "The XposedInstaller you used is not supported.", Toast.LENGTH_SHORT).show(); + } catch (Throwable ignored2) { } - }); + } final Constructor fileConstructor1 = XposedHelpers.findConstructorExact(File.class, String.class); final Constructor fileConstructor2 = XposedHelpers.findConstructorExact(File.class, String.class, String.class); final String dataDir = applicationInfo.dataDir; - DexposedBridge.hookMethod(fileConstructor1, new com.taobao.android.dexposed.XC_MethodHook() { + XposedBridge.hookMethod(fileConstructor1, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { super.beforeHookedMethod(param); - final String path = (String)param.args[0]; + final String path = (String) param.args[0]; if (path.startsWith(BASE_DIR)) { param.args[0] = path.replace(BASE_DIR, path.equals(BASE_DIR) ? dataDir : dataDir + "/exposed_"); } } }); - DexposedBridge.hookMethod(fileConstructor2, new com.taobao.android.dexposed.XC_MethodHook() { + XposedBridge.hookMethod(fileConstructor2, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { super.beforeHookedMethod(param); - final String path = (String)param.args[0]; + final String path = (String) param.args[0]; if (path.startsWith(BASE_DIR)) { param.args[0] = path.replace(BASE_DIR, path.equals(BASE_DIR) ? dataDir : dataDir + "/exposed_"); } } }); - // fix bug on Android O: https://github.com/emilsjolander/StickyListHeaders/issues/477 - Class stickyListHeadersClass = XposedHelpers.findClass("se.emilsjolander.stickylistheaders.StickyListHeadersListView", appClassLoader); - DexposedBridge.findAndHookMethod(stickyListHeadersClass, "onSaveInstanceState", new com.taobao.android.dexposed.XC_MethodHook() { + + } + + + private static void initForWeChatTranslate(String moduleClassName, ApplicationInfo applicationInfo, ClassLoader appClassLoader) { + if (!"com.hkdrjxy.wechart.xposed.XposedInit".equals(moduleClassName)) { + return; + } + + if (!("com.hiwechart.translate".equals(applicationInfo.processName) || "com.tencent.mm".equals(applicationInfo.processName))) { + return; + } + + final IBinder[] translateService = new IBinder[1]; + Intent intent = new Intent(); + intent.setAction("com.hiwechart.translate.aidl.TranslateService"); + ComponentName v2 = new ComponentName("com.hiwechart.translate", "com.hiwechart.translate.aidl.TranslateService"); + intent.setComponent(v2); + appContext.bindService(intent, new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + translateService[0] = service; + } + + @Override + public void onServiceDisconnected(ComponentName name) { + + } + }, Context.BIND_AUTO_CREATE); + + Class serviceManager = XposedHelpers.findClass("android.os.ServiceManager", appClassLoader); + final String serviceName = Build.VERSION.SDK_INT >= 21 ? "user.wechart.trans" : "wechart.trans"; + XposedHelpers.findAndHookMethod(serviceManager, "getService", String.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { super.beforeHookedMethod(param); - param.setResult(AbsSavedState.EMPTY_STATE); - Field mPrivateFlags = XposedHelpers.findField(View.class, "mPrivateFlags"); - int flags = mPrivateFlags.getInt(param.thisObject); - mPrivateFlags.set(param.thisObject, flags | 0x00020000); + if (serviceName.equals(param.args[0])) { + Log.i("mylog", "get service :" + translateService[0]); + param.setResult(translateService[0]); + } } }); + } - // make the module reload when enter ModuleFragment - DexposedBridge.findAndHookMethod(Fragment.class, "onResume", new com.taobao.android.dexposed.XC_MethodHook() { - @Override - protected void afterHookedMethod(MethodHookParam param) throws Throwable { - super.afterHookedMethod(param); - Fragment fragment = (Fragment) param.thisObject; - if ("de.robv.android.xposed.installer.ModulesFragment".equals(fragment.getClass().getName())) { - // ModulesFragment, call reload - Class moduleUtilClass = fragment.getClass().getClassLoader().loadClass("de.robv.android.xposed.installer.util.ModuleUtil"); - Object moduleUtil = XposedHelpers.callStaticMethod(moduleUtilClass, "getInstance"); - XposedHelpers.callMethod(moduleUtil, "reloadInstalledModules"); - Activity activity = fragment.getActivity(); - log("module fragment reload success, activity:" + activity); - if (activity != null) { - String tips = Locale.getDefault().toString().contains("zh") ? "勾选模块之后,需要在主界面右上角按钮 -> 重启 才能生效哦~" : - "module will take effect after Settings->Reboot!"; - Toast.makeText(activity, tips, Toast.LENGTH_SHORT).show(); + @SuppressLint("ApplySharedPref") + private static void initForQQ(Context context, ApplicationInfo applicationInfo, ClassLoader appClassLoader) { + if (applicationInfo == null) { + return; + } + final String QQ = decodeFromBase64("Y29tLnRlbmNlbnQubW9iaWxlcXE="); + if (!QQ.equals(applicationInfo.packageName)) { + return; + } + if (QQ.equals(applicationInfo.processName)) { + SharedPreferences sp = context.getSharedPreferences(decodeFromBase64("aG90cGF0Y2hfcHJlZmVyZW5jZQ=="), // hotpatch_preference + Context.MODE_PRIVATE); + sp.edit().remove(decodeFromBase64("a2V5X2NvbmZpZ19wYXRjaF9kZXg=")).commit(); // key_config_patch_dex + } + } + + private static void initForWechat(Context context, ApplicationInfo applicationInfo, ClassLoader appClassLoader) { + if (applicationInfo == null) { + return; + } + + if (!WECHAT.equals(applicationInfo.packageName)) { + return; + } + if (WECHAT.equals(applicationInfo.processName)) { + // for main process + String dataDir = applicationInfo.dataDir; + + File tinker = new File(dataDir, decodeFromBase64("dGlua2Vy")); + File tinker_temp = new File(dataDir, decodeFromBase64("dGlua2VyX3RlbXA=")); + File tinker_server = new File(dataDir, decodeFromBase64("dGlua2VyX3NlcnZlcg==")); + + deleteDir(tinker); + deleteDir(tinker_temp); + deleteDir(tinker_server); + + final int mainProcessId = Process.myPid(); + XposedHelpers.findAndHookMethod(Process.class, "killProcess", int.class, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + super.beforeHookedMethod(param); + int pid = (int) param.args[0]; + if (pid != mainProcessId) { + return; + } + // try kill main process, find stack + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + if (stackTrace == null) { + return; + } + + for (StackTraceElement stackTraceElement : stackTrace) { + if (stackTraceElement.getClassName().contains("com.tencent.mm.app")) { + XposedBridge.log("do not suicide..." + Arrays.toString(stackTrace)); + param.setResult(null); + break; + } } } + }); + } + } + + public static boolean deleteDir(File dir) { + if (dir == null) { + return false; + } + if (dir.isDirectory()) { + String[] children = dir.list(); + for (String file : children) { + boolean success = deleteDir(new File(dir, file)); + if (!success) { + return false; + } } - }); + } + return dir.delete(); + } + + /** + * avoid from being searched by google. + * + * @param base64 + * @return + */ + private static String decodeFromBase64(String base64) { + return new String(Base64.decode(base64, 0)); } /** * write xposed property file to fake xposedinstaller + * * @param propertyFile the property file used by XposedInstaller - * @param version to fake version - * @param retry need retry, when retry, delete file and try again + * @param version to fake version + * @param retry need retry, when retry, delete file and try again */ private static void writeXposedProperty(File propertyFile, String version, boolean retry) { Properties properties = new Properties(); @@ -354,25 +679,26 @@ private static String getXposedVersionFromProperty(File propertyFile) { /** * try read module config fules. + * * @return is need check module */ private static boolean loadModuleConfig(String rootDir, String processName) { if (lastModuleList != null && TextUtils.equals(lastModuleList.first, processName) && lastModuleList.second != null) { - Log.d(TAG, "lastmodule valid, do not load config repeat"); + // Log.d(TAG, "lastmodule valid, do not load config repeat"); return true; // xposed installer has config file, and has already loaded for this process, return. } // load modules final File xposedInstallerDir = new File(rootDir, XPOSED_INSTALL_PACKAGE); - Log.d(TAG, "xposedInstaller Dir:" + xposedInstallerDir); + // Log.d(TAG, "xposedInstaller Dir:" + xposedInstallerDir); if (!xposedInstallerDir.exists()) { - Log.d(TAG, "XposedInstaller not installed, ignore."); + // Log.d(TAG, "XposedInstaller not installed, ignore."); return false; // xposed installer not enabled, must load all. } final File modiles = new File(xposedInstallerDir, "exposed_conf/modules.list"); - Log.d(TAG, "module file:" + modiles); + // Log.d(TAG, "module file:" + modiles); if (!modiles.exists()) { Log.d(TAG, "xposed installer's modules not exist, ignore."); return false; // xposed installer config file not exist, load all. @@ -394,7 +720,7 @@ private static boolean loadModuleConfig(String rootDir, String processName) { } lastModuleList = Pair.create(processName, moduleSet); - Log.d(TAG, "last moduleslist: " + lastModuleList); + // Log.d(TAG, "last moduleslist: " + lastModuleList); return true; } catch (IOException e) { e.printStackTrace(); diff --git a/exposed-core/src/main/java/me/weishu/exposed/LogcatService.java b/exposed-core/src/main/java/me/weishu/exposed/LogcatService.java index b0d8f63..9351d6e 100644 --- a/exposed-core/src/main/java/me/weishu/exposed/LogcatService.java +++ b/exposed-core/src/main/java/me/weishu/exposed/LogcatService.java @@ -7,13 +7,12 @@ import android.text.TextUtils; import android.util.Log; -import com.taobao.android.dexposed.DexposedBridge; - import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedHelpers; /** @@ -57,7 +56,9 @@ public void run() { List cmds = new ArrayList(); cmds.add("sh"); cmds.add("-c"); - cmds.add("logcat -v time -s XposedStartupMarker:D Xposed:I appproc:I XposedInstaller:I art:F DexposedBridge:I >> " + path); + cmds.add("logcat -v time -s XposedStartupMarker:D Xposed:I appproc:I XposedInstaller:I art:F DexposedBridge:I ExposedBridge:D " + + "Runtime:I EpicNative:D VClientImpl:D VApp:I " + + " >> " + path); ProcessBuilder pb = new ProcessBuilder(cmds); Process p = pb.start(); p.waitFor(); @@ -72,7 +73,7 @@ public void run() { logcatThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { - DexposedBridge.log(e); + XposedBridge.log(e); // Do nothing else. mReading = false; } diff --git a/exposed-core/src/main/java/me/weishu/exposed/MethodHookParamDx2X.java b/exposed-core/src/main/java/me/weishu/exposed/MethodHookParamDx2X.java deleted file mode 100644 index 3c4a1c9..0000000 --- a/exposed-core/src/main/java/me/weishu/exposed/MethodHookParamDx2X.java +++ /dev/null @@ -1,67 +0,0 @@ -package me.weishu.exposed; - -import android.os.Bundle; - -import de.robv.android.xposed.XC_MethodHook; - -/** - * Created by weishu on 17/11/30. - */ - -public class MethodHookParamDx2X extends XC_MethodHook.MethodHookParam { - - private com.taobao.android.dexposed.XC_MethodHook.MethodHookParam beWrapped; - - public MethodHookParamDx2X(com.taobao.android.dexposed.XC_MethodHook.MethodHookParam beWrapped) { - this.beWrapped = beWrapped; - - this.method = beWrapped.method; - this.args = beWrapped.args; // reference, lalala - this.thisObject = beWrapped.thisObject; - } - - @Override - public synchronized Bundle getExtra() { - return beWrapped.getExtra(); - } - - @Override - public Object getObjectExtra(String key) { - return beWrapped.getObjectExtra(key); - } - - @Override - public void setObjectExtra(String key, Object o) { - beWrapped.setObjectExtra(key, o); - } - - @Override - public Object getResult() { - return beWrapped.getResult(); - } - - @Override - public void setResult(Object result) { - beWrapped.setResult(result); - } - - @Override - public Throwable getThrowable() { - return beWrapped.getThrowable(); - } - - @Override - public boolean hasThrowable() { - return beWrapped.hasThrowable(); - } - - @Override - public void setThrowable(Throwable throwable) { - beWrapped.setThrowable(throwable); - } - - @Override - public Object getResultOrThrowable() throws Throwable { - return beWrapped.getResultOrThrowable(); - } -} diff --git a/exposed-core/src/main/java/me/weishu/exposed/ModuleLoadListener.java b/exposed-core/src/main/java/me/weishu/exposed/ModuleLoadListener.java new file mode 100644 index 0000000..0f6a924 --- /dev/null +++ b/exposed-core/src/main/java/me/weishu/exposed/ModuleLoadListener.java @@ -0,0 +1,12 @@ +package me.weishu.exposed; + +import android.content.pm.ApplicationInfo; + +/** + * author: weishu on 18/1/14. + */ + +public interface ModuleLoadListener { + void onLoadingModule(String moduleClassName, ApplicationInfo applicationInfo, ClassLoader appClassLoader); + void onModuleLoaded(String moduleClassName, ApplicationInfo applicationInfo, ClassLoader appClassLoader); +} diff --git a/exposed-core/src/main/java/me/weishu/exposed/ParamDx2X.java b/exposed-core/src/main/java/me/weishu/exposed/ParamDx2X.java deleted file mode 100644 index ff455d0..0000000 --- a/exposed-core/src/main/java/me/weishu/exposed/ParamDx2X.java +++ /dev/null @@ -1,33 +0,0 @@ -package me.weishu.exposed; - -import android.os.Bundle; - -import de.robv.android.xposed.callbacks.XCallback; - -/** - * Created by weishu on 17/11/30. - */ - -public class ParamDx2X extends XCallback.Param { - - private com.taobao.android.dexposed.callbacks.XCallback.Param beWrapped; - - public ParamDx2X(com.taobao.android.dexposed.callbacks.XCallback.Param beWrapped) { - this.beWrapped = beWrapped; - } - - @Override - public synchronized Bundle getExtra() { - return beWrapped.getExtra(); - } - - @Override - public Object getObjectExtra(String key) { - return beWrapped.getObjectExtra(key); - } - - @Override - public void setObjectExtra(String key, Object o) { - beWrapped.setObjectExtra(key, o); - } -} diff --git a/exposed-core/src/main/java/me/weishu/exposed/XC_MethodHookX2Dx.java b/exposed-core/src/main/java/me/weishu/exposed/XC_MethodHookX2Dx.java deleted file mode 100644 index d0338af..0000000 --- a/exposed-core/src/main/java/me/weishu/exposed/XC_MethodHookX2Dx.java +++ /dev/null @@ -1,32 +0,0 @@ -package me.weishu.exposed; - -import de.robv.android.xposed.ExposedHelper; -import de.robv.android.xposed.XC_MethodHook; -import de.robv.android.xposed.XposedHelpers; - -/** - * Xposed's XC_MethodHook 2 Dexposed's XC_Method. - */ -public class XC_MethodHookX2Dx extends com.taobao.android.dexposed.XC_MethodHook { - - private XC_MethodHook beWrapped; - - public XC_MethodHookX2Dx(XC_MethodHook methodHook) { - beWrapped = methodHook; - } - - @Override - protected void beforeHookedMethod(com.taobao.android.dexposed.XC_MethodHook.MethodHookParam param) throws Throwable { - ExposedHelper.beforeHookedMethod(beWrapped, new MethodHookParamDx2X(param)); - } - - @Override - protected void afterHookedMethod(com.taobao.android.dexposed.XC_MethodHook.MethodHookParam param) throws Throwable { - ExposedHelper.afterHookedMethod(beWrapped, new MethodHookParamDx2X(param)); - } - - @Override - protected void call(Param param) throws Throwable { - XposedHelpers.callMethod(beWrapped, "call", new ParamDx2X(param)); - } -} diff --git a/exposed-core/src/main/java/me/weishu/exposed/XposedClassLoader.java b/exposed-core/src/main/java/me/weishu/exposed/XposedClassLoader.java index 42810dc..ae2cdfb 100644 --- a/exposed-core/src/main/java/me/weishu/exposed/XposedClassLoader.java +++ b/exposed-core/src/main/java/me/weishu/exposed/XposedClassLoader.java @@ -3,17 +3,22 @@ /** * XposedClassLoader: use to only load xposed api classes. */ -public class XposedClassLoader extends ClassLoader { +class XposedClassLoader extends ClassLoader { private ClassLoader mHostClassLoader; - public XposedClassLoader(ClassLoader hostClassLoader) { - super(ClassLoader.getSystemClassLoader()); // parent is BootClassLoader + + XposedClassLoader(ClassLoader hostClassLoader) { + super(ClassLoader.getSystemClassLoader().getParent()); // BootstrapClassLoader mHostClassLoader = hostClassLoader; } @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - if (name.startsWith("de.robv.android.xposed") || name.startsWith("android")) { + if (name.startsWith("de.robv.android.xposed") + || name.startsWith("android") + || name.startsWith("external") + || name.startsWith("me.weishu.epic.art") + || name.startsWith("com.taobao.android.dexposed")) { return mHostClassLoader.loadClass(name); } return super.loadClass(name, resolve);