使用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啊,對手一定是以為遇到了機器人。不過這樣玩意義就不大了。
把過程分享出來大家一起討論學習一下。

好了,不能玩游戲了,看代碼學習去。。。。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容