前两天和同事聊天,同事A说,同事B也在玩羊了个羊啊!当时的我一脸懵,羊了个羊?是个啥?

羊了个羊?

可能我不太关注微博,从别人那儿才听说到这个游戏,那我必须紧跟潮流啊!于是我就打开微信小程序搜了一下《羊了个羊》这个小游戏。不得不说,这游戏真上(xia)头。刚打开的时候,我心里想的是:“就这?”,然后进入第二关,一开始还没觉得什么,直到后面,我感觉到了不对劲。大概刷了七八次,我就放弃了。然后过了一个小时…不行,我今天一定要把它通了!大概又刷了七八次,这什么破游戏!坑爹呢!

说实话,这个游戏,一开始我是看不上的,我认为这个游戏的真正厉害的地方,在于方块堆叠的设计,看似没有多少层,但是扒开一层,却发现还有下一层,当你觉得你越来越接近最后一层的时候,实则还在差的老远了;另外,看似随机打乱的方块,实则也需要精打细算每一步。我不知道真正通关这个小游戏的人有多少,但至少,我是没那个耐心的,大家尽可放心,不能通关这个游戏,只能证明这个方块的随机性和摆放的设计非常巧妙,并不能证明智商高低,至少从程序的角度来看,无非就是随机打乱而已。

直到后来,无意中看了别人的通(po)关(jie)教程,才发现原来这个游戏,还有另一个玩法

漏了个洞?

老实说,虽然我是做服务器的,但是在我印象中,认为微信小游戏还是相对比较安全的,毕竟有微信的加持,小程序的代码加密,混淆,加壳之类的安全保障应该会有吧?服务器端的话,接口的发送频率,幂等性判断,token校验,重发包,数据防护,包体加密等的防护也应该会有吧?可万万没想到的是,我在网上刷视频的时候,竟然也会刷到这类的破解视频,而且破解刷分的原理,也是简单到离谱。我猜测,可能人家小团队,也没有想到这么小的游戏会有这么高的热度?
吐槽归吐槽,刷分还是要刷的,于是我也小小的操作了一下,让自己在朋友圈霸个榜

本地修改

这是我第一个看到的破解方式,方法简单到离谱。几年前,我也用cocos写过一些微信小游戏,赚了一些小零花钱,虽然过去了一段时间了,但也还记得一部分。这个破解方法,也让我一眼看出来它是cocos写的。
这个方法有一个局限性,它必须在电脑端的微信操作

简单描述一下步骤吧:

1.打开微信目录

首先你需要打开你的微信文件目录,从设置->文件管理->打开文件夹,就可以打开你的微信目录,然后找到Applet目录,这是微信小程序的应用目录,把它删掉(排除其他应用的干扰)
applet

2.找到小程序目录

从电脑端的微信上正常打开羊了个羊的小游戏,并正常游玩第一关,这个时候,你就会发现,你的Applet目录里又有了一个目录,文件名看不懂没关系,那是小程序的appid
applet

3.找到资源目录

进到它下面的usr\gamecaches\resources目录,这是这个羊了个羊这个小游戏的资源目录,你会发现下面有一堆资源,我们要修改的,就是其中一个json文件。
乍得一看,好像全是无规则的数字命名,实则对于程序员来说再熟悉不过了,这个数字一看就是时间戳,而我试了一下,拿去一转换,也确实是时间戳,和文件的创建时间也刚好对上。
在微信小程序中,有个对包体大小的限制(我印象中好像挺小的),所以游戏里大部分资源都是微信的cdn服务器拉取的,而拉取下来的时间,也正是资源的文件名。

resource目录

4.修改关卡文件

以上还是正常操作,但接下来的操作就很巧妙了,这么多文件,我们修改哪一个呢?
做游戏开发的程序员应该比较清楚,游戏里的大部分静态资源,我们都是会交给策划去配置的,就算是我们程序自己定义的一些静态数据,我们也会习惯性的把它们放在资源分类中,这样能比较好的做到代码和数据的分离。这样做的好处就是我们能在不修改代码的前提下,修改资源文件来改变游戏的逻辑。比如我们接下来要修改的关卡配置
老实说,我挺佩服第一个发现这个修改关卡配置的人的。好在羊了个羊的配置文件不算多,当然了,png和mp3我们都不用看了,这类都是图片和声音的资源,我们主要关注的是json文件,在程序开发中,json是比较常见的一种配置文件格式。我们大致浏览一遍,会发现很多“cc.spriteFrame”,“cc.jsonAsset”,“cc.AutoClip”等关键字,这也是让我断定它是cocos开发的原因。

