最近和老婆迷上了头脑王者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啊,对手一定是以为遇到了机器人。不过这样玩意义就不大了。
把过程分享出来大家一起讨论学习一下。
好了,不能玩游戏了,看代码学习去。。。。