如今的平常生活已经离不开微信,不免会生出微信有没有什么API可使用的想法。php
那样就能够拿本身微信作个消息聚合、开个投票什么的,能够显然没有这种东西。html
不过还好,有网页版微信不就等于有了API么,这个项目就是出于这个想法出现的。node
看完这一系列教程,你就能从头开始实现本身关于微信以及相似工具的想法,例如一个完善的微信机器人。python
固然,若是你只对使用微信的API感兴趣,能够直接跳到下一篇教程,直接使用我已经完成的API。git
本文为该教程的第一部分,主要讲述抓包与伪造,将会以最简单的方法介绍使用Python模拟登录抓取数据等内容。github
Python与基本的网络基础都不困难,因此即便没有这方面基础辅助搜索引擎也彻底能够学习本教程。web
关于本教程有任何建议或者疑问,都欢迎邮件与我联系,或者在github上提出(i7meavnktqegm1b@qq.com)正则表达式
教程将会从如何分析微信协议开始,第一部分将教你如何从零开始获取并模拟扩展我的微信号所须要的协议。json
第二部分将会就这些协议进行利用,以微信机器人为例介绍我给出的项目基本框架与存储、任务识别等功能。浏览器
第三部分就项目基本框架开发插件,以消息聚合等功能为例对框架作进一步介绍与扩展。
目前的样例微信号被扩展为了可以完成信息上传下载的机器人,用于展现信息交互功能。
其支持文件、图片、语音的上传下载,能够扫码尝试使用。
本文是这一教程的第一部分,须要配置抓包与Python环境。
本教程使用的环境以下:
Windows 8.1
Python 2.7.11 (安装Image, requests)
Wireshark 2.0.2
微信版本6.3.15
Wireshark是常见的抓包软件,这里经过一些配置抓取微信网页端的流量。
因为微信网页端使用https,须要特殊的配置才能看到有意义的内容,具体的配置见这里。
配置完成之后开始抓包,载入https://www.baidu.com
后若能看到http请求则配置成功。
微信网页端登录分为不少步,这里以第一步扫码为例讲解如何从抓包开始完成模拟。
在抓包之前,咱们须要先想清楚这是一个什么样的过程。
咱们都登陆过网页端微信,没有的话能够如今作一个尝试:微信网页端。
这个过程简单而言能够分为以下几步:
向服务器提供一些用于获取二维码的数据
服务器返回二维码
向服务器询问二维码扫描状态
服务器返回扫描状态
有了这些概念之后就能够开始将这四步和包对应起来。
开启wireshark抓包后登录网页端微信,完成扫码登录,而后关闭wireshark抓包。
筛选http请求(就是菜单栏下面输入的那个http),能够看到这样的界面。
这里须要讲的就是第一列“No.”列的数字就是后文说的几号包,例如第一行就是30号包。数据包的类型则在Info列中能够看到,是GET,POST或是别的请求。
那么咱们能够开始分析抓到的包了,咱们先粗略的浏览一下数据包。
第325号包引发了个人注意,由于登录过程中很是有特征的一个过程是二维码的获取,因此咱们尝试打开这一数据包的图片的内容。
325号包是由292号包的请求获取的,292号包又是一个普通的get请求,因此咱们尝试直接在浏览器中访问这一网址。(访问本身抓到的网址)
具体的网址经过双击打开292号包便可找到。如须要能够点击这里看图。
咱们发现直接在浏览器中获取了一张二维码,因此这颇有可能就是上述1、二步的过程了。
那么咱们是向服务器提供了哪些数据获取了二维码呢?
每次咱们登陆的二维码会变化,且没有随二维码传回的标识,因此咱们确定提供了每次不一样的信息
网址中最后一部分看起来比较像标识:https://login.weixin.qq.com/qrcode/4ZtmDT6OPg==
为了进一步验证猜测,再次抓包,发现相似292号包的请求url仅最后一部分存在区别
因此咱们提供了4ZtmDT6Opg==
获取到了这一二维码。
那么这一标识是随机生成的仍是服务器获取的呢?
从最近的包开始分析服务器传回的数据(Source是服务器地址的数据),发现就在上一行,286号包有咱们感兴趣的数据。
打开这个包,能够看到其返回的数据为window.QRLogin.code = 200; window.QRLogin.uuid = "4ZtmDT6OPg==";
(见下方截图)
显然致使服务器返回这一请求的284号包就是咱们获取标识(下称uuid)所须要伪造的包。
那么284号包须要传递给服务器哪些数据?
这是一个get请求,因此咱们分析其请求的url:https://login.weixin.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=en_US&_=1453725386008
。
能够发现须要给出五个量appid, redirect_uri, fun, lang, _
。
其中除了appid其他都显然是固定的量(_的格式显然为时间戳)。
然而搜索284号包以前的包也没有发现这一数值的来源,因此暂且认为其也是固定的量,模拟时若是出现问题再作尝试。
到了这里,1,2步的过程咱们已经可以对应上相应的包了。
3,4部的最显著特征是在扫描成功之后会获取扫描用的微信号的头像。
咱们仍是首先大体的浏览一下服务器返回的数据包,试图找到包含图片的数据包。
从325号包(微信头像确定在二维码以后获取)开始浏览。
咱们发现338号包中包含一个base64加密的图片,解压后能够看到本身的头像。
因此这个数据包就是服务器返回的扫描成功的数据包了,而前面那部分window.code=201
显然就是表示状态的代码。(见下方截图)
通过尝试与再次抓包,咱们理解状态码的涵义:200:登录成功 201:扫描成功 408:图片过时
那么第四部咱们已经可以彻底的理解
咱们很容易的找到了在登陆过程中不断出现的请求,那么要怎么模拟呢?
首先这是一个简单的get请求,url为:https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=4ZtmDT6OPg==&tip=1&r=-2026440414&_=1453725386009
能够发现须要给出五个量loginicon, uuid, tip, r, _
经过屡次抓包发现除了r之外均可以找到简单的规律,那么r的规律等待模拟时再尝试处理
至此你应该已经能将四个过程所有与具体的数据包对应。为了不有遗漏的过程,咱们将没有使用到的与服务器交互的数据包标识出来(右键Mark)。通过简单的浏览,认为其中并无必须的数据包交互。但值得注意的是,若是以后模拟数据包没有问题却没法登录的话应当再回到这些数据包中搜寻。
这里作一个简单的小结,这一部分简单的介绍了分析数据包的基本思路,以及一些小的技巧。固然这些仅供参考,在具体的抓包中彻底能够根据具体的交互过程自由发挥。而目前留下来的问题有:第一步时的appid与第三步时的r,留待模拟时在作研究。
这一部分咱们使用python的requests模块,能够经过pip install requests
安装。
咱们先来简单的讲述一下这个包。
import requests # 新建一个session对象(就像开了一个浏览器同样) session = requests.Session() # 使用get方法获取https://www.baidu.com/s?wd=python url = 'https://www.baidu.com/s' params = { 'wd': 'python', } r = session.get(url = url, params = params) with open('baidu.htm') as f: f.write(r.content) # 存入文件,可使用浏览器尝试打开 # 举例使用post方法 import json url = 'https://www.baidu.com' data = { 'wd': 'python', } r = session.get(url = url, data = json.dumps(data)) with open('baidu.htm') as f: f.write(r.content) # 以上代码与下面的代码不连续
若是想要更多的了解这个包,能够浏览requests快速入门。
你能够尝试获取一个你熟悉的网站来测试使用requests,在测试时能够打开抓包,查看你发送的数据包与想要发送的数据包是否同样。
那么咱们开始模拟第1、二个过程,向服务器提供一些用于获取二维码的数据,服务器返回二维码。
向服务器提交284,292号包
从服务器返回数据中提取出uuid与二维码图片
284号包
咱们须要模拟的地址为:https://login.weixin.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=en_US&_=1453725386008 ,因此咱们模拟的代码以下:
#coding=utf8 import time, requests session = requests.Session() url = 'https://login.weixin.qq.com/jslogin' params = { 'appid': 'wx782c26e4c19acffb', 'redirect_uri': 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage', 'fun': 'new', 'lang': 'en_US', '_': int(time.time()), } r = session.get(url, params = params) print('Content: %s'%r.text)
固然,将模拟的地址所有写在url里面效果彻底同样。
值得一提的是requests会帮咱们自动urlencode,若是不须要urlencode(/变为了%2F)能够将全部内容都写在url里面。
提取出uuid
这里使用re,若是不了解正则表达式的话能够直接拿来用,毕竟和这一个教程并不相关。
# 上接上一段程序 import re regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)";' # 咱们能够看到返回的量是上述的格式,括号内的内容被提取了出来 data = re.search(regx, r.text) if data and data.group(1) == '200': uuid = data.group(2) print('uuid: %s'%uuid)
若是没能成功获取到uuid能够尝试再运行一次。
292号包
咱们须要模拟的url为:https://login.weixin.qq.com/qrcode/4ZtmDT6OPg== ,因此咱们模拟的代码以下:
# 上接上一段程序 url = 'https://login.weixin.qq.com/qrcode/' + uuid r = session.get(url, stream = True) with open('QRCode.jpg', 'wb') as f: f.write(r.content) # 如今你能够在你存储代码的位置发现一张存下来的图片,用下面的代码打开它 import platform, os, subprocess if platform.system() == 'Darwin': subprocess.call(['open', 'QRCode.jpg']) elif platform.system() == 'Linux': subprocess.call(['xdg-open', 'QRCode.jpg']) else: os.startfile('QR.jpg')
因为咱们须要获取图像,因此须要以二进制数据流的形式获取服务器返回的数据包,因此增长stream = True
。
而将二进制数据流写入也须要在打开文件时设定二进制写入,即open('QRCode.jpg', 'wb')
。
固然,若是获取失败能够再运行一次。
同理的3、四步也能够按照这个方法写出,这里就再也不赘述,只给出代码。
而通过测试咱们发现,第一步时的appid实际是一个固定的量,第三步时的r甚至不输入也能够登陆。
# 上接上一段代码 import time while 1: url = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login' # 这里演示一下不使用自带的urlencode params = 'tip=1&uuid=%s&_=%s'%(uuid, int(time.time())) r = session.get(url, params = params) regx = r'window.code=(\d+)' data = re.search(regx, r.text) if not data: continue if data.group(1) == '200': # 下面一段是为了以后获取登陆信息作准备 uriRegex = r'window.redirect_uri="(\S+)";' redirectUri = re.search(uriRegex, r.text).group(1) r = session.get(redirectUri, allow_redirects=False) redirectUri = redirectUri[:redirectUri.rfind('/')] baseRequestText = r.text break elif data.group(1) == '201': print('You have scanned the QRCode') time.sleep(1) elif data.group(1) == '408': raise Exception('QRCode should be renewed') print('Login successfully')
当你看到Login successfully时,说明至此咱们已经成功从零开始,经过抓包分析,用python成功模拟了python登录。
不过是否是看上去没有什么反馈呢?那是由于咱们尚未模拟会产生反馈的包,但其实差的只是研究发文字、发图片什么的包了。
为了体现咱们已经登录了,加上后面这段代码就能够看到登录的帐号信息:
# 上接上一段代码 import xml.dom.minidom def get_login_info(s): baseRequest = {} for node in xml.dom.minidom.parseString(s).documentElement.childNodes: if node.nodeName == 'skey': baseRequest['Skey'] = node.childNodes[0].data.encode('utf8') elif node.nodeName == 'wxsid': baseRequest['Sid'] = node.childNodes[0].data.encode('utf8') elif node.nodeName == 'wxuin': baseRequest['Uin'] = node.childNodes[0].data.encode('utf8') elif node.nodeName == 'pass_ticket': baseRequest['DeviceID'] = node.childNodes[0].data.encode('utf8') return baseRequest baseRequest = get_login_info(baseRequestText) url = '%s/webwxinit?r=%s' % (redirectUri, int(time.time())) data = { 'BaseRequest': baseRequest, } headers = { 'ContentType': 'application/json; charset=UTF-8' } r = session.post(url, data = json.dumps(data), headers = headers) dic = json.loads(r.content.decode('utf-8', 'replace')) print('Log in as %s'%dic['User']['NickName'])
这里作一个简单的小结:
模拟数据包整体而言是以寻找未知的必须数据为线索,辅助一些技巧,串联起整个过程。
首先须要用python初始化一个session,不然登陆过程的存储将会比较麻烦。
模拟数据包的时候首先区分get与post请求,对应session的get与post方法。
get的数据为url后半部分的内容,post是数据包最后一部分的内容。
get方法中传入数据的标示为params, post方法中传入数据的标示为data。
session的get,post方法返回一个量,能够经过r.text自动编码显示。
存储图片有特殊的方式与配置。
到如今为止我展现了一个完整的抓包、分析、模拟的过程完成了模拟登录,其余一些事情其实也都是相似的过程,想清楚每一步要作些什么便可。
这里用到的软件都只介绍了最简单的一些方法,进一步的内容这里给出一些建议:
wireshark能够直接浏览官方文档,有空能够作一个了解。
requests包的使用经过搜索引擎便可,特殊的功能建议直接阅读源码。
那么作一个小练习好了,测试一下学到的东西:读取命令行的输入并发送给本身。(这部分的源码放在了文末)
在分析包的过程当中记得抓好位置的必要数据这个线索,练习以前提到过的一些技巧。
把大的过程拆分红一个一个小的任务可能会让分析简单不少。
若是发现登陆过程意料以外的断了,分析不出缘由,能够尝试多抓几回包再比较分析。
这是由于微信网页端存在心跳机制,一段时间不交互将会断开链接。
另外,每次获取数据时(webwxsync)记得更新SyncKey。
在项目中已经模拟好了几乎全部的请求,你能够经过参考个人方法与数据包。
若是以后微信网页版出现更新我会在本项目中及时更新。
项目中的微信网页端接口见这里
这是由于使用requests包会自动将中文文件名编码为服务器端没法识别的格式,因此须要修改requests包或者使用别的方法上传文件。
最简单的方法即将requests包的packages/urlib3中的fields.py中的format_header_param
方法改成以下内容:
def format_header_param(name, value): if not any(ch in value for ch in '"\\\r\n'): result = '%s="%s"' % (name, value) try: result.encode('ascii') except UnicodeEncodeError: pass else: return result if not six.PY3: # Python 2: value = value.encode('utf-8') value = email.utils.encode_rfc2231(value, 'utf-8') value = '%s="%s"' % (name, value.decode('utf8')) return value
建议更新Python版本至2.7.11
源码可在该地址下载:这里
但愿读完这篇文章能对你有帮助,有什么不足之处万望指正(鞠躬)。
有什么想法或者想要关注个人更新,欢迎来Github上Star或者Fork。
160426
LittleCoder
EOF