通过Xposed插件,实现稳定的QQ聊天http接口

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()方法的逻辑:

BaseChatPie.b()方法

可以看出,b()方法在校验消息合法性后调用了com.tencent.mobileqq.activity.ChatActivityFacade的a方法。这是一个静态方法,一共五个参数:AppQQAppInterface,Context,SessionInfo,String,ArrayList.
从命名可以看出发消息时第1,2个参数应该不会变化,第4个参数时消息框中的内容,第五个参数为null.
所以只需要生成SessionInfo就可以通过Xposed插件发送消息。
SessionInfo的结构比较简单:
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://:@/数据库名"
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 ""%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/")
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/",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):


消息

------------------------我是分割线---------------------------

另外,如果你对这篇文章感兴趣,可以点一下关注或者喜欢。。。

你可能感兴趣的:(通过Xposed插件,实现稳定的QQ聊天http接口)