[原创]以WechatEnhancement为例看Xposed插件适配APP不同版本的方法
最近在研究Xposed插件,那么一个很基本的问题就是:如何让同一个插件能够适配想要hook的App的多个不同的版本。
那么就从比较著名的WechatEnhancement插件入手吧。根据描述,它支持多个版本的微信。
话不多说,开始动手。先下载源代码并解压缩,然后切换到文件夹的根目录并输入
grep -ril version .
也就是列出根目录以及所有子目录下包含version字符串(不区分大小写)的所有文件。得到以下结果:
./app/build.gradle ./app/src/main/res/layout/preference_range.xml ./app/src/main/res/values/colors.xml ./app/src/main/AndroidManifest.xml ./app/src/main/java/me/firesun/wechat/enhancement/util/HookParams.java ./app/src/main/java/me/firesun/wechat/enhancement/util/SearchClasses.java ./app/src/main/java/me/firesun/wechat/enhancement/Main.java ./app/src/main/java/me/firesun/wechat/enhancement/SettingsActivity.java ./LICENSE ./gradlew.bat
因为源代码应该是在./app/src/main/java/me/firesun/wechat/enhancement/文件夹下,所以我们接下来要重点关注该文件夹下的文件。
那么就从./app/src/main/java/me/firesun/wechat/enhancement/Main.java开始看起。
先贴源代码:
package me.firesun.wechat.enhancement;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
import me.firesun.wechat.enhancement.plugin.ADBlock;
import me.firesun.wechat.enhancement.plugin.AntiRevoke;
import me.firesun.wechat.enhancement.plugin.AntiSnsDelete;
import me.firesun.wechat.enhancement.plugin.AutoLogin;
import me.firesun.wechat.enhancement.plugin.HideModule;
import me.firesun.wechat.enhancement.plugin.IPlugin;
import me.firesun.wechat.enhancement.plugin.Limits;
import me.firesun.wechat.enhancement.plugin.LuckMoney;
import me.firesun.wechat.enhancement.util.HookParams;
import me.firesun.wechat.enhancement.util.SearchClasses;
import static de.robv.android.xposed.XposedBridge.log;
public class Main implements IXposedHookLoadPackage {
private static IPlugin[] plugins = {
new ADBlock(),
new AntiRevoke(),
new AntiSnsDelete(),
new AutoLogin(),
new HideModule(),
new LuckMoney(),
new Limits(),
};
@Override
public void handleLoadPackage(final LoadPackageParam lpparam) {
if (lpparam.packageName.equals(HookParams.WECHAT_PACKAGE_NAME)) {
try {
XposedHelpers.findAndHookMethod(ContextWrapper.class, "attachBaseContext", Context.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Context context = (Context) param.args[0];
String processName = lpparam.processName;
//Only hook important process
if (!processName.equals(HookParams.WECHAT_PACKAGE_NAME) &&
!processName.equals(HookParams.WECHAT_PACKAGE_NAME + ":tools")
) {
return;
}
String versionName = getVersionName(context, HookParams.WECHAT_PACKAGE_NAME);
log("Found wechat version:" + versionName);
if (!HookParams.hasInstance()) {
SearchClasses.init(context, lpparam, versionName);
loadPlugins(lpparam);
}
}
});
} catch (Error | Exception e) {
}
}
}
private String getVersionName(Context context, String packageName) {
try {
PackageManager packageManager = context.getPackageManager();
PackageInfo packInfo = packageManager.getPackageInfo(packageName, 0);
return packInfo.versionName;
} catch (PackageManager.NameNotFoundException e) {
}
return "";
}
private void loadPlugins(LoadPackageParam lpparam) {
for (IPlugin plugin : plugins) {
try {
plugin.hook(lpparam);
} catch (Error | Exception e) {
log("loadPlugins error" + e);
}
}
}
}
首先,该类实现了IXposedHookLoadPackage接口,并重写了handleLoadPackage函数。在该文件中搜索version,发现关键代码:
String versionName = getVersionName(context, HookParams.WECHAT_PACKAGE_NAME);
这句话即是获取微信版本号的代码。HookParams.WECHAT_PACKAGE_NAME的值为"com.tencent.mm",即微信的包名。
继续往下看:
if (!HookParams.hasInstance()) {
SearchClasses.init(context, lpparam, versionName);
loadPlugins(lpparam);
}
这里再一次提到了HookParams类。那么我们就看看这是一个什么类。
贴一下./app/src/main/java/me/firesun/wechat/enhancement/util/HookParams.java的源代码:
package me.firesun.wechat.enhancement.util;
public class HookParams {
public static final String SAVE_WECHAT_ENHANCEMENT_CONFIG = "wechat.intent.action.SAVE_WECHAT_ENHANCEMENT_CONFIG";
public static final String WECHAT_ENHANCEMENT_CONFIG_NAME = "wechat_enhancement_config";
public static final String WECHAT_PACKAGE_NAME = "com.tencent.mm";
public static final int VERSION_CODE = 46; //大版本变动时候才需要修改
public String SQLiteDatabaseClassName = "com.tencent.wcdb.database.SQLiteDatabase";
public String SQLiteDatabaseUpdateMethod = "updateWithOnConflict";
public String SQLiteDatabaseInsertMethod = "insert";
public String SQLiteDatabaseDeleteMethod = "delete";
public String ContactInfoUIClassName = "com.tencent.mm.plugin.profile.ui.ContactInfoUI";
public String ContactInfoClassName;
public String ChatroomInfoUIClassName = "com.tencent.mm.plugin.chatroom.ui.ChatroomInfoUI";
public String WebWXLoginUIClassName = "com.tencent.mm.plugin.webwx.ui.ExtDeviceWXLoginUI";
public String AlbumPreviewUIClassName = "com.tencent.mm.plugin.gallery.ui.AlbumPreviewUI";
public String SelectContactUIClassName = "com.tencent.mm.ui.contact.SelectContactUI";
public String MMActivityClassName = "com.tencent.mm.ui.MMActivity";
public String SelectConversationUIClassName = "com.tencent.mm.ui.transmit.SelectConversationUI";
public String SelectConversationUICheckLimitMethod;
public String LuckyMoneyReceiveUIClassName = "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI";
public String XMLParserClassName;
public String XMLParserMethod;
public String MsgInfoClassName;
public String MsgInfoStorageClassName;
public String MsgInfoStorageInsertMethod;
public String ReceiveUIParamNameClassName;
public String ReceiveUIMethod;
public String NetworkRequestClassName;
public String RequestCallerClassName;
public String RequestCallerMethod;
public String GetNetworkByModelMethod;
public String ReceiveLuckyMoneyRequestClassName;
public String ReceiveLuckyMoneyRequestMethod;
public String LuckyMoneyRequestClassName;
public String GetTransferRequestClassName;
public boolean hasTimingIdentifier = true;
public String versionName;
public int versionCode;
private static HookParams instance = null;
private HookParams() {
}
public static HookParams getInstance() {
if (instance == null)
instance = new HookParams();
return instance;
}
public static void setInstance(HookParams i) {
instance = i;
}
public static boolean hasInstance() {
return instance != null;
}
}
可以看到HookParams类是一个懒汉式单例模式类。它的属性有LuckyMoneyReceiveUIClassName等等。
那么,由于第一次运行HookParams.hasInstance()当然会返回false,因此会执行SearchClasses.init(context, lpparam, versionName);。
那么就看看SearchClasses.java:
package me.firesun.wechat.enhancement.util;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import com.google.gson.Gson;
import net.dongliu.apk.parser.ApkFile;
import net.dongliu.apk.parser.bean.DexClass;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import de.robv.android.xposed.XSharedPreferences;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
import me.firesun.wechat.enhancement.Main;
import static me.firesun.wechat.enhancement.util.ReflectionUtil.log;
public class SearchClasses {
private static List<String> wxClasses = new ArrayList<>();
private static XSharedPreferences preferencesInstance = null;
public static void init(Context context, XC_LoadPackage.LoadPackageParam lparam, String versionName) {
if (loadConfig(lparam, versionName))
return;
log("failed to load config, start finding...");
generateConfig(lparam.appInfo.sourceDir, lparam.classLoader, versionName);
saveConfig(context);
}
public static void generateConfig(String wechatApk, ClassLoader classLoader, String versionName) {
HookParams hp = HookParams.getInstance();
hp.versionName = versionName;
hp.versionCode = HookParams.VERSION_CODE;
int versionNum = getVersionNum(versionName);
if (versionNum >= getVersionNum("6.5.6") && versionNum <= getVersionNum("6.5.23"))
hp.LuckyMoneyReceiveUIClassName = "com.tencent.mm.plugin.luckymoney.ui.En_fba4b94f";
if (versionNum < getVersionNum("6.5.8"))
hp.SQLiteDatabaseClassName = "com.tencent.mmdb.database.SQLiteDatabase";
if (versionNum < getVersionNum("6.5.4"))
hp.hasTimingIdentifier = false;
if (versionNum >= getVersionNum("7.0.0"))
hp.LuckyMoneyReceiveUIClassName = "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyNotHookReceiveUI";
if (versionNum >= getVersionNum("7.0.0"))
hp.ChatroomInfoUIClassName = "com.tencent.mm.chatroom.ui.ChatroomInfoUI";
ApkFile apkFile = null;
try {
apkFile = new ApkFile(wechatApk);
DexClass[] dexClasses = apkFile.getDexClasses();
wxClasses.clear();
for (int i = 0; i < dexClasses.length; i++) {
wxClasses.add(ReflectionUtil.getClassName(dexClasses[i]));
}
} catch (Error | Exception e) {
log("Open ApkFile Failed!");
} finally {
try {
apkFile.close();
} catch (Exception e) {
log("Close ApkFile Failed!");
}
}
//LuckMoney
try {
Class ReceiveUIParamNameClass = ReflectionUtil.findClassesFromPackage(classLoader, wxClasses, "com.tencent.mm", 1)
.filterByMethod(String.class, "getInfo")
.filterByMethod(int.class, "getType")
.filterByMethod(void.class, "reset")
.firstOrNull();
hp.ReceiveUIParamNameClassName = ReceiveUIParamNameClass.getName();
Class RequestCallerClass = ReflectionUtil.findClassesFromPackage(classLoader, wxClasses, "com.tencent.mm", 1)
.filterByField("foreground", "boolean")
.filterByMethod(void.class, int.class, String.class, int.class, boolean.class)
.filterByMethod(void.class, "cancel", int.class)
.filterByMethod(void.class, "reset")
.firstOrNull();
hp.RequestCallerClassName = RequestCallerClass.getName();
hp.RequestCallerMethod = ReflectionUtil.findMethodsByExactParameters(RequestCallerClass,
void.class, RequestCallerClass, int.class)
.getName();
Class NetworkRequestClass = ReflectionUtil.findClassesFromPackage(classLoader, wxClasses, "com.tencent.mm", 1)
.filterByMethod(void.class, "unhold")
.filterByMethod(RequestCallerClass)
.firstOrNull();
hp.NetworkRequestClassName = NetworkRequestClass.getName();
hp.GetNetworkByModelMethod = ReflectionUtil.findMethodsByExactParameters(NetworkRequestClass,
RequestCallerClass)
.getName();
Class ReceiveLuckyMoneyRequestClass = ReflectionUtil.findClassesFromPackage(classLoader, wxClasses, "com.tencent.mm.plugin.luckymoney", 1)
.filterByField("msgType", "int")
.filterByMethod(void.class, int.class, String.class, JSONObject.class)
.firstOrNull();
hp.ReceiveLuckyMoneyRequestClassName = ReceiveLuckyMoneyRequestClass.getName();
hp.ReceiveLuckyMoneyRequestMethod = ReflectionUtil.findMethodsByExactParameters(ReceiveLuckyMoneyRequestClass,
void.class, int.class, String.class, JSONObject.class)
.getName();
hp.LuckyMoneyRequestClassName = ReflectionUtil.findClassesFromPackage(classLoader, wxClasses, "com.tencent.mm.plugin.luckymoney", 1)
.filterByField("talker", "java.lang.String")
.filterByMethod(void.class, int.class, String.class, JSONObject.class)
.filterByMethod(int.class, "getType")
.filterByNoMethod(boolean.class)
.firstOrNull()
.getName();
hp.GetTransferRequestClassName = ReflectionUtil.findClassesFromPackage(classLoader, wxClasses, "com.tencent.mm.plugin.remittance", 1)
.filterByField("java.lang.String")
.filterByNoField("int")
.filterByMethod(void.class, int.class, String.class, JSONObject.class)
.filterByMethod(String.class, "getUri")
.firstOrNull()
.getName();
Class LuckyMoneyReceiveUIClass = ReflectionUtil.findClassIfExists(hp.LuckyMoneyReceiveUIClassName, classLoader);
hp.ReceiveUIMethod = ReflectionUtil.findMethodsByExactParameters(LuckyMoneyReceiveUIClass,
boolean.class, int.class, int.class, String.class, ReceiveUIParamNameClass)
.getName();
} catch (Error | Exception e) {
log("Search LuckMoney Classes Failed!");
throw e;
}
//ADBlock
try {
Class XMLParserClass = ReflectionUtil.findClassesFromPackage(classLoader, wxClasses, "com.tencent.mm.sdk.platformtools", 0)
.filterByMethod(Map.class, String.class, String.class)
.firstOrNull();
hp.XMLParserClassName = XMLParserClass.getName();
hp.XMLParserMethod = ReflectionUtil.findMethodsByExactParameters(XMLParserClass, Map.class, String.class, String.class)
.getName();
} catch (Error | Exception e) {
log("Search LuckMoney Classes Failed!");
}
//AntiRevoke
try {
ReflectionUtil.Classes storageClasses = ReflectionUtil.findClassesFromPackage(classLoader, wxClasses, "com.tencent.mm.storage", 0);
Class MsgInfoClass = storageClasses
.filterByMethod(boolean.class, "isSystem")
.firstOrNull();
hp.MsgInfoClassName = MsgInfoClass.getName();
if (versionNum < getVersionNum("6.5.8")) {
Class MsgInfoStorageClass = storageClasses
.filterByMethod(long.class, MsgInfoClass)
.firstOrNull();
hp.MsgInfoStorageClassName = MsgInfoStorageClass.getName();
hp.MsgInfoStorageInsertMethod = ReflectionUtil.findMethodsByExactParameters(MsgInfoStorageClass, long.class, MsgInfoClass)
.getName();
} else {
Class MsgInfoStorageClass = storageClasses
.filterByMethod(long.class, MsgInfoClass, boolean.class)
.firstOrNull();
hp.MsgInfoStorageClassName = MsgInfoStorageClass.getName();
hp.MsgInfoStorageInsertMethod = ReflectionUtil.findMethodsByExactParameters(MsgInfoStorageClass, long.class, MsgInfoClass, boolean.class)
.getName();
}
} catch (Error | Exception e) {
log("Search AntiRevoke Classes Failed!");
}
//Photo Limits
try {
Class SelectConversationUIClass = XposedHelpers.findClass(hp.SelectConversationUIClassName, classLoader);
hp.SelectConversationUICheckLimitMethod = ReflectionUtil.findMethodsByExactParameters(SelectConversationUIClass,
boolean.class, boolean.class)
.getName();
hp.ContactInfoClassName = ReflectionUtil.findClassesFromPackage(classLoader, wxClasses, "com.tencent.mm.storage", 0)
.filterByMethod(String.class, "getCityCode")
.filterByMethod(String.class, "getCountryCode")
.firstOrNull()
.getName();
} catch (Error | Exception e) {
log("Search Photo Limits Classes Failed!");
}
}
private static int getVersionNum(String version) {
String[] v = version.split("\\.");
if (v.length == 3)
return Integer.valueOf(v[0]) * 100 * 100 + Integer.valueOf(v[1]) * 100 + Integer.valueOf(v[2]);
else
return 0;
}
private static boolean loadConfig(XC_LoadPackage.LoadPackageParam lpparam, String curVersionName) {
try {
SharedPreferences pref = getPreferencesInstance();
HookParams hp = new Gson().fromJson(pref.getString("params", ""), HookParams.class);
if (hp == null
|| !hp.versionName.equals(curVersionName)
|| hp.versionCode != HookParams.VERSION_CODE) {
return false;
}
HookParams.setInstance(hp);
log("load config successful");
return true;
} catch (Error | Exception e) {
log("load config failed!");
}
return false;
}
private static void saveConfig(Context context) {
try {
Intent saveConfigIntent = new Intent();
saveConfigIntent.setAction(HookParams.SAVE_WECHAT_ENHANCEMENT_CONFIG);
saveConfigIntent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
saveConfigIntent.putExtra("params", new Gson().toJson(HookParams.getInstance()));
context.sendBroadcast(saveConfigIntent);
log("saving config...");
} catch (Error | Exception e) {
log("saving config failed!");
}
}
private static XSharedPreferences getPreferencesInstance() {
if (preferencesInstance == null) {
preferencesInstance = new XSharedPreferences(Main.class.getPackage().getName(), HookParams.WECHAT_ENHANCEMENT_CONFIG_NAME);
preferencesInstance.makeWorldReadable();
} else {
preferencesInstance.reload();
}
return preferencesInstance;
}
}
看一下init方法:
public static void init(Context context, XC_LoadPackage.LoadPackageParam lparam, String versionName) {
if (loadConfig(lparam, versionName))
return;
log("failed to load config, start finding...");
generateConfig(lparam.appInfo.sourceDir, lparam.classLoader, versionName);
saveConfig(context);
}
先不看loadConfig函数,因为预感核心代码并不在这里。看generateConfig函数。
HookParams hp = HookParams.getInstance();
hp.versionName = versionName;
hp.versionCode = HookParams.VERSION_CODE;
int versionNum = getVersionNum(versionName);
if (versionNum >= getVersionNum("6.5.6") && versionNum <= getVersionNum("6.5.23"))
hp.LuckyMoneyReceiveUIClassName = "com.tencent.mm.plugin.luckymoney.ui.En_fba4b94f";
if (versionNum < getVersionNum("6.5.8"))
hp.SQLiteDatabaseClassName = "com.tencent.mmdb.database.SQLiteDatabase";
if (versionNum < getVersionNum("6.5.4"))
hp.hasTimingIdentifier = false;
if (versionNum >= getVersionNum("7.0.0"))
hp.LuckyMoneyReceiveUIClassName = "com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyNotHookReceiveUI";
if (versionNum >= getVersionNum("7.0.0"))
hp.ChatroomInfoUIClassName = "com.tencent.mm.chatroom.ui.ChatroomInfoUI";
首先,第一行调用了HookParams.getInstance();。那么,HookParams类中的私有instance属性将被初始化。接下来,hp的versionName属性将被设置为当前微信的版本号。hp的versionCode属性将被设置为HookParams类的VERSION_CODE常量。注意,这个VERSION_CODE常量代表着当前插件的版本号,不是微信的版本号。
接着,调用了getVersionNum函数:int versionNum = getVersionNum(versionName);
这个函数的作用是将微信的版本号字符串转换成整数以方便比较版本号的大小。
那么接下来几行代码的逻辑就很清楚了:将当前版本号与某个版本号相比较,然后对hp的属性进行设置。那么我们终于知道了HookParams正是用来存放与hook有关的参数的单例模式类。
剩下的代码逻辑大致是这样的:扫描微信apk得到其中的所有的类名,然后通过一定的规则过滤类名得到要hook的方法名,设置到hp的属性中。
注意:上传附件及图片大小不得大于30M。
⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途。
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。
