1. 起因
由于本人水平有限,无法分析客户端的协议。而qq的web版设计过于反人类,cookie过期速度非常快,每次登录都要求扫描二维码,所以想了这么一种方法。
2. 工作原理
首先,在安卓手机上安装xposed框架,通过编写xposed插件来hook手机QQ发消息的方法。然后和自己的服务器建立连接,当接收到服务器发消息的指令后,由注入qq客户端的xposed插件完成最终发送消息。
如此一来,就可以通过服务器提供一个http接口来控制qq发送消息,还不用去研究qq的通讯协议。
3. 实际操作
3.1 准备工作
一部可以联网的安卓手机,一台服务器。
QQ的版本:轻聊版 v3.7.1.704 (2018.11的最新版)
Xposed Installer的版本: 2.4
3.2 手机端
3.2.1 寻找QQ发消息的方法
显然,直接反编译难度比较大。我的思路是:
编写一个 xposed插件,hook 安卓View 的setOnClickListener 方法,传入一个自己编写的OnClickListener,在视图被点击时记录用Log记录其ID,然后在源码中搜索该ID,就可以找出按钮的点击事件。而按钮的点击事件就是发消息的方法。
public class HookOnClickListener implements View.OnClickListener {
private View.OnClickListener original;
public HookOnClickListener(View.OnClickListener original) {
this.original = original;
}
@Override
public void onClick(View v) {
int id=v.getId();
Log.d("hookqq","View Id:"+id);
original.onClick(v);
}
}
以上是假OnClickListener的代码,类似于中间人攻击
通过上面的代码,获得聊天界面“发送”按钮的ID是0x7F09019C。在QQ反编译的代码中搜索可得“发送”按钮的onClick方法在com.tencent.mobileqq.activity.BaseChatPie里面,最后调用的是void b()方法,我们来看看b()方法的逻辑:
可以看出,b()方法在校验消息合法性后调用了com.tencent.mobileqq.activity.ChatActivityFacade的a方法。这是一个静态方法,一共五个参数:AppQQAppInterface,Context,SessionInfo,String,ArrayList.
从命名可以看出发消息时第1,2个参数应该不会变化,第4个参数时消息框中的内容,第五个参数为null.
所以只需要生成SessionInfo就可以通过Xposed插件发送消息。
SessionInfo的结构比较简单:
使用xposed插件记录每次发消息时这些变量的值可以发现:
String a;//对方的QQ号
String d;//对方昵称
String b,c,d,e,f;//一直为null
long a;//当前时间戳,有时是-1
int a;//一直是0
int b;//一直是32
int c;//一直是1
int d;//10004,可能是消息类型
3.2.2 开始写插件
根据上面的分析,我们只需要hook ChatActivityFacade.a(...)方法,在它第一次调用时记录AppQQAppInterface,Context的值,然后再用过反射创建SessionInfo对象就可以通过插件发送消息。
另外,由于QQ编译时经过混淆,SessionInfo中出现了同名变量的情况。这种情况虽然在Java中是无法通过编译的,但是由于Dalvik字节码通过变量类型+变量名称来区分成员变量,所以不影响运行。
为了设置SessionInfo中成员变量的值,XposedHelper提供的反射工具已经无法满足我们的需求,所以需要自己编写一个方法,通过变量的类型和命名来获取Field对象。
如下所示:
public static Field getFieldByNameAndType(Class<?> target,String fieldName,Class<?> fieldType){
Field[] fs=target.getDeclaredFields();
for(Field f:fs){
f.setAccessible(true);
if(f.getType()==fieldType & f.getName().equals(fieldName)){
return f;
}
}
return null;
}
为了方便使用,我们注册一个BroadcastReceiver,当收到广播后就调用原来的方法发消息。
然后创建一个线程,每个几秒查询服务器的消息,当有消息需要发送时再发送一个广播即可。
3.3 服务器端
这个比较简单,我是用python flask写的。只需要一个上传消息和查询消息的接口即可。直接贴代码:
from flask import Flask,request,abort
from flask_sqlalchemy import SQLAlchemy
import json
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"]="mysql+pymysql://<mysql用户名>:<mysql密码>@<mysql主机地址>/数据库名"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"]=False
db=SQLAlchemy(app)
#为了防止其他人查询到消息,设一个访问密码
access_key="查询消息的密码"
class msg(db.Model):
id=db.Column(db.Integer,primary_key=True)
receiver=db.Column(db.String(11))
content=db.Column(db.String(4096))
def __init__(self,receiver,content):
self.receiver=receiver
self.content=content
def __repr__(self):
return "<msg %r>"%self.id
def get_json(self):
data={"id":self.id,"receiver":self.receiver,"content":self.content}
return json.dumps(data)
@app.route('/')
def hello_world():
return 'Hello from Flask!'
@app.route("/getmsg/<key>")
def do_getmsg(key):
min_=request.args.get("min")
if not min_:
min_=1
if key!=access_key:
return 'Access denied.'
msgs=msg.query.filter(msg.id>int(min_)).order_by(msg.id.desc()).limit(10).all()
json_str=[]
for m in msgs:
json_str.append(m.get_json())
return "[%s]" % ",".join(json_str)
@app.route("/sendmsg/<int:qq>",methods={"POST"})
def do_sendMsg(qq):
content=request.form.get("content",None)
if not content:
abort(500)
msg_=msg(qq,content)
db.session.add(msg_)
db.session.commit()
return "ok."
if __name__ == '__main__':
app.run()
4 成果展示
只需要向 http://xxxxx.com/sendmsg/接收者QQ号 发送一个POST请求,表单content为消息内容,就可以实现控制QQ发消息。
发送POST请求我也是用python写的:
import requests
data={"content":"消息内容"}
r=requests.post("http://域名或者IP地址/sendmsg/QQ号",data=data)
print(r.text)
然后就能收到消息(我用的是TIM QQ):
------------------------我是分割线---------------------------
另外,如果你对这篇文章感兴趣,可以点一下关注或者喜欢。。。