使用Python获取王者头脑2的问题与答案

最近和老婆迷上了头脑王者2。赢一局25金币,输一局掉100金币,这设置妥妥的亏本啊,玩下来才发现运营者的考虑是想让你多多分享,病毒营销啊。
天天金币不够用,又不想分享怎么办,技术手段看看有没有思路,开干!

0x00.获取网络数据流

小程序的所有通信都是必须要求https的。所以要想获取数据必须采用中间人攻击获取明文数据流

1.Charles分析

先用了Charles。发现这个小游戏大部分的数据交互都用的是websocket。在下面的地址
https://tnwz2-server.hortorgames.com

2.更强工具mitmproxy安装启动

对付websocket Charles局限了。我找到了这个工具 :mitmproxy
mitmproxy 如官方描述:

mitmproxy is a free and open source interactive HTTPS proxy.

看了一下果真强大,还有Python接口
嗯,先安装(MacOS):

brew install mitmproxy 

再安装python支持包:

pip3 install mitmproxy --user

启动:

mitmproxy -m socks5

启动后会在8080端口监听。指定socks5代理方式,http代理也可以使用

3.mitmproxy配置

手机WIFI设置中配置好代理端口和电脑ip
用手机浏览器访问http://mitm.it,看到下面的界面说明代理设置没问题了

image.png

点击对应的平台下载证书安装。然后在系统中设置信任,我就不多说怎么设置了
试一下访问https://www.baidu.com没问题。证书生效了

4.获取websocket数据流

如下代码(file:websocket.py):

mport mitmproxy.log
import mitmproxy.websocket
import mitmproxy.proxy.protocol
from mitmproxy import ctx



class InfoPrint:
    # HTTP lifecycle
    # 去除用不到的生命周期方法
   def websocket_message(self, flow: mitmproxy.websocket.WebSocketFlow):
        """
            Called when a WebSocket message is received from the client or
            server. The most recent message will be flow.messages[-1]. The
            message is user-modifiable. Currently there are two types of
            messages, corresponding to the BINARY and TEXT frame types.
        """
        content = flow.messages[-1].content # 获取二进制数据
        ctx.log.info("## msg:%s" % (str(content)))
    

addons = [
    InfoPrint()
]

启动:mitmdump -s websocket.py -m socks5 |grep '##'

0x01.数据解包

1.找规律

数据流是2进制。但看起来没有加密。拿了一条数据分析:
1f8b08000000000002ff6a5e9c9c9bb2d62fbf2433ad32de39233fbf387571716aa1f292a4fc94ca232e6dab8b32d3334a1cf38acb538b2632ae484a2c29c949f57439c77420517e716966ca39867791624b8b93f38b52ddd66416bb651615970481b41c5a9608d504080000ffff4852639f63000000
所有数据都是1f8b08开头。。。似乎是一种格式,google一下,简单了,gzip!

In [155]: import gzip

In [156]: gzip.decompress(b)
Out[156]: b'\x83\xa3cmd\xadNotify_Choose\xa3seq#\xa4body\xc4D\x86\xabrightAnswer\x91\x01\xa8battleID\xce\x02\xc0a\x1f\xa3uid\xce\x00\xeeY\x16\xa5scoreF\xacisFirstRight\xc2\xa6answer\x91\x01'

多处理几条找规律:

b'\x83\xa3cmd\xa9Resp_Sync\xa3seq\x08\xa4body\xc41\x81\xaamatchTeams\x81\xa5teams\x81\xce\x00\xd2\xd4\xd0\x81\xa7members\x81\xce\x00\xeeY\x16\x81\xa6online\xc2'
b'\x83\xa3cmd\xa9Resp_Sync\xa3seq\t\xa4body\xc41\x81\xaamatchTeams\x81\xa5teams\x81\xce\x00\xd2\xd4\xd0\x81\xa7members\x81\xce\x00\xeeY\x16\x81\xa6online\xc3'
b'\x83\xa3cmd\xafTeam_MatchStart\xa3seq\x04\xa4body\xc4\x01\x80'
b'\x84\xa3cmd\xb0Push_MatchStared\xa3seq\n\xa4rSeq\x04\xa4body\xc4;\x82\xa6player\x80\xaamatchTeams\x81\xa5teams\x81\xce\x00\xd2\xd4\xd0\x82\xa5state\x02\xacstateTimeout\xce\\C\x13\xca'
b'\x83\xa3cmd\xa9Resp_Sync\xa3seq\x0b\xa4body\xc43\x81\xaamatchTeams\x81\xa5teams\x81\xce\x00\xd2\xd4\xd0\x82\xa5state\x03\xacstateTimeout\xce\\C\x145'

