在1月初无意看到某微信爱好者学习交流群里发现讨论一个名为 MicroChat (基于Mars)利用微信AndroidAPP客户端通讯协议代码!!, 震惊之余,已对作者膜拜。心情激动之下下载了下来,参考了一些文章对原始版本进行了部分修正和应用测试。 后测试增加了一些功能实现以及对扩展模拟任意设备方式登入验证,和特定功能处理的思路。本人能力有限,技术很菜很水,但是秉着对技术向往以及分享我的坑给更多的学习者科普了解,故整理编辑了一篇文章带领大家先一睹为快。并且对MicroChat基础功能做了一些扩展思路,如果有错误的地方,欢迎批评指正 !
准备工作
开发环境:
开发工具: Visual Studio 2015 及以上版本(marsWin32SDK 需要vc140以上)
抓包和分析工具: Wireshark / Fiddler / Charles、TCPDump
编译配套: Boost 、 ATL
附加目录
如果本机缺少引用目录请手动附加
其他提醒
编译顺序为: Mars相关依赖 / SQLite3 -> MicrochatSDK(基于Mars Win32 Example) -> MicroChat(用户层)
微信是如何通信的呢?
端口
经过使用扫描器对微信常用域名的扫描发现远程端口有: 80 443 8080 5222 5223 5228 等
域名
dns.weixin.qq.com
support.weixin.qq.com 80/8080
short.weixin.qq.com 443/8080 (sz)
long.weixin.qq.com 80/443 (sz)
wx.qlogo.cn 80
timg.cn 等
基本执行过程概况
- 程序启动后,优先尝试DNS解析特定域名(上述域名,会返回所有节点);
dns查询
dns.weixin.qq.com
返回一组IP地址long.weixin.qq.com
返回一组IP地址,本次通信中,微信使用了最后一个IP作为TCP长连接的连接地址。
http://dns.weixin.qq.com/cgi-bin/micromsg-bin/newgetdns?uin=0&clientversion=620888113&scene=0&net=1
用于请求服务器获得最优IP路径。服务器通过结算返回一个xml定义了域名:IP对应列表。
仔细阅读,可看到微信已经开始了国际化的步伐:香港、加拿大、韩国等。
具体文本,请参考:https://gist.github.com/yongboy/9341884
获取到long.weixin.qq.com最优IP,然后建立到101.227.131.105的TCP长连接
- 如果DNS查询不可用,程序转为使用hardcode的ip链接服务;
- 如果dns可用,返回的ip为根据ISP智能解析的结果,程序使用返回的ip链接服务;
- 程序在注册、验证、解封、小程序等内置内容请求、阶段会使用https链接,加密协议为腾讯的mmtls;
- 客户端使用tcp 80/8080连接远端服务器。80/8080两个端口同时或任何单独一个,均可提供服务;
- 80端口为短链接,8080为长链接, 程序会优先使用8080端口;
请求确认连接后获取数据。
提交请求中包含 账号 密码 登录方式(可以模拟任何设备~) 设备信息 网络信息 网络设备信息 地理位置 等~
POST http://short.weixin.qq.com/cgi-bin/micromsg-bin/getprofile HTTP/1.1 (application/octet-stream)
返回一个名为“micromsgresp.dat”的附件,个人信息
POST http://short.weixin.qq.com/cgi-bin/micromsg-bin/whatsnews HTTP/1.1 (application/octet-stream)
仍然返回一个名为“micromsgresp.dat”的附件
大概是微信新版本介绍和验证之类的、资讯、订阅更新等...
POST http://short.weixin.qq.com/cgi-bin/micromsg-bin/downloadpackage HTTP/1.1 (application/octet-stream)
输出为micromsgresp.dat文件
然后还会再有一个 micromsgresp.dat创建,大概可能是未读消息和联系人列表吧
(测试大概总共会有1~3次)
GET http://support.weixin.qq.com/cgi-bin/mmsupport-bin/reportdevice?channel=34&deviceid=A952001f7a840c2a&clientversion=620888113&platform=0&lang=zh_CN&installtype=0 HTTP/1.1
客户端自身信息和设备信息校验反馈
POST http://short.weixin.qq.com/cgi-bin/micromsg-bin/reportstrategy HTTP/1.1 (application/octet-stream)
返回分块数据
GET http://wx.qlogo.cn/mmhead/Q3auHgzwzM7NR4TYFcoNjbxZpfO9aiaE7RU5lXGUw13SMicL6iacWIf2A/96
图片等一些静态资源都会被分配到wx.qlogo.cn域名下,会根据加载或手动访问情况异步多线程下载缓存在本地目录。
- 当连续2次心跳发送失败时,客户端会弹出提示“当前网络状况不好,是否提交反馈数据”,确认后客户端试图通过web提交反馈数据;
心跳频率约为5分钟
登陆之后,会建立一个long.weixin.qq.com的HTTP长连接,端口号为8080
具体查看长连接初始数据通信,没有发现任何包含"HTTP"字样的数据,以为是微信自定义的TCP/HTTP通信格式。据分析,用于可能用于获取数据、心跳交换消息等用途吧。
初始消息传输
个人资料、离线未阅读消息部分等通过 POST HTTP短连接单独获取。如上..
二进制简单分析
抽取微信某次HTTP协议方式通信数据,16进制表示,每两个靠近的数字为一个byte字节。
协议文本分析
微信协议报文可能如下:
一个消息包 = 消息头 + 消息体 消息头固定16字节长度,消息包长度定义在消息头前4个字节中。
单纯摘取第0000行为例,共16个字节的头部:
00 00 00 10 00 10 00 01 00 00 00 06 00 00 00 0f
16进制表示,每两个紧挨着数字代表一个byte字节。
微信消息包格式:
- 前4字节表示数据包长度,可变 值为16时,意味着一个仅仅包含头部的完整的数据包(可能表示着预先定义好的业务意义),后面可能还有会别的消息包
- 2个字节表示头部长度,固定值,0x10 = 16
- 2个字节表示谢意版本,固定值,0x01 = 1
- 4个字节操作码说明数字,可变
- 序列号,可变
- 头部后面紧跟着消息体,非明文,加密形式
- 一个消息包,最小16 byte字节
如果对报文不是很了解可以学习参考一下其他底层通信协议 比如.. ModbusTCP
通过数据多次采样分析:
0000 - 0040为单独的数据包 0050行为下一个数据包的头部,前四个字节值为0xca = 202,表示包含了从0050-0110共202个字节数据 一次数据发送,可能包含若干子数据包 换行符\n,16进制表示为0x0a,在00f0行,包含了两个换行符号 一个数据体换行符号用于更细粒度的业务数据分割 是否蒙对,需要问问做微信协议的同学 所有被标记为HTTP协议通信所发送数据都包含换行符号 动手试试猜想,模拟微信TCP长连接 开始很不解为什么会出现如此怪异的HTTP双通道长连接请求,难道基于TCP通信,然后做了一些手脚?很常规的TCP长连接,传输数据时(不是所有数据传输),被wireshark误认为HTTP长连接。这个需要做一个实验证实一下自己想法,设想如下:
写一个Ping-Pong客户端、服务器端程序,然后使用Wireshark看一下结果,是否符合判断。
测试服务端代码
https://gist.githubusercontent.com/yongboy/9341037/raw/pong_server.c
/**
* nieyong@youku.com
* how to compile it:
* gcc pong_server.c -o pong_server /usr/local/lib/libev.a -lm
*/
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <err.h>
#include <unistd.h>
#include "../include/ev.h"
static int server_port = 8080;
struct ev_loop *loop;
typedef struct {
int fd;
ev_io ev_read;
} client_t;
ev_io ev_accept;
static void free_res(struct ev_loop *loop, ev_io *ws);
int setnonblock(int fd) {
int flags = fcntl(fd, F_GETFL);
if (flags < 0)
return flags;
flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) < 0)
return -1;
return 0;
}
static void read_cb(struct ev_loop *loop, ev_io *w, int revents) {
client_t *client = w->data;
int r = 0;
char rbuff[1024];
if (revents & EV_READ) {
r = read(client->fd, &rbuff, 1024);
}
if (EV_ERROR