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


消息

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容