看样子像是某种协议,或者某种序列化方式

2.确定反序列化方法

试了bson、pickle都不对
Google一下\0x83\0xa3cmd 找到一个线索有下面一段代码:

序列化前的包:
message = {'load': {'cmd': '_auth', 'id': 'zhu1', 'pub': '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqicJ8L2vmjeEpSfi1TEE\nZzdPo5Ibgo5a+EvQtMqzm/elKhWjNSp82PE9Fl5BuGgexk9P0+kwpAEws6vWNgyG\nJuTow+PYhVMWU6B0P+pMcdm7YxcqKczvHXmH6ugzLt1uQmwcW0RjL2POGxgLyprL\nM1isdX/bSLnMabtAfsORv7q7BmsJaXIxtdFwH8yDuJRvluia448OjHU4ugQr+yWj\nfRuqOzR5UFk0K4CivPt6E/E7DdxJV4j1OSsnGXi5XPLoJ7dACQne6/2xOlSkb6tZ\neQBC/HdfWTflD4LYSlrsVlQTl1+DDmYHQL7eCYg0zC34xi+DC+Qd0MJ1DOPiRwWq\nswIDAQAB\n-----END PUBLIC KEY-----'}, 'enc': 'clear'}

序列化后:
'\x82\xa4load\x83\xa3cmd\xa5_auth\xa2id\xa4zhu1\xa3pub\xda\x01\xc2-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqicJ8L2vmjeEpSfi1TEE\nZzdPo5Ibgo5a+EvQtMqzm/elKhWjNSp82PE9Fl5BuGgexk9P0+kwpAEws6vWNgyG\nJuTow+PYhVMWU6B0P+pMcdm7YxcqKczvHXmH6ugzLt1uQmwcW0RjL2POGxgLyprL\nM1isdX/bSLnMabtAfsORv7q7BmsJaXIxtdFwH8yDuJRvluia448OjHU4ugQr+yWj\nfRuqOzR5UFk0K4CivPt6E/E7DdxJV4j1OSsnGXi5XPLoJ7dACQne6/2xOlSkb6tZ\neQBC/HdfWTflD4LYSlrsVlQTl1+DDmYHQL7eCYg0zC34xi+DC+Qd0MJ1DOPiRwWq\nswIDAQAB\n-----END PUBLIC KEY-----\xa3enc\xa5clear'

#master收到该包后
在zeromq.py文件ZeroMQReqServerChannel类的handle_message方法
if payload['enc'] == 'clear' and payload.get('load', {}).get('cmd') == '_auth':
    stream.send(self.serial.dumps(self._auth(payload['load'])))

和saltstack有关,我没接触过这个,看来这个数据流应该是一种salt支持的序列化方式
那就google搜源码看看:
找到这个 https://github.com/MadeiraCloud/salt/blob/master/sources/salt/payload.py
发现用的是msgpack这个包

In [158]: import msgpack

In [159]: msgpack.loads(b'\x83\xa3cmd\xadNotify_Choose\xa3seq#\xa4body\xc4D\x86\xabrightAnswer\x91\x01\xa8battleID\xce\x02\xc0a\x1f\xa3uid\xce\x00\xeeY\x16\xa5scoreF\xacisFirstRight\xc2\xa6answer\x91\x01')
Out[159]:
{b'cmd': b'Notify_Choose',
 b'seq': 35,
 b'body': b'\x86\xabrightAnswer\x91\x01\xa8battleID\xce\x02\xc0a\x1f\xa3uid\xce\x00\xeeY\x16\xa5scoreF\xacisFirstRight\xc2\xa6answer\x91\x01'}

找到路子了,那就看看这里面都有什么数据

0x02.分析协议

def websocket_message(self, flow: mitmproxy.websocket.WebSocketFlow):
       content = flow.messages[-1].content
        content = gzip.decompress(content)
        if len(content) == 0:
            return
        try:
            msg = msgpack.loads(content)
            cmd = msg[b'cmd']
            seq = msg[b'seq']
            body = msgpack.loads(msg[b'body'])
            # 打印出来分析,消息前加两个#方便过滤
            ctx.log.info("## msg:%s  seq:%d" % (cmd, seq))
            ctx.log.info("## body:%s " % str(body))
        except Exception as e:
            pass

打印数据来分析
启动:mitmdump -s websocket.py -m socks5 |grep '##'

部分数据:

