众所周知,前端传过来的啥都是不可信的,所以正常情况都是前端数据加密、签名,后端解密、验签,这套逻辑确实能挡住一大部分的hacker,但是H5这玩意也套用这套体系的话其实挺尴尬的,因为前端数据处理的js逻辑是用户可见的。

即使你把代码混淆、加密、搞一堆花指令、甚至搞一堆异步调用导致调用堆栈花里胡哨,但是依旧可以通过各种下断点、跟堆栈找到核心的数据处理代码,也就是数据还是明文状态时的那个片区域,只要在那边下断点该数据,无论怎么加密、签名。数据它都会被认为是合法的。

而今天的主角是企鹅俱乐部重制版(Club Penguin Rewritten),一款热度并不怎么高甚至挺凉凉的情怀向游戏,游戏代码本身也确实有混淆,数据也有加密,但是抵不住浏览器调试呀。

目标的话自然是刷金币咯。

抓包

当然万事先抓包看看数据包有没有加密和签名,如果没有的话直接改。

file

数据包有加密,直接爆改数据包的思路可以打消了。

看控制台

先玩一局小游戏,看看控制台有没有一些相关的日志可以跟堆栈。

file

还真有日志,生产环境打日志虽然挺常见的但我个人感觉还是尽量减少为好,最好的做法是日志函数加个prod判断,非生产的情况才正常打日志。

其中getCoins()非常得吸引我的目光,毕竟是在Sent之前的金钱相关调用,很大概率就是我们的目标代码块,因为输出的数据都还是明文形式的。

不过跟过去发现是个日志函数,那么就有点麻烦了,因为给日志函数下断点跟堆栈的话,会有一堆不必要的中断去干扰程序的正常运行,但是没办法,只能尽量在小游戏结束的时候下断点,减少干扰吧。

file

成功在目标日志中断,尝试跟调用堆栈。

file

跟过去之后在函数开头下个断点,准备下回直接改数据,看看能不能成。

file

从0分改成12分,余额从419变成431,刷新重新进游戏看看余额变化是不是真的生效了。

file

余额成功变成431。

file

那么就可以想办法进行简化操作了,现在利用调用堆栈去跟到目标代码块进行修改比较繁琐,而且手动改比较累,那么可以利用Fiddler的自动回复功能直接插入js代码硬写入每次小游戏获得的金币数。

不过首先得定位到这个js是哪个文件来的,因为这个游戏是动态加载的js,没法直接定位到来源,所以去开发者工具里面搜一下特征代码,比如说'3|2|1|0|4'['split']('|'),成功定位到资源https://play.cprewritten.net/assets/client/system/RuffleManager.js?v=1.7.130-beta

file

那么浏览器手动访问一下这个资源,然后在Fiddler里面改代码就完事。

file

直接加一句_0x24442f.coins=65535;_0x24442f.score=65535;,把成绩改成65535,然后浏览器强刷一下这个资源,确保浏览器的缓存是咱们hack过的。

file

然后刷新游戏,再去玩一次小游戏,看看余额是不是+65535了。

file

file

大功告成。

一些想法

对于H5游戏开发,数据加密、签名是自然要的,代码的混淆处理也是必不可少的,但是一些细微的点也应当关注:

  1. 生产环境不打非报错日志,将运行日志以其他方式收集,如有必要再由用户上传至开发处进行bug分析。
  2. 反调试断点,虽然while(true)这种轮询会导致游戏性能下降,但是周期拉大一点还是可以的,不过这种也可以直接Fiddler自动回复给屏蔽掉相应代码,但有总比没有好。

但是服务器端也同样应当对前端传来的数据进行合法性判断,如:

  1. 小游戏可以前端限制最高分,若后端收到了比最高分还高的数据,那么直接数据丢弃并上报该账号。
  2. 一些不太复杂的非实时游戏,可以将游戏过程数据化后在结算时一起上传,然后服务器端进行验证,如果过程不合理也数据丢弃并上报该账号。这个思路在各种交互式的验证码上比较常见,比如说上传鼠标轨迹、用户操作时间间隔。
  3. 游戏的参与数据由服务器下发,如随机地图、随机种子等,并且配合实时的或结算时的用户操作数据上传,如果用户操作与参与数据的预期情况不一致则同2。

The End