我看的破解教程中,说的方法是把文件按大小排序,找到2kb大小的那个文件,就是关卡配置文件。老实说,这种方法有点投机取巧,万一这个游戏后期修改配置,文件变大了或者变小了,那这个方法就不再准确了,当然了,这也是小白最容易理解的方式。
其实只要我们看到这个文件的里有一个关键字“levelConfigData”,就大致能明白这是一个关卡配置了。
json文件

再然后,就是另一个让我佩服的点了,第一个人是如何发现它应该怎么修改的?levelConfigData里有dailyLevel和topicLevel两个大类,这个很容易理解,会一点英文应该都能猜到它应该是每日关卡和话题关卡两个类别。再然后,我们看到两个大类的数组中的前一个数字都是一样的,这个按照我的理解应该是关卡id,而后面那个数字,我猜测应该是每个方块的id,也就是说,这里有多少个数组,就有多少个类型的方块。而真正把这些方块排序的,应该是代码中随机实现的。真正让我佩服的是,教程里说,把后面的数字改的全部和前面的一样,像这样:
image-1663481102311

这样一来,游戏里就只会有一个方块id了?

5.正常通关

实际进入游戏之后,游戏虽然不是全部方块一样,但是第二关就和第一关一模一样了。但诡异的是,正常打的话,一共就两关,这样修改之后,需要通关四关才能通关(都是第一关的难度),连发现这个修改方式的本人也不知道是什么原因,我猜测是这样的修改触发了小程序的奇妙bug导致的。
不管怎么说吧,这样修改也确实能让人轻松简单的通关,并且也算是正常通关,还能获得皮肤,并进入羊群。
加入羊群
加入羊群
加入羊群

直接通关

本地修改尚且需要修改文件,并且还要手动玩一下才能通关,接下来这个漏洞,就稍显严重了。
微信小程序本质是基于html+js的h5应用,只不过经过微信的一层封装,摇身一变,成了微信小程序。它的本质仍然是h5应用,既然是h5应用,那么请求和获取服务器的数据就一定是通过http请求。http的抓包工具又遍地都是,所以我们很容易就能抓取到我们在玩羊了个羊的过程中的所有请求数据。
如果我们通关一次羊了个羊,并进行抓包,就会发现,整个游戏过程都是单机进行,并且在游戏结束后向服务器请求了一个接口,这个接口名字叫做game_over。这就相当于,我们在自己手机上通关之后,再告诉服务器“我通关了”,服务器再记录下你的通关信息。

既然这样,我们岂不是可以省去手机上通关的过程,直接告诉服务器“我通关了”

还真就能这样!甚至直接使用get请求就够了!通过抓包我们可以看到,它的通关请求里,实际上是有带有一些参数的,为了不让服务器发现异常,我们也无需修改这些参数,只要让它帮我们记录通关数据即可。但是这些参数里并没有玩家相关信息,那么它怎么知道我是谁的呢?

这一点还算稍微有一点门槛,“有一点门槛,但不多”。在web开发中,最常用到的一个表示用户身份的东西叫做token,这个东西是一个临时身份认证,就像一个临时身份证,在它失效前,你用这个临时身份证,就可以代表你是谁,一旦过期,那么我就不认识你了。在羊了个羊这个游戏中,也有一个用户token,这个token是由微信给你分配的,不过它并没有放在请求参数中,而是藏在了http请求的header中,所有的请求的header中,都有一个叫t的值,它甚至不用token来作为变量名。不过看着这个乱序的字符串,大致也能猜出来, 它就是token。

token
token

而通关这个接口,就需要带上这个“t”的值。这样一来,服务器就能记录上你通关了。而这个接口,你请求几次,服务器就给你通关几次

如果你会写点代码,你肯定不会满足于此,既然能随便通关了,我能允许它只通关一次两次?

于是,我写了一个很简单的脚本,就是连续请求通关接口,并把这个脚本扔在服务器24h跑,我看谁还能超过我?

import random
import time
import sys_logger
import requests