## cmd:b'Resp_Sync' seq:8
## body:{b'battleInfo': {b'quizMap': {1: {b'className': b'\xe5\x8e\x86\xe5\x8f\xb2', b'category': 1, b'categoryName': b'\xe6\x96\x87\xe5\x8c\x96', b'answers': [2], b'answerNum': 1, b'id': 26408, b'quizType': 2, b'class': 3, b'quiz': b'\xe4\xb8\x89\xe5\x9b\xbd\xe6\x97\xb6\xe6\x9c\x9f\xef\xbc\x8c\xe6\x9d\x83\xe8\x87\xa3\xe8\x91\xa3\xe5\x8d\x93\xe7\x9a\x84\xe4\xb9\x89\xe5\xad\x90\xe6\x98\xaf\xef\xbc\x9f', b'options': [b'\xe5\x85\xb3\xe7\xbe\xbd', b'\xe5\xbc\xa0\xe9\xa3\x9e', b'\xe5\x90\x95\xe5\xb8\x83', b'\xe5\xad\x99\xe6\x9d\x83']}}, b'quizNum': 1, b'nextStateBeginTime': 1547907141, b'battleState': 3}}
## cmd:b'Resp_Sync' seq:9
## body:{b'battleInfo': {b'nextStateBeginTime': 1547907152, b'battleState': 4}}
## cmd:b'Resp_Sync' seq:10
## body:{b'battleInfo': {b'teams': {3: {b'score': 155}}, b'playerBattleInfo': {14423928: {b'rightQuizIDs': [1], b'answers': {1: {b'chooseData': [2], b'costTime': 1}}, b'scores': {1: 155}}}}}
## cmd:b'Notify_Choose' seq:11
## body:{b'answer': [2], b'rightAnswer': [2], b'battleID': 54182041, b'uid': 14423928, b'score': 155, b'isFirstRight': True}
## cmd:b'Resp_Sync' seq:12
## body:{b'battleInfo': {b'teams': {2: {b'score': 155}}, b'playerBattleInfo': {16296535: {b'scores': {1: 155}, b'rightQuizIDs': [1], b'answers': {1: {b'chooseData': [2], b'costTime': 1}}}}}}
## cmd:b'Notify_Choose' seq:13
## body:{b'battleID': 54182041, b'uid': 16296535, b'score': 155, b'isFirstRight': True, b'answer': [2], b'rightAnswer': [2]}
## cmd:b'Resp_Sync' seq:14
## body:{b'battleInfo': {b'teams': {2: {b'score': 239}}, b'playerBattleInfo': {14317409: {b'rightQuizIDs': [1], b'answers': {1: {b'chooseData': [2], b'costTime': 3}}, b'scores': {1: 84}}}}}
## cmd:b'Notify_Choose' seq:15
## body:{b'answer': [2], b'rightAnswer': [2], b'battleID': 54182041, b'uid': 14317409, b'score': 84, b'isFirstRight': False}
## cmd:b'Resp_Sync' seq:16
## body:{b'battleInfo': {b'playerBattleInfo': {16276981: {b'rightQuizIDs': [1], b'answers': {1: {b'chooseData': [2], b'costTime': 4}}, b'scores': {1: 74}}}, b'teams': {2: {b'score': 313}}}}
## cmd:b'Notify_Choose' seq:17
## body:{b'score': 74, b'isFirstRight': False, b'answer': [2], b'rightAnswer': [2], b'battleID': 54182041, b'uid': 16276981}
## cmd:b'Resp_Sync' seq:18
## body:{b'battleInfo': {b'playerBattleInfo': {16126335: {b'wrongQuizIDs': [1], b'answers': {1: {b'chooseData': [3], b'costTime': 4}}, b'scores': {1: 0}}}}}
## cmd:b'Notify_Choose' seq:19
## body:{b'uid': 16126335, b'score': 0, b'isFirstRight': False, b'answer': [3], b'rightAnswer': [2], b'battleID': 54182041}
## cmd:b'Fight_BattleChoose' seq:4
## body:{b'answer': [2], b'battleID': 54182041}
## cmd:b'Resp_Sync' seq:20
## body:{b'battleInfo': {b'teams': {3: {b'score': 229}}, b'playerBattleInfo': {15620374: {b'rightQuizIDs': [1], b'answers': {1: {b'chooseData': [2], b'costTime': 4}}, b'scores': {1: 74}}}}}
## cmd:b'Notify_Choose' seq:21
## body:{b'battleID': 54182041, b'uid': 15620374, b'score': 74, b'isFirstRight': False, b'answer': [2], b'rightAnswer': [2]}
## cmd:b'Resp_Sync' seq:22
## body:{b'baseSync': {b'sysTime': 1547907145}}

1.分析msg

数据包太多,专看看包含answer的。发现个有意思的,看样子像是找到目标了

