- 申请ID:走猫步的鱼
- 个人邮箱:1253427499@qq.com
- 原创技术文章:Xposed模块-微信机器人(自动回复消息)
开发环境
- Android Studio 3.5
- 微信 7.0.6
准备工作
在 Android Studio 中新建项目这个直接略过。
-
添加 xposed 依赖。
在 app 下的 build.gradle 中添加以下依赖
compileOnly 'de.robv.android.xposed:api:82'
compileOnly 'de.robv.android.xposed:api:82:sources'
-
配置清单文件。
在 AndroidManifest.xml 中的 <application> 中添加以下内容
<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="微信自动回复模块。" />
<meta-data
android:name="xposedminversion"
android:value="54" />
-
编写主Hook类。
package com.wanzi.wechatrobot
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.callbacks.XC_LoadPackage
class MainHook :IXposedHookLoadPackage{
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam?) {
}
}
-
添加模块入口。
在 main 包下新建文件夹 assets ,并新建文件 xposed_init ,写入以下内容
com.wanzi.wechatrobot.MainHook
> 这里的内容是主Hook类的地址。
开始
这里主要分为两步,第一步是拦截微信数据库消息,第二步是调用微信方法发送消息。
拦截微信数据库
微信使用的数据库是他们自家的开源数据库 WCDB,所以我们只需要去看一下他们的api,找出 插入数据 的方法,然后通过 hook 这个方法,就可以获取到我们需要的数据。
通过查看 api 和了解一些 SQL 常识,我们可以大概判断插入数据是这个方法[insert](https://tencent.github.io/wcdb/references/android/reference/com/tencent/wcdb/database/SQLiteDatabase.html#insert(java.lang.String,%20java.lang.String,%20android.content.ContentValues),下面我们就先 hook 下这个方法看看。
class MainHook : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam?) {
// 只关心微信包名
if (lpparam?.packageName != "com.tencent.mm") {
return
}
XposedHelpers.findAndHookMethod(
"com.tencent.wcdb.database.SQLiteDatabase",
lpparam.classLoader,
"insert",
String::class.java, // table
String::class.java, // nullColumnHack
ContentValues::class.java,
object : XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam?) {
val table = param?.args?.get(0) as String
val values = param.args?.get(2) as ContentValues
// https://github.com/WANZIzZ/WeChatRecord/blob/master/app/src/main/java/com/wanzi/wechatrecord/CoreService.kt
// 这个是我以前写过的一个破解微信数据库的代码,从这个里面我们可以知道 message 表就是聊天记录表,现在我们只关心这个表
if (table != "message") {
return
}
Log.i("Wanzi", "拦截微信数据库 values:${values}")
}
}
)
}
}
好了,写完以后,运行一下项目,然后在 Xposed 模块中启用我们的模块。
最后让其他人给我们的微信发一条消息,我们可以在 Android Sutdio 的 Logcat 中发现我们成功的拦截到了接收到的消息,
我们需要的数据就在 values 中。
好了,拦截微信数据库已经成功,下面就是我们就要调用微信方法发送消息。
调用微信方法发送消息
要调用微信方法发送消息,首先就得要知道微信是调用哪些方法来发送的,这里我们用 Android SDK 自带的 Android Device Monitor 来调试分析。
开始调试
点 OK ,然后在微信聊天页面随便发送一条消息。
发送消息以后,再点圈中的按钮。
这样就会生成分析文件。
既然是点击事件,肯定和 click 有关,所以我们可以先搜索 click
然后一步步往下找
果然没错,走到了微信的点击事件,接着再往下走
我们发现,一共调用了4个方法,这4个方法中,只有 TQ
最让我们起疑,为什么呢?大家看一下, TQ
的参数是字符串,这个字符串会不会就是消息内容呢?返回的是 Boolean ,会不会就是消息是否发送成功呢?我们来 Hook 一下试试。
XposedHelpers.findAndHookMethod(
"com.tencent.mm.ui.chatting.p",
lpparam.classLoader,
"TQ",
String::class.java,
object : XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam?) {
val str = param?.args?.get(0) as String
Log.i("Wanzi", "拦截TQ str:$str 结果:${param.result}")
}
}
)
运行一下代码,然后我们发送一条微信消息,看下 Logcat 日志:
哈哈,果然就是它了,我们再往下找。
可以看到,调用了2个方法,这两个方法里面,只有 avz
看起来最可疑,我们继续往下追踪
这里看到,只调用了1个方法,参数是一个字符串,一个整数,返回值还是一个 Boolean,这个字符串应该还是消息内容,我们想一想,消息内容有了,是不是还缺少一个消息接收者?下面我们通过分析下微信源码来找找消息接受者在哪里。
分析微信源码
通过反编译微信,我们可以得到微信源码。
我这里使用的是 jadx 来反编译的。
分析 chatting.c.ai.eS
private boolean eS(String str, final int i) {
int i2 = 0;
AppMethodBeat.i(31684);
final String arL = bo.arL(str);
if (arL == null || arL.length() == 0) {
ab.e("MicroMsg.ChattingUI.SendTextComponent", "doSendMessage null");
AppMethodBeat.o(31684);
return false;
}
this.Aro.avn(arL);
bz bzVar = new bz();
bzVar.cIT.cIV = arL;
bzVar.cIT.context = this.ctY.AsT.getContext();
bzVar.cIT.username = this.ctY.getTalkerUserName();
com.tencent.mm.sdk.b.a.yVI.l(bzVar);
if (bzVar.cIU.cIW) {
AppMethodBeat.o(31684);
return true;
}
boolean z = WXHardCoderJNI.hcSendMsgEnable;
int i3 = WXHardCoderJNI.hcSendMsgDelay;
int i4 = WXHardCoderJNI.hcSendMsgCPU;
int i5 = WXHardCoderJNI.hcSendMsgIO;
if (WXHardCoderJNI.hcSendMsgThr) {
i2 = g.We().dAB();
}
this.Arp = WXHardCoderJNI.startPerformance(z, i3, i4, i5, i2, WXHardCoderJNI.hcSendMsgTimeout, 202, WXHardCoderJNI.hcSendMsgAction, "MicroMsg.ChattingUI.SendTextComponent");
com.tencent.mm.ui.chatting.d.a.dRn().post(new Runnable() {
public final void run() {
String str;
AppMethodBeat.i(31681);
if (ai.this.ctY == null) {
ab.w("MicroMsg.ChattingUI.SendTextComponent", "NULL == mChattingContext");
AppMethodBeat.o(31681);
return;
}
com.tencent.mm.plugin.report.service.g.DG(20);
if (ai.a(ai.this)) {
ai.this.ctY.dRi();
aw.Vs().a((m) new com.tencent.mm.ar.a(ai.this.ctY.uhw.field_username, arL), 0);
AppMethodBeat.o(31681);
return;
}
if (((h) ai.this.ctY.aU(h.class)).getCount() == 0 && com.tencent.mm.storage.ad.asF(ai.this.ctY.getTalkerUserName())) {
bv.afx().c(10076, Integer.valueOf(1));
}
String talkerUserName = ai.this.ctY.getTalkerUserName();
int px = t.px(talkerUserName);
String str2 = arL;
String str3 = null;
try {
str3 = ((com.tencent.mm.ui.chatting.c.b.t) ai.this.ctY.aU(com.tencent.mm.ui.chatting.c.b.t.class)).avx(talkerUserName);
} catch (NullPointerException e2) {
ab.printErrStackTrace("MicroMsg.ChattingUI.SendTextComponent", e2, "", new Object[0]);
}
if (bo.isNullOrNil(str3)) {
ab.w("MicroMsg.ChattingUI.SendTextComponent", "tempUser is null");
AppMethodBeat.o(31681);
return;
}
o oVar = (o) ai.this.ctY.aU(o.class);
int lastIndexOf = str2.lastIndexOf(8197);
if (lastIndexOf <= 0 || lastIndexOf != str2.length() - 1) {
str = str2;
} else {
str = str2.substring(0, lastIndexOf);
ab.w("MicroMsg.ChattingUI.SendTextComponent", "delete @ last char! index:".concat(String.valueOf(lastIndexOf)));
}
ChatFooter dPL = oVar.dPL();
int i = i;
int i2 = dPL.wFF.wHB.containsKey(talkerUserName) ? ((LinkedList) dPL.wFF.wHB.get(talkerUserName)).size() > 0 ? 291 : i : i;
com.tencent.mm.modelmulti.h hVar = new com.tencent.mm.modelmulti.h(str3, str, px, i2, oVar.dPL().ii(talkerUserName, str2));
((com.tencent.mm.ui.chatting.c.b.t) ai.this.ctY.aU(com.tencent.mm.ui.chatting.c.b.t.class)).g(hVar);
aw.Vs().a((m) hVar, 0);
if (t.pt(talkerUserName)) {
aw.Vs().a((m) new j(q.PZ(), arL + " key " + bs.dGp() + " local key " + bs.dGo() + "NetType:" + at.getNetTypeString(ai.this.ctY.AsT.getContext().getApplicationContext()) + " hasNeon: " + n.PF() + " isArmv6: " + n.PH() + " isArmv7: " + n.PG()), 0);
}
AppMethodBeat.o(31681);
}
});
this.ctY.ru(true);
AppMethodBeat.o(31684);
return true;
}
这里我们发现,传入的 str 被处理了一下,变成了 arL
,接下来我们看下调用 arL
的地方,第一个是在:
我们点进去看看
传入的 str 在这里被使用了,我们追踪进 setContent
看看
这个类是 com.tencent.mm.g.c.dd
,我们发现这里不仅有 field_content
,还有 field_talker
,刚才我们在调试的时候,只找到了消息内容,还缺少一个消息接收者,那这个 kX
方法传入的是不是就是消息接收者呢?我们来 hook 下这个方法试试。
XposedHelpers.findAndHookMethod(
"com.tencent.mm.g.c.dd",
lpparam.classLoader,
"kX",
String::class.java,
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam?) {
val str = param?.args?.get(0) as String
Log.i("Wanzi", "拦截kX str:$str")
}
}
)
继续运行项目,然后用我们的微信发送一条消息,接着看下 Logcat
果然,这个 kX
传入的就是消息接受者。
这下消息内容有了,消息接受者也有了,那剩下的就是在哪里一起使用他们,我们来打印下 kX
调用堆栈信息看看。
XposedHelpers.findAndHookMethod(
"com.tencent.mm.g.c.dd",
lpparam.classLoader,
"kX",
String::class.java,
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam?) {
val str = param?.args?.get(0) as String
Log.i("Wanzi", "拦截kX str:$str")
Log.i("Wanzi", "打印堆栈\n${Log.getStackTraceString(Throwable())}")
}
}
)
用微信发送一条消息后,接着看下 Logcat
看一下,这里最可疑的应该就是这个 com.tencent.mm.modelmulti.h.<init>
了,我们来看下这个类的构造函数
果然调用了 kX
、setContent
,应该就是它了。我们发现这个类一共有4个构造函数:
一共是4个:
- 第一个无参(pass)
- 第二个传入的是
local id
(pass)
- 第三个传入两个字符串一个整数,通过分析,我们得知第一个字符串是消息接收者,第二个字符串是消息内容,第三个整数是消息类型(这个可以参考我之前写过的WeChatRecord)
- 第四个和第三个差别不大
我们现在有了消息类,是不是还差怎么把消息发出去?接着回到 chatting.c.ai.eS
来,刚才我们就是从这里开始分析源码的。
现在我们已经知道了要发送消息,肯定会用到 com.tencent.mm.modelmulti.h
,那我们就看下,eS
方法里面哪块调用了 com.tencent.mm.modelmulti.h
最后调用 hVar
的是这里,我们大胆的猜想一下,是不是就是通过这里来发送微信消息的?来,先看下源码
接着我们照着微信的调用步骤,代码走起
XposedHelpers.findAndHookMethod(
"com.tencent.wcdb.database.SQLiteDatabase",
lpparam.classLoader,
"insert",
String::class.java, // table
String::class.java, // nullColumnHack
ContentValues::class.java,
object : XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam?) {
val table = param?.args?.get(0) as String
val values = param.args?.get(2) as ContentValues
// https://github.com/WANZIzZ/WeChatRecord/blob/master/app/src/main/java/com/wanzi/wechatrecord/CoreService.kt
// 这个是我以前写过的一个破解微信数据库的代码,从这个里面我们可以知道 message 表就是聊天记录表,现在我们只关心这个表
if (table != "message") {
return
}
Log.i("Wanzi", "拦截微信数据库 values:${values}")
val talker = values.getAsString("talker")
val content = values.getAsString("content")
val clz_h = XposedHelpers.findClass("com.tencent.mm.modelmulti.h", lpparam.classLoader)
val message = XposedHelpers.newInstance(clz_h, talker, content, 1)
val clz_aw = XposedHelpers.findClass("com.tencent.mm.model.aw", lpparam.classLoader)
val clz_p = XposedHelpers.callStaticMethod(clz_aw, "Vs")
XposedHelpers.callMethod(clz_p,"a",message,0)
}
}
)
这里我们选择在接收到消息的时候,把消息内容再发回去,再次运行代码,然后让别人给我们发一条消息试试
哈哈哈,成功啦!
GitHub地址