首先将取一个android手机QQ应用的一个版本取apk到电脑,使用jadx-gui或者任意的反编译软件进行反编译,拿到反编译后的代码这是准备阶段。
首先需要取到QQ内使用的Context上下文信息,这个步奏通常是反编译一个应用进行二次开发的第一步奏,我定位到了如下包中的一个类:“xxx.xxx.xxx.xxx.InjectUtils”能够分析定位到一个名字为injectExtraDexes的方法,在这个方法中能够Hook到application信息,QQ源码如下:
public static synchronized String injectExtraDexes(Application application, boolean z) {
String injectExtraDexesOat;
synchronized (InjectUtils.class) {
if (QLog.isColorLevel()) {
QLog.i(TAG, 2, "Runtime : " + (isRuntimeART() ? "ART" : "Dalvik") + " ; sdk :" + VERSION.SDK_INT);
}
try {
if (VERSION.SDK_INT >= 21) {
injectExtraDexesOat = injectExtraDexesOat(application, z);
} else {
injectExtraDexesOat = injectExtraDexManual(application, z);
}
} catch (Throwable th) {
QLog.e(TAG, 1, "", th);
injectExtraDexesOat = null;
}
}
return injectExtraDexesOat;
}
当我们能够取到application的时候自然能够获取到QQ应用的包信息以及版本信息,就可以在代码中进行版本的适配,同时可以获取到父类的ClassLoader方便后续的Hook操作,本博客针对的是QQ 6.7.0v的一个能在网上找到的较为老的一个版本,可以更为方便的做代码分析样例来使用。新版本的QQ不清楚是否在技术层面支持了Xposed框架的检测以及各项功能接口代码的保护,还有可能对非正常人为的报错异常信息进行上传而封号的处理。
之后通过定位查找关键字,查找对应消息页面的反编译代码定位到一个叫做MessageHandlerUtils的工具类,相关代码如下:
public static boolean a(QQAppInterface qQAppInterface, MessageRecord messageRecord, boolean z) {
if (messageRecord == null || (messageRecord.msg == null && messageRecord.msgData == null)) {
if (QLog.isColorLevel()) {
QLog.w("Q.msg.MessageHandlerUtils", 2, "---------------msgFilter message [before filter] is null !");
}
return true;
}
StringBuilder stringBuilder;
long currentTimeMillis = System.currentTimeMillis();
if (QLog.isColorLevel()) {
StringBuilder stringBuilder2 = new StringBuilder(256);
stringBuilder2.append("---------------msgFilter istroop: ").append(messageRecord.istroop).append(" shmsgseq: ").append(messageRecord.shmsgseq).append(" friendUin: ").append(messageRecord.frienduin).append(" senderUin: ").append(messageRecord.senderuin).append(" msgType: ").append(messageRecord.msgtype).append(" time:").append(messageRecord.time).append(" msgContent: ").append(messageRecord.getLogColorContent()).append(" isNormalMsg: ").append(z);
stringBuilder = stringBuilder2;
} else {
stringBuilder = null;
}
List<MessageRecord> b = qQAppInterface.a().b(messageRecord.frienduin, messageRecord.istroop);
if (messageRecord.istroop == 1 || messageRecord.istroop == 1026 || messageRecord.istroop == 3000) {
if (b != null && b.size() > 0) {
for (MessageRecord a : b) {
if (MsgProxyUtils.a(a, messageRecord, false, z)) {
if (QLog.isColorLevel() && stringBuilder != null) {
stringBuilder.append(" filterType: troop msg isNormalMsg=" + z);
QLog.w("Q.msg.MessageHandlerUtils", 2, stringBuilder.toString());
}
MsgAutoMonitorUtil.a().h(System.currentTimeMillis() - currentTimeMillis);
return true;
}
}
}
if (qQAppInterface.a().f(messageRecord)) {
return true;
}
} else if (MsgProxyUtils.c(messageRecord.istroop)) {
if (b != null && b.size() > 0) {
for (MessageRecord a2 : b) {
if (MsgProxyUtils.a(a2, messageRecord, z)) {
if (QLog.isColorLevel() && stringBuilder != null) {
stringBuilder.append(" filterType: " + messageRecord.istroop);
QLog.w("Q.msg.MessageHandlerUtils", 2, stringBuilder.toString());
}
MsgAutoMonitorUtil.a().h(System.currentTimeMillis() - currentTimeMillis);
return true;
}
}
}
if (qQAppInterface.a().f(messageRecord)) {
return true;
}
} else if (messageRecord.istroop == 7220) {
if (b != null && b.size() > 0) {
for (MessageRecord a22 : b) {
if (MsgProxyUtils.a(a22, messageRecord, true)) {
if (QLog.isColorLevel() && stringBuilder != null) {
stringBuilder.append(" filterType: other");
QLog.w("Q.msg.MessageHandlerUtils", 2, stringBuilder.toString());
}
MsgAutoMonitorUtil.a().h(System.currentTimeMillis() - currentTimeMillis);
return true;
}
}
}
} else if (b != null && b.size() > 0) {
for (MessageRecord a222 : b) {
if (a222.time == messageRecord.time && a222.msg.equals(messageRecord.msg)) {
if (QLog.isColorLevel() && stringBuilder != null) {
stringBuilder.append(" filterType: other");
QLog.w("Q.msg.MessageHandlerUtils", 2, stringBuilder.toString());
}
MsgAutoMonitorUtil.a().h(System.currentTimeMillis() - currentTimeMillis);
return true;
}
}
}
if (QLog.isColorLevel() && stringBuilder != null) {
QLog.d("Q.msg.MessageHandlerUtils", 2, stringBuilder.toString());
}
MsgAutoMonitorUtil.a().h(System.currentTimeMillis() - currentTimeMillis);
return false;
}
在这个方法中就可以很轻易的拿到传到这个方法的参数信息,然后再根据getObjectField的方法拿到该对象的实例,我不知道该不该这样理解或是这样子表达,然后再上诉的QQ反编译的代码中,能够看到这个方法参数的实例MessageRecord.istroop/.friendui/.msg等等的信息,这样你单独在这里先停止下面的代码分析和处理,在这里单独的对上述得到的信息打Log,在控制台看看你到底得到的信息是什么,测试一下在往下分析也来得及。
经过log验证,你会发现你在QQ中接收到的消息就是 msg 其它的分别是qq号,群判断等...这些东西都能够在QQ的数据库中存储,你可以将QQ的数据库拿出来没用可视化工具查看QQ的数据库结构信息,分析消息部分每个字段代表的具体含义,但是我不清楚QQ数据库有没有KEY,微信数据库是有KEY的,但是能够通过Hook的方式拿到KEY,我想即便有密码也大同小异,我记得微信数据库密码是两个字段分别进行MD5和某一种加密方式的拼接在取其前几位的字段,这步掠过也可以的,因为这些东西是进行设置QQ机器人的各项回复依据,例如群消息不回复等...
然后你能够拿到好友对你发送的消息了,能够拿到这条消息的对应各项参数信息了,那你就能能够进行验证分析是否跟你本地配置的文件是否匹配,这决定了是否进行回复和回复那些消息内容甚至可以延时多少时间进行回复等操作...当然Hook的代码是依附于QQ的源码的,类似于一个病毒细菌一样,嵌入在了QQ的体内,所以的配置的东西在QQ内是无法获取的,不知道你们能不能明白,举个例子,你写的一个APP是Demo1,那么你写的本地界面设置的参数信息保存是保存在你Demo1的进程中,保存在了你Demo1对应的内容提供者的路径文件夹下,那么你无权去访问QQ的数据库SP它的进程。我不知道这样的比喻是否恰当但是却是常识性的东西,那么该怎么办?开始初学的时候我确实煞费苦心,但是其实Xposed框架早就准备了这样的一个共享的SP文件用来存储小量信息,我简称为XSP(XSharedPreferences),这篇博客主要讲解干货原理分析,所以设置本地的配置就不写了,先写死的东西,比如我用我的大号进行测试,然后小号给大号发送消息会自动回复,回复特别内容会回复特别的内容。
然后下面会遇到一个问题是如何查找到send发送消息的接口?我的办法是哪一步实行的发送动作,将消息发送出去了,就从哪里入手查找发送的功能代码,最后定位到了ChatActiviryFacade类,查找到了一个类似于发送消息的方法:
public static long[] a(QQAppInterface qQAppInterface, Context context, SessionInfo sessionInfo, String str, ArrayList arrayList, SendMsgParams sendMsgParams) {
if (QLog.isColorLevel()) {
QLog.d("SendMsgBtn", 2, " sendMessage start currenttime:" + System.currentTimeMillis());
}
if (str == null || str.length() == 0 || sendMsgParams == null || sessionInfo == null || TextUtils.isEmpty(sessionInfo.f1386a)) {
return null;
}
long[] a = a(qQAppInterface, sessionInfo, str, arrayList, sendMsgParams);
ThreadManager.a(new ljr(sendMsgParams, qQAppInterface, sessionInfo), 8, null, true);
if (!QLog.isColorLevel()) {
return a;
}
QLog.d("SendMsgBtn", 2, " sendMessage end currenttime:" + System.currentTimeMillis());
return a;
}
整个类反编译后有4000多行的代码,你如何分析查找到关键处,是尤为重要的,我也只是一枚渣渣只能够凭借感觉和关键字还有一些运气成分去碰几个好像还挺像那么回事的代码片儿,然后用Hook去取这个方法的参数或是返回值去验证是否判断正确,有时候你hook不到信息,很有可能是你没有拿到一个正确的Context上下文对象,而如何能找到对应的Context也显得尤为重要。
那么你找到了关键的发送方法了,那么之后呢?当然是寻找对应参数的实例,然后使用callStaticMethod方法调用此方法,就能够将消息发送出去啦~到这里可以松一口气了,因为之前的代码大多是试探,或是根据自己的感觉,在或者根据页面的对应相关逻辑分析查找关键字,来一步一步的分析定位,或者发现这是一条死胡同,就赶紧撤回继续去找,很费时间也有一些看运气的成分,毕竟这是大厂的APP由几十人共同研发而成的代码,想一想把他攻克下来也是很有自豪感和成就感的,那好像就能松口气继续研究下去了。
回归正传查找定位这几个参数的实例的过程这里就省略了,直接上最后的Xposed代码,首先新建工程将Xposed的jar包以及准备工作完成,我再之前的博客里总结的基础的准备阶段,这里不明白的可以找我之前的博客,网上查也会查到一堆这里也不过多的详述了:
findAndHookMethod(InjectUtils, loadPackageParam.classLoader, "injectExtraDexes",
Application.class, boolean.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
final Application application = (Application) param.args[0];
PackageInfo packageInfo = application.getPackageManager().getPackageInfo(application.getPackageName(), 0);
String QqVersion = packageInfo.versionName;
Log.e("HookQQ", QqVersion);
Common.initVersion(QqVersion);
MessageUtil messageUtil = new MessageUtil(application.getClassLoader(), application);
messageUtil.initAutoReply();
}
});
这部分是hook的主入口,之前会进行各程序进程包的剔除,寻找关键部分,然后进入到MessageUtils消息处理工具类中:
public void initAutoReply() {
sApplication = callStaticMethod(findClass(BaseApplicationImpl, classLoader), "getApplication");
findAndHookMethod(MessageHandlerUtils, classLoader, "a",
QQAppInterface,
MessageRecord,
boolean.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
// if (!XPreferenceUtil.getMasterSwitch()) return;
final String frienduin = getObjectField(param.args[1], "frienduin").toString();
final String selfuin = getObjectField(param.args[1], "selfuin").toString();
final String senderuin = getObjectField(param.args[1], "senderuin").toString();
final int istroop = (int) getObjectField(param.args[1], "istroop");
final String msg = getObjectField(param.args[1], "msg").toString();
boolean isread = (boolean) getObjectField(param.args[1], "isread");
Log.e("HookQQ","frienduin---" + frienduin);
Log.e("HookQQ","selfuin---" + selfuin);
Log.e("HookQQ","senderuin---" + senderuin);
// String[] whiteList = XPreferenceUtil.getWhiteList();
// 白名单 好友列表的数组 先写死
// 查询内容为 qq号 !
String[] whiteList = {"xxx","xxx","xxx"};
if (whiteList.length != 0 && !senderuin.equals(selfuin)) {
// if (XPreferenceUtil.getNoReplyTroop() && istroop == 1) return;
// 上面是群消息不接收与回复
if(istroop == 1)return;
iteratorWhiteList(frienduin, selfuin, msg, senderuin, istroop, isread, whiteList);
}
}
});
}
执行发送白名单的方法为iteratorWhiteList执行对字符串拆分然后遍历取相关信息与设置的白名单信息做比较,匹配后进入send方法:
private void iteratorWhiteList(String frienduin, String selfuin, String msg, String senderuin, int istroop, boolean isread, String[] list) {
for (String s : list) {
if (frienduin.equals(s) && !isread && !frienduin.equals(selfuin)) {
Log.e("HookQQ", "iteratorWhiteList is running...");
ArrayList<Map<String, String>> keyReply = getKeyReply();
if (keyReply != null && keyReply.size() != 0) {
for (Map<String, String> stringMap: keyReply){
if (stringMap.get("content").trim().equals(msg)) {
send(frienduin, selfuin, istroop, stringMap.get("reply_content"));
return;
}
}
}
send(frienduin, selfuin, istroop, "统一回复!!!");
}
}
}
发送send方法如下:
private void send(String frienduin, String selfuin, int istroop, String s) {
Object qqAppInterface = callMethod(sApplication, "getAppRuntime", selfuin);
Object sessionInfo = newInstance(findClass(SessionInfo, classLoader));
Object sendMsgParams = newInstance(findClass(SendMsgParams, classLoader));
// 嗯 很暴躁!
XposedUtil.setField(sessionInfo, "a", frienduin, String.class);
XposedUtil.setField(sessionInfo, "a", istroop, int.class);
callStaticMethod(XposedHelpers.findClass(ChatActivityFacade, classLoader), "a",
qqAppInterface, mContext, sessionInfo, s, new ArrayList<>(), sendMsgParams);
}
以上的代码只是部分,只是以分析和逻辑处理代码的分享,Hook的QQ具体包名类名,更多的都没有给出,然后接下来是测试验证好使的截图(回复谁的消息,关键字匹配回复等都是写死的,之前已经说过了,这里可以写的更为灵活,可以用socket或是Http去请求后台,在后台进行更为详细的判断之后再返回给前端具体的消息,后台也可以根据需求绑定图灵机器人的相关API接口,在回调给前台这样...反正随便你设计...最好的最灵活的方式肯定是这样了):
点个赞评论下呗~么么哒~