HZW 某游戏作弊修改分数问题解析

【注意:此文章为博主原创文章!转载需注意,请带原文链接,至少也要是txt格式!】
某个游戏活动(游戏早已废弃不存在,所以公开),通过小程序或者APP扫描二维码进入游戏,过程中发现,这游戏其实就是一个H5页面,地址是:
https://w.baidu.com/game-football/index.html?naviType=4
游戏作弊访问方式解析
通过微信小程序、APP都可以正常玩这款游戏,但是如果通过浏览器打开,则提示“请在小程序或APP中打开”,既然有提示就好办了,我们通过搜索发现,真正校验名字是checkRunEnable
:
这里既然是一个判断函数,那么看看isHzwEnv(),我们直接跟进,如下图:
从这里可以看出里面有一个“或”的判断,他们俩任意为真则可以继续。this.isHzwApp() || this.isWechatMP()
到这里就比较简单了,跟进一下这两个任意一个函数即可。为了方便,直接在isHzwApp这个函数里面加一个条件,让i、e、t都满足条件为真即可。
这里的判断逻辑就过去了,然后可以通过网页玩此H5游戏了,这里最好还是要找一下具体引用checkRunEnable
此判断逻辑的点,方便后续游戏作弊分析,具体引用点如下图:
其实只要把所有引用的点都加入“断点”即可,这个还是比较容易的。后续的判断比较简单,就是判断还可以玩多少次,这个游戏有限制次数。不过这个都无所谓了。然后就是进入startGame()函数里面,开始游戏。
分析游戏结果处理逻辑
然后开始玩游戏,过程中一直在抓包,这个游戏并没有采取websocket的时时获取数据模式,游戏过程中也没有进行数据交互,那么最终玩完游戏时才提交的数据包,具体如下:
通过上面的分析,加上这个数据包的提交,可以得出结论,游戏肯定可以作弊,因为所有的数据都是本地生成,最终加密提交到服务端。那么也就是说,我们可以通过多种方法作弊:
第一:hook关键点,在加密之前修改游戏结果,然后放行,系统加密修改后的数据传到服务端;
第二:找到整体加密方法,分析加密逻辑,然后自行写作弊脚本。
整体来说,第一个方法简单,第二个方法耗时耗力比较难。
既然写文章了,那么就两个方法都来一遍!!!
hook关键点,修改游戏结果作弊 (简单)
既然从本地提交数据到服务端,必然有提交入口,通过提交数据包,可以找关键点,例如关键词rank/join
这就是一个,通过搜索最终发现结果如下图:
提交的结果是e
,那么,从图中也可以明确的知道,此提交数据入口在函数join
中,e
的值也是清晰可见
var e = {
t: r.aes.aesGameScore(t)
};
这就很明确了,直接告诉你是用的aes加密的。 从图中也可以明确知道,应该是有一个数据结果t
,此结果放入r.aes.aesGameScore(t)
函数中,先hook一下看看入参t
具体是什么。在这里hook一下断点,然后玩游戏,结果如下图:
很明显,这个t
承着的就是游戏结果(游戏时长),那么可以直接通过修改此时t的值,然后放行,即可完成游戏作弊。如下图:
然后放行,然后如下图:
最终我们得到游戏的加密结果0929651bf9b6f18c3fe5983ebc95c99403811395504088afd971fcd66392deccf191064b497716bdf0609bb627a37cd8515b1def190fc81242db51403aca9ff5f93325a17a609445e15cdb1bbe63197d
,这也是最简单的作弊方式,到此也算是作弊完成。最终完全放行即可。
分析游戏加密逻辑,制作游戏作弊脚本 (较难)
如果仅仅是为了游戏作弊,那么完成上述步骤即可,但是本着技术领域的探索,可以继续深究。既然aesGameScore
函数就是加密游戏结果的,那么跟进此函数。具体代码如下:
execute: function() {
function i(t, e, r) {
var i = n
, c = i.enc.Utf8.parse(e)
, s = i.enc.Utf8.parse(r);
return i.AES.encrypt(t, c, {
iv: s,
mode: i.mode.CBC,
padding: i.pad.PKCS7
}).ciphertext.toString()
}
t({
aesEncrypt: i,
aesGameScore: function(t) {
var e = parseInt(((new Date).getTime() / 1e3).toString())
, n = (Array(7).join("0") + Math.floor(t).toString()).slice(-6)
, c = e + n
, s = r.cookie().uid
, o = (s.toString() + Array(17).join("0")).slice(0, 16)
, a = {
uid: parseInt(s),
timestamp: e,
score: Math.round(1e3 * t)
};
return i(o + JSON.stringify(a), "KcghK8WtBefxP9da", c)
}
}),
e._RF.push({}, "b5c6b/CEFRKA6jsC8yvqFIG", "AES128", void 0),
e._RF.pop()
}
这里就需要一步一步的对加密函数进行分析。
e = parseInt(((new Date).getTime() / 1e3).toString())
此步骤是获取当前时间戳,舍弃小数点后面,仅保留整数。
n = (Array(7).join("0") + Math.floor(t).toString()).slice(-6)
取游戏结果整数,舍弃小数点后面,如果不足6位前面用0补齐。
c = e + n
组合时间戳及游戏结果。
s = r.cookie().uid
从cookie中获取当前用户uid。
o = (s.toString() + Array(17).join("0")).slice(0, 16)
取用户uid,不足16位后面用0补齐。
a = {uid: parseInt(s),timestamp: e,score: Math.round(1e3 * t)};
组合所需加密数据为字典格式。
i(o + JSON.stringify(a), "KcghK8WtBefxP9da", c)
最终把数据再次进行组合,放入加密函数i中。
这里有一个点需要注意,就是加密结果.toString()
需要通过JS变为字符格式,如果要是用python写的话就是hex
具体经过分析数据结果如下:
------------------------------
①
1669797575 时间戳
-----------------------------
②
t = 2.2731666666666657 ##这里仅取小数点后3位
(Array(7).join("0") + Math.floor(t).toString()).slice(-6)
000002 游戏分数取整值,取小数点当前分数6位
-----------------------------
③=①+②
1669797575000002 偏移量(时间戳+游戏分数取值)
-----------------------------
④
1234567890 uid
-----------------------------
⑤
s.toString() = 1234567890
(s.toString() + Array(17).join("0")).slice(0, 16)
1234567890000000 用户uid补全数据
------------------------------
⑥=⑤+{uid: ④, timestamp: ①, score: 2273}
最终需要加密的数据格式:
1234567890000000{"uid":1234567890,"timestamp":1669797575,"score":2274}
通过断点也可以看到,如下图:
由此可见,分析结果与预期一致,那么下一步就是根据加密逻辑,写作弊python脚本。
具体脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | # -*- coding:utf-8 -*- # __author__ = xieyunlong # __Blog__ = https://woj.app # __Date__ = 2022/6/10 # __ver__ = python3 # !/usr/bin/env python3 # -*- coding:utf-8 -*- # AES/ECB/PKCS7Padding 加密解密 # 环境需求: # pip3 install pycryptodome from Crypto.Cipher import AES from Crypto.Util import Padding from base64 import b64encode, b64decode from binascii import b2a_hex, a2b_hex import time import requests def aes_ecb_pkcs7_b64_encrypt(data, key, iv): data = Padding.pad(data.encode('utf8'), AES.block_size, 'pkcs7') aes = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8')) return b2a_hex(aes.encrypt(data)) def aes_ecb_pkcs7_b64_decrypt(data, key): data = b64decode(data.encode('utf-8')) aes = AES.new(key.encode('utf-8'), AES.MODE_CBC) return Padding.unpad(aes.decrypt(data), AES.block_size, 'pkcs7').decode('utf-8') def connect_data(end_time, _game_souce): key = "KcghK8WtBefxP9da" time_ux = int(end_time) # 游戏时间戳 print(_game_souce) # 游戏分数默认6位,最多9位,不够前面用0补齐,后3位为小数点,2.123秒=002123,123456秒=123456000,后3位0是毫秒 game_souce = '{:.3f}'.format(_game_souce) # 必须是浮点数,小数点后面必须有值 if '.' in str(game_souce): game_souce = game_souce.split('.') game_souce_1 = game_souce[0] if len(game_souce_1) < 6: game_souce_3 = '{:0>6d}'.format(int(game_souce_1)) else: game_souce_3 = game_souce_1[-6:] # game_souce_2 = game_souce[1][:3] game_souce = '{}{:x<3d}'.format(game_souce_1, int(game_souce[1])) # 这里int(game_souce[1]) 可以更换为game_souce_2 iv = '{}{}'.format(time_ux, game_souce_3) print(iv) user_uid = 1039666656 ##用户uid user_uid_all = "{}000000".format(user_uid) # 用户uid补全 data_connect = '{user_uid_all}{{"uid":{user_uid},"timestamp":{time_ux},"score":{game_souce}}}'.format( user_uid_all=user_uid_all[:16], user_uid=user_uid, time_ux=time_ux, game_souce=game_souce) print(data_connect) t_data = aes_ecb_pkcs7_b64_encrypt(data_connect, key, iv) return t_data session = requests.Session() start_time = time.time() headers = {"Origin": "https://123123.xxxx.com", "Sec-Ch-Ua": "\"Google Chrome\";v=\"107\", \"Chromium\";v=\"107\", \"Not=A?Brand\";v=\"24\"", "Accept": "application/json, text/plain, */*", "Sec-Ch-Ua-Platform": "\"Android\"", "User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Mobile Safari/537.36", "Referer": "https://123123.xxxx.com/", "Connection": "close", "Sec-Fetch-Site": "same-site", "Sec-Fetch-Dest": "empty", "Accept-Encoding": "gzip, deflate", "Sec-Fetch-Mode": "cors", "Accept-Language": "zh,zh-CN;q=0.9,en-US;q=0.8,en;q=0.7", "Sec-Ch-Ua-Mobile": "?1", "Content-Type": "application/json;charset=UTF-8"} cookies = {"uid": "123123", "hserecomkey": ""} paramsGet = {"sn": "XMTD5KXVRHUXE7dz"} # 第一步,记录游戏开始时间 response = session.post("https://123123123.xxxx.com/api/v1/rank-reward/count/reduce", data='null', params=paramsGet, headers=headers, cookies=cookies) print(response.json()) # 第二步,玩了多久就等多久 time.sleep(678) ##需要游戏多数分 这里就休息多数秒 end_time = time.time() # 处理结果并加密 t_data = connect_data(end_time, end_time - start_time) # rawBody = {"t":t_data.decode('utf-8')} rawBody = '{{"t":"{t_data}"}}'.format(t_data=t_data.decode('utf-8')) print(rawBody) # 第三步,提交加密数据 response = session.post("https://123123.xxxx.com/api/v1/rank-reward/rank/join", data=rawBody, params=paramsGet, headers=headers, cookies=cookies) print("Status code: %i" % response.status_code) print("Response body: %s" % response.json()) |
布施恩德可便相知重
微信扫一扫打赏
支付宝扫一扫打赏