美文网首页@IT·互联网
通过Xposed插件,实现稳定的QQ聊天http接口

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

作者: 蒹葭杨柳 | 来源:发表于2018-11-17 21:44 被阅读16次

    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://<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):


    消息

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

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

    相关文章

      网友评论

        本文标题:通过Xposed插件,实现稳定的QQ聊天http接口

        本文链接:https://www.haomeiwen.com/subject/limifqtx.html