最近和老婆迷上了頭腦王者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,看到下面的界面說明代理設置沒問題了
點擊對應的平臺下載證書安裝。然后在系統中設置信任,我就不多說怎么設置了
試一下訪問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啊,對手一定是以為遇到了機器人。不過這樣玩意義就不大了。
把過程分享出來大家一起討論學習一下。
好了,不能玩游戲了,看代碼學習去。。。。