def req_game_over(_token: str):
    url = "https://cat-match.easygame2021.com/sheep/v1/game/game_over"

    _time = random.randint(500, 1000)
    querystring = {"rank_score": "1", "rank_state": "1", "rank_time": str(_time), "rank_role": "1", "skin": "1"}

    headers = {
        't': _token,
        'cache-control': "no-cache",
    }

    response = requests.request("GET", url, headers=headers, params=querystring)
    sys_logger.logger.info(response.text)


if __name__ == '__main__':
    tokens = [
        'token1',
        'token2']

    while True:
        for token in tokens:
            try:
                req_game_over(token)
                time.sleep(1)
            except ConnectionError:
                sys_logger.logger.info("server err")

        _sleepTime = random.randint(3, 5)
        sys_logger.logger.info("sleep " + str(_sleepTime) + "s")
        time.sleep(_sleepTime)

其他破解

以上两种是我见到的最常见,操作也相对简单的破解方法。其实还有其他的破解方式,我就没做太深入的了解,因为大致原理其实是差不多的。比如有看到说能修改小程序的js代码,拥有无限道具的。甚至有能通过接口修改其他任何人的省份的(如果你发现的归属羊群一直在变,很可能是别人在反复修改你的定位)。
总的来说,方法都大同小异,关键在于游戏本身有没有对这些安全性做保护。

反了个思!

玩笑归玩笑,娱乐归于娱乐。看见别的bug,我们总是很开心的,但是我们也要时刻通过这些bug反思我们自己。如果是我,我要如何防范这些破解和刷分?
从羊了个羊这个游戏来看,对这个游戏的破解无非就分为两大类:本地修改客户端,和伪造消息给服务器

本地修改客户端

就算是说破了天,单机游戏想要破解都只是时间早晚的问题。别说微信小游戏了,主机游戏甚至游戏主机,都难逃命运,不是破不了,只是时间成本的问题。但我认为,我们还是可以通过以下方式来做安全防护:

1.资源文件加密处理

这也是我用到的第一个修改方式——修改关卡配置,但是如果配置文件本身做加密处理,或者打包成二进制文件等,机器认识,人不认识,那么也就很难做修改了。

2.内存防护

我之前项目有用到过腾讯提供的客户端加壳保护,一旦发现你通过内存修改器修改内存等违规操作,保护程序会立即杀死程序来做自我保护。以前我玩腾讯PC游戏的TP子程序,大致也是这个作用。

3.服务器校验

一般来说,就算是单机游戏,我们最好也需要有服务器来进行校验。
往简单了说,我们至少可以通过分析玩家的行为,比如通关时间过短,通过次数过多,或者点击次数过快等分析出玩家的异常行为。
往复杂了说,我们把单机改成服务器运行逻辑,或者单机和服务器分别运行逻辑,服务器来做数据校验。
总之,服务器参与的越多,本地修改数据的可行性越低

伪造消息包

这种方式需要抓包客户端和服务器的交互数据。一旦玩家发现了交互逻辑,则可以通过伪造相同的消息包来达到相同的效果。而这种方式,我们需要在服务端做好各种非法请求的校验。
从通用点来说,我们可以通过接口请求频率,ip访问限制,参数篡改校验等拦截一批非法请求。
从逻辑点来说,我们依然可以通过玩家的异常行为,来拦截玩家非法请求。

一键通关

一键通关的接口随意发送通关消息,就能无限通关,它的保护做的是远远不够的。从游戏逻辑上来说,羊了个羊每天是只能通关一次的,至少从这一点来说,如果对每天通关次数有拦截的话,就算是伪造消息,也只能一天通关一次;其次,重复伪造消息就需要重发消息包,只要对消息包加上时间戳或者事物id,保证同一个包只能请求一次,这也能拦截一大批重发的伪造消息包。

修改任意玩家地理位置

另外需要重点说的是,能通过接口请求修改其他人的定位地址,绝对是属于很严重的漏洞了。它的原理是直接请求修改地理位置的接口来达到修改玩家定位的效果,但是致命的是,它只需要传玩家的uid,就可以修改对应玩家的定位,但偏偏游戏大厅能请求到所有玩家的uid。这样的话,修改任意玩家的定位就显得十分容易。在这个接口中,最基础的,我们不能泄露玩家的关键信息在公共接口中,必须保证请求这个接口的一定是玩家本人;其次,修改地理位置这个操作理应也做一些限制,比如必须一天或一周或一个月才能修改一次。

Q.E.D.