cmd:b'Resp_Sync'
seq:8
body:{
b'battleInfo':
  {b'quizMap': {
    1: {
      b'className': b'\xe5\x8e\x86\xe5\x8f\xb2', b'category': 1, 
      b'categoryName': b'\xe6\x96\x87\xe5\x8c\x96', 
      b'answers': [2], b'answerNum': 1, b'id': 26408, b'quizType': 2, b'class': 3, 
      b'quiz': b'\xe4\xb8\x89\xe5\x9b\xbd\xe6\x97\xb6\xe6\x9c\x9f\xef\xbc\x8c\xe6\x9d\x83\xe8\x87\xa3\xe8\x91\xa3\xe5\x8d\x93\xe7\x9a\x84\xe4\xb9\x89\xe5\xad\x90\xe6\x98\xaf\xef\xbc\x9f', 
      b'options': [
          b'\xe5\x85\xb3\xe7\xbe\xbd', 
          b'\xe5\xbc\xa0\xe9\xa3\x9e', 
          b'\xe5\x90\x95\xe5\xb8\x83', 
          b'\xe5\xad\x99\xe6\x9d\x83'
      ]}},
       b'quizNum': 1, 
       b'nextStateBeginTime': 1547907141, b'battleState': 3
}}

似乎答案和问题都在这条数据里面,并且还告诉了客户端下个问题开始的时间。
修改代码解析看看

2.解析数据

    def websocket_message(self, flow: mitmproxy.websocket.WebSocketFlow):
        """
            Called when a WebSocket message is received from the client or
            server. The most recent message will be flow.messages[-1]. The
            message is user-modifiable. Currently there are two types of
            messages, corresponding to the BINARY and TEXT frame types.
        """
        content = flow.messages[-1].content
        content = gzip.decompress(content)
        if len(content) == 0:
            return
        try:
            msg = msgpack.loads(content)
            cmd = msg[b'cmd']
            if cmd != b'Resp_Sync':
                ctx.log.info("## cmd:%s" % (cmd))
                return
            seq = msg[b'seq']
            body = msgpack.loads(msg[b'body'])
            if b'battleInfo' not in body:
                ctx.log.info("battleInfo is None")
                return
            quizMap = body.get(b'battleInfo', {}).get(b'quizMap')
            if not quizMap:
                return
            for k in quizMap:
                num = k
                quiz = quizMap[k]
                if b'className' not in quiz:
                    continue
                className = quiz[b'className'].decode()
                categoryName = quiz[b'categoryName'].decode()
                question = quiz[b'quiz'].decode()
                answers = {}
                for index in quiz[b'answers']:
                    answers[index] = quiz[b'options'][index].decode()
            ctx.log.info("## class:%s  category:%s" % (className, categoryName))
            ctx.log.info("## question:%s " % question)
            for index, a in answers.items():
                ctx.log.info("## %d:%s" % (index+1, a))
            # ctx.log.info("## bdoy:%s" % (str(body)))
        except Exception as e:
            pass
            # ctx.log.info("## msg:%s" % (str(msg)))
    

不出所料:

## class:世界  category:生活
## question:《汉谟拉比法典》是谁颁布的?
## 2:古巴比伦国王
## cmd:b'Fight_BattleChoose'
## cmd:b'Notify_Choose'
## cmd:b'Notify_Choose'
## cmd:b'Notify_Choose'
## cmd:b'Notify_Choose'
## cmd:b'Notify_Choose'
## class:文学  category:文化
## question:「东风不与周郎便」的下一句是?
## 0:铜雀春深锁二乔
## cmd:b'Fight_BattleChoose'
## cmd:b'Notify_Choose'
## cmd:b'Notify_Choose'
## cmd:b'Notify_Choose'
## cmd:b'Notify_Choose'
## cmd:b'Notify_Choose'
## cmd:b'Notify_Choose'
## class:人物  category:流行
## question:马云在创业之前从事过什么正式工作?
## 1:教师
## cmd:b'Fight_BattleChoose'
## cmd:b'Notify_Choose'
## cmd:b'Notify_Choose'
## cmd:b'Notify_Choose'
## cmd:b'Notify_Choose'
## cmd:b'Notify_Choose'
## cmd:b'Notify_Choose'

哈哈,实测,在上道题还没有结束的时候就已经把下道题的问题和答案都拿到了。

0x03.结束

试了几局,妥妥的MVP啊,对手一定是以为遇到了机器人。不过这样玩意义就不大了。
把过程分享出来大家一起讨论学习一下。

好了,不能玩游戏了,看代码学习去。。。。

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

推荐阅读更多精彩内容