EN…最近项目做完,然后心血来潮,找了一个老游戏玩一下,是的,就是扫雷,然后发现吧,这游戏要是可以一键完成那就好了。主要还是为了练习一下逆向
效果演示
本文主要是分享逆向的思路,技术难点其实不难,主要是思路
目标地址在这里:扫雷游戏
我们的目标 ———— 实现一键完成游戏
目标细分
- 找到入口
- 找到需要的功能模块
- 调用现有的模块实现我们需要的功能
整个是这么的结构,记得把事件监听器的祖先去掉,剩下的就是跟自己相关的事件
在此处下断点,为什么?
因为我们不知道作者是通过哪个来触发雷区的,所以要判断他是通过onmousedown事件来处理触发雷区,还是通过鼠标的onmouseup来触发雷区,然后思路就是要去跟触发雷区前的那个事件,因为你出发完了之后,你就跟不到她调用的堆栈了,调试起来就很难了。
可以看到在onmousedown断点的时候,地图没有被标记颜色,但是onmouseup的时候,是已经处理完了,所以我们的入口点就在onmousedown,顺着这个继续往下跟。
开始调试分析
随便点击一个位置,单步跟进去,然后看到这里又几个判断,在每个判断里面下断点
var k = Math['floor']((g['clientX'] - h['left']) / 25)
var l = Math['floor']((g['clientY'] - h['top']) / 25)
上面这两行代码,获取到的是当前点击的方块坐标,这个我们先记录一下,单步继续往下
o0o(k, l)
最后走到了这一步,说明这个函数是一个关键的函数,如果不确定可以多验证几次,继续往里走
可以看到,这个函数是一个递归调用,我们先把其他断点去掉在以下几个地方打断点
然后刷新一下页面,这里我们不单步调试,直接执行,为什么,主要是看他对canvas在干什么,因为你们看‘drawImage’,这个基本就是对canvas的操作,如果你一开始就直接单步调试的话,木有目的性,效率太慢了,
可以看到下图演示,通过前面单独点击和这里初始化的效果,可以判断出,这个函数是每次点击都会操作的,里面肯定有我们需要的东西
我们先把断点去掉,然后让他初始化完毕,再重新打上断点,来单步调试一下这个的作用。
我们先点击一个有雷的区域,会进到下面这个函数
f17()
在他的上一行,是一个给canvas绘图的方法
我们验证一下,改成gfs[1],点是雷块的时候,就变成了标记旗帜,我们记录下这个方法
ctx['drawImage'](gfs[1], k * 25, l * 25) // 可以给块绘成旗帜图案
并且当 “d31[l][k][1] == 1” 的时候会进到进到这个判断,判断是一个雷区,这一步我们找到了雷的判定规则
我们看一下d31里面是什么东西,这里我们自己写一个hook代码,去hook他,也可以自己改源码来打印结果
这里我们自己写一个代码,我们利用目前获取到的信息,将所有雷的区域标记出来
function gameHook () {
for (let l = 0; l < Y; l++) {
for (let k = 0; k < X; k++) {
if (d31[l][k][1] == 1) {
ctx['drawImage'](gfs[1], k * 25, l * 25)
}
}
}
}
保存好代码,然后保存,刷新页面,然后再控制台执行我们自己的代码,看到了吧,这些就是有地雷的区域,你也可以自己点击验证一下,验证完了之后,记得把你刚刚改的 gfs[1] 改回原来的值,原来是 gfs[2],这样不会干扰到我们后面的调试
到了这一步,我们还需要解决,当他标记出来之后,我们还需要给他的雷计数赋值,这里的数量是没有变化的,所以我们还要找他的赋值功能。
通过前面,我们知道,ctx[‘drawImage’](gfs[1], k * 25, l * 25)这个的功能是标志旗帜的功能,扫雷这个游戏,他一般只有标记的时候才会出现旗帜的符号,我们全局搜一下这个方法
他在这两个地方,出现,我们在这里下两个断点,然后我们在canvas上右击标记,看下他是怎么运行的
右击标记之后,是进到了这个函数,我们单步往下走
进到crm(–RM)处,会有一个闭包,这里我们不管,直接在最后下断点,然后运行,可以看到,雷的数量就少了一个,所以我们先不用管里面的细节,直接把这个方法拿来用
crm(–RM)还有一个RM,这个一看就肯定是雷的数量,要是不确定,可以自己去验证一下。然后根据这个特性,我们继续完善一下自己的程序,保存更新,刷新页面,运行函数
function gameHook() {
for (let l = 0; l < Y; l++) {
for (let k = 0; k < X; k++) {
if (d31[l][k][1] == 1) {
ctx['drawImage'](gfs[1], k * 25, l * 25)
if (RM) {
crm(--RM)
}
}
}
}
}
下图可以看到,运行最新的程序之后,他的计数已经全部为0了,此时基本算是完成了找雷和给雷计数的操作,还剩下一个操作就是把没有雷的块也要点开以及让游戏完成
这一步,将不是雷的块全部点开,保存代码,刷新运行,可以看到下面的效果
function gameHook() {
for (let l = 0; l < Y; l++) {
for (let k = 0; k < X; k++) {
if (d31[l][k][1] == 1) {
ctx['drawImage'](gfs[1], k * 25, l * 25)
if (RM) {
crm(--RM)
}
} else {
ctx['drawImage'](gfb[d31[l][k][2]], k * 25, l * 25)
}
}
}
}
最后我们要让成功的笑脸出现,还记得我们之前调试的时候,点击雷区,出现一个ends的方法么
这个方法应该是一个游戏结束的方法,我们去看一下有几个地方调用了他,能调用他的地方,有以下几种情况
- 踩到雷区了
- 游戏通过了
我们踩到雷的地方已经找到了,就还要找游戏通过的时候,全局搜一下方法
可以看到,被d64和scs函数调用了,以及我们刚刚的f17,我们主要关注d64和f17,分别在这两个里面下断点
当我们清空的时候,首先会进到d64的函数中,那么说明他这个是初始化的时候调用的,而且从代码上看,他里面是在初始化一些事件,由此可以确定,这个不是我们要找的,那就只剩下一个scs的调用了
我们只留下一个scs的断点,然后自定义一个数量,然后玩一下游戏,让游戏成功完成,看他会不会进到我们预期的断点里面
当我们把最后一个块点掉之后,顺利进到了断点位置,可以确定,这个位置就是我们需要的
然后我们分析一下他的源码
有两个地方是会出现alert,而且第一个的判断是当他不为雷的时候,就提示,以及当总雷数RM不为0的时候提示,那么我们只需要把提示后半部分提取出来就行,我们不考虑其他,修改我们自己的程序就是这样子,保存刷新页面,然后运行
function gameHook() {
for (let l = 0; l < Y; l++) {
for (let k = 0; k < X; k++) {
if (d31[l][k][1] == 1) {
ctx['drawImage'](gfs[1], k * 25, l * 25)
d31[l][k][0] = 2
if (RM) {
crm(--RM)
}
} else {
ctx['drawImage'](gfb[d31[l][k][2]], k * 25, l * 25)
}
}
}
crm(0)
$('face')['src'] = gif[1]
up()
}
最后的成果演示
本文主要是提供分析思路,技术难点上的话乜有太多,都是在CV,有什么不懂的地方可以交流,今天就到这里啦