V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
NxnXgpuPSfsIT
V2EX  ›  Python

手把手教你扩展个人微信号( 1)

  •  2
     
  •   NxnXgpuPSfsIT ·
    littlecodersh · 2016-05-22 15:09:52 +08:00 · 8594 次点击
    这是一个创建于 3114 天前的主题,其中的信息可能已经有所发展或是发生改变。

    现在的日常生活已经离不开微信,难免会生出微信有没有什么 API 可以使用的想法。

    那样就可以拿自己微信做个消息聚合、开个投票什么的,可以显然没有这种东西。

    不过还好,有网页版微信不就等于有了 API 么,这个项目就是出于这个想法出现的。

    目标

    看完这一系列教程,你就能从头开始实现自己关于微信以及类似工具的想法,例如一个完善的微信机器人

    当然,如果你只对使用微信的 API 感兴趣,可以直接跳到下一篇教程,直接使用我已经完成的API

    本文为该教程的第一部分,主要讲述抓包与伪造,将会以最简单的方法介绍使用 Python 模拟登陆抓取数据等内容。

    Python 与基本的网络基础都不困难,所以即使没有这方面基础辅助搜索引擎也完全可以学习本教程。

    关于本教程有任何建议或者疑问,都欢迎邮件与我联系,或者在github 上提出[email protected]

    教程流程简介

    教程将会从如何分析微信协议开始,第一部分将教你如何从零开始获取并模拟扩展个人微信号所需要的协议。

    第二部分将会就这些协议进行利用,以微信机器人为例介绍我给出的项目基本框架与存储、任务识别等功能。

    第三部分就项目基本框架开发插件,以消息聚合等功能为例对框架做进一步介绍与扩展。

    简单成果展示:

    目前的样例微信号被扩展为了能够完成信息上传下载的机器人,用于展示信息交互功能。

    其支持文件、图片、语音的上传下载,可以扫码尝试使用。

    QRCode

    本部分所需环境

    本文是这一教程的第一部分,需要配置抓包与 Python 环境。

    本教程使用的环境如下:

    • Windows 8.1
    • Python 2.7.11 (安装 Image, requests )
    • Wireshark 2.0.2
    • 微信版本 6.3.15

    Wireshark 配置

    Wireshark 是常见的抓包软件,这里通过一些配置抓取微信网页端的流量。

    由于微信网页端使用 https ,需要特殊的配置才能看到有意义的内容,具体的配置见这里

    配置完成以后开始抓包,载入https://www.baidu.com后若能看到 http 请求则配置成功。

    分析并模拟扫码,并获取登录状态

    微信网页端登陆分为很多步,这里以第一步扫码为例讲解如何从抓包开始完成模拟。

    分析过程

    在抓包以前,我们需要先想清楚这是一个什么样的过程。

    我们都登录过网页端微信,没有的话可以现在做一个尝试:微信网页端

    这个过程简单而言可以分为如下几步:

    1. 向服务器提供一些用于获取二维码的数据
    2. 服务器返回二维码
    3. 向服务器询问二维码扫描状态
    4. 服务器返回扫描状态

    有了这些概念以后就可以开始将这四步和包对应起来。

    对应过程与实际的包

    开启 wireshark 抓包后登陆网页端微信,完成扫码登陆,然后关闭 wireshark 抓包。

    筛选 http 请求(就是菜单栏下面输入的那个 http ),可以看到这样的界面。

    抓到的包总览

    这里需要讲的就是第一列“ No.”列的数字就是后文说的几号包,例如第一行就是 30 号包。数据包的类型则在 Info 列中可以看到,是 GET,POST 或是别的请求。

    那么我们可以开始分析抓到的包了,我们先粗略的浏览一下数据包。

    第 325 号包引起了我的注意,因为登陆过程当中非常有特征的一个过程是二维码的获取,所以我们尝试打开这一数据包的图片的内容。

    QRCodeUrl

    325 号包是由 292 号包的请求获取的, 292 号包又是一个普通的 get 请求,所以我们尝试直接在浏览器中访问这一网址。(访问自己抓到的网址)

    具体的网址通过双击打开 292 号包即可找到。如需要可以点击这里看图。

    我们发现直接在浏览器中获取了一张二维码,所以这很有可能就是上述一、二步的过程了。

    那么我们是向服务器提供了哪些数据获取了二维码呢?

    • 每次我们登录的二维码会变化,且没有随二维码传回的标识,所以我们肯定提供了每次不同的信息
    • 网址中最后一部分看起来比较像标识: https://login.weixin.qq.com/qrcode/4ZtmDT6OPg==
    • 为了进一步验证猜想,再次抓包,发现类似 292 号包的请求 url 仅最后一部分存在区别
    • 所以我们提供了4ZtmDT6Opg==获取到了这一二维码。

    那么这一标识是随机生成的还是服务器获取的呢?

    • 从最近的包开始分析服务器传回的数据( Source 是服务器地址的数据),发现就在上一行, 286 号包有我们感兴趣的数据。
    • 打开这个包,可以看到其返回的数据为window.QRLogin.code = 200; window.QRLogin.uuid = "4ZtmDT6OPg==";(见下方截图)
    • 显然导致服务器返回这一请求的 284 号包就是我们获取标识(下称 uuid )所需要伪造的包。

    uuid 返回包

    那么 284 号包需要传递给服务器哪些数据?

    到了这里, 1,2 步的过程我们已经能够对应上相应的包了。

    3,4 部的最显著特征是在扫描成功以后会获取扫描用的微信号的头像。

    我们还是首先大致的浏览一下服务器返回的数据包,试图找到包含图片的数据包。

    • 从 325 号包(微信头像肯定在二维码之后获取)开始浏览。
    • 我们发现 338 号包中包含一个 base64 加密的图片,解压后可以看到自己的头像。
    • 所以这个数据包就是服务器返回的扫描成功的数据包了,而前面那部分window.code=201显然就是表示状态的代码。(见下方截图)
    • 经过尝试与再次抓包,我们理解状态码的涵义:200:登陆成功 201:扫描成功 408:图片过期
    • 那么第四部我们已经能够完全的理解

    微信扫码状态码

    我们很容易的找到了在登录过程当中不断出现的请求,那么要怎么模拟呢?

    至此你应该已经能将四个过程全部与具体的数据包对应。为了避免有遗漏的过程,我们将没有使用到的与服务器交互的数据包标识出来(右键 Mark )。经过简单的浏览,认为其中并没有必须的数据包交互。但值得注意的是,如果之后模拟数据包没有问题却无法登陆的话应当再回到这些数据包中搜寻。

    这里做一个简单的小结,这一部分简单的介绍了分析数据包的基本思路,以及一些小的技巧。当然这些仅供参考,在具体的抓包中完全可以根据具体的交互过程自由发挥。而目前留下来的问题有:第一步时的 appid 与第三步时的 r ,留待模拟时在做研究。

    使用 Python 模拟扫码

    这一部分我们使用 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 ,在测试时可以打开抓包,查看你发送的数据包与想要发送的数据包是否一样。

    那么我们开始模拟第一、二个过程,向服务器提供一些用于获取二维码的数据,服务器返回二维码。

    • 向服务器提交 284,292 号包
    • 从服务器返回数据中提取出 uuid 与二维码图片

    284 号包

    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 号包

    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')

    当然,如果获取失败可以再运行一次。

    同理的三、四步也可以按照这个方法写出,这里就不再赘述,只给出代码。

    而经过测试我们发现,第一步时的 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

    小练习答案

    源码可在该地址下载:这里

    结束语

    希望读完这篇文章能对你有帮助,有什么不足之处万望指正(鞠躬)。

    有什么想法或者想要关注我的更新,欢迎来GithubStar或者Fork

    160426

    LittleCoder

    EOF

    19 条回复    2018-07-16 13:24:01 +08:00
    radio777
        1
    radio777  
       2016-05-22 16:01:03 +08:00
    先收藏,慢慢看
    fuliti
        2
    fuliti  
       2016-05-22 18:15:22 +08:00
    真乃大神也。

    可惜看不懂。
    skyshy
        3
    skyshy  
       2016-05-22 19:45:06 +08:00
    很好玩的样子~
    GreatMartial
        4
    GreatMartial  
       2016-05-22 19:45:39 +08:00
    额,学习一下
    coolloves
        5
    coolloves  
       2016-05-22 21:25:55 +08:00 via Android
    马克之,感谢分享!!!
    yanchao7511461
        6
    yanchao7511461  
       2016-05-22 22:02:04 +08:00
    好像很不错的样子
    moonair
        7
    moonair  
       2016-05-23 00:45:17 +08:00
    啊 引起了我学习的兴趣 谢谢
    terence4444
        8
    terence4444  
       2016-05-23 08:19:04 +08:00 via iPhone
    这个比起公众号 API 有什么好处吗
    NxnXgpuPSfsIT
        9
    NxnXgpuPSfsIT  
    OP
       2016-05-23 09:36:57 +08:00
    @terence4444 各有优势吧
    一般来说,个人号 api 没有认证、功能(例如发送)的限制、获取方便、可加入群聊
    fantastic
        10
    fantastic  
       2016-05-23 09:42:36 +08:00
    先马克
    ziyuan
        11
    ziyuan  
       2016-05-23 09:51:26 +08:00
    非常好,正需要这样的东东,请问一下,如果要发送图片加文字这种的,象转发朋友圈里的内容,这样的功能有吗?
    wohenyingyu01
        12
    wohenyingyu01  
       2016-05-23 10:37:19 +08:00
    厉害厉害,原来 ssl 包可以这么解……
    haython
        13
    haython  
       2016-05-23 11:51:46 +08:00
    这是因为微信网页端存在心跳机制,一段时间不交互将会断开连接。
    你这里说的交互,是指真正的发消息,还是只是请求一下接口?
    以前尝试过,大概 1 天之后就掉线了,需要重新登录
    NxnXgpuPSfsIT
        14
    NxnXgpuPSfsIT  
    OP
       2016-05-23 14:42:59 +08:00 via Android
    @haython 交互呀,演示机器人一直都挂着。心跳包原理是一样的,具体的模拟在我的 github 里有。
    forever139
        15
    forever139  
       2016-05-23 17:40:19 +08:00
    不错,感谢分享
    NxnXgpuPSfsIT
        16
    NxnXgpuPSfsIT  
    OP
       2016-05-27 14:05:00 +08:00
    @ziyuan 没有,这个通过网页微信的接口暂时没有办法实现。
    neomaidasi
        17
    neomaidasi  
       2016-09-03 11:15:21 +08:00
    @NxnXgpuPSfsIT 有没有试过抓客户端的包?说实话网页版的功能太局限了。
    dodo20120
        18
    dodo20120  
       2016-10-15 14:30:41 +08:00
    @NxnXgpuPSfsIT 为啥我抓取的就抓取不到你第一张图那个样子的, http 只能看到两条
    mamujun
        19
    mamujun  
       2018-07-16 13:24:01 +08:00 via iPhone
    不错感谢分享
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2871 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 08:51 · PVG 16:51 · LAX 00:51 · JFK 03:51
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.