re | buuctf逆向刷题之Ultimate MineSweeper全分析

写在前头

最近在buuctf上刷逆向题,做到Ultimate MineSweeper,这是一道用.NET写的扫雷题,题目不难,有类名和函数名符号,分析起来很容易,耐心一点都能找到flag,但我还是对这个题目很感兴趣,毕竟每个逆向爱好者都有一颗破解扫雷的心,我认真的把整个程序都逆了一遍,再加上目前在网上看到的解法都需要patch程序,所以我写了这篇笔记,展示了一种不需要修改任何程序的解法

观察程序

经典扫雷游戏,鼠标左右键控制,点到雷就炸

修改程序的解法

.NET程序,那就用reflector打开看看,一通点点点,找到一个可疑的GetKey方法,取了三个revealedCells进行处理获得buffer数组,再用buffer同bytes数组逐字节异或,返回ascii编码,很像啊

找到调用函数SquareRevealedCallback,是翻完方格的回调函数,大概意思是,如果翻到雷了,停止游戏,弹出failurepopup失败框,如果所有非雷区都被翻开,则弹出successpopup成功框,并且将上面生成的flag显示到框中

也就是说,只要游戏通关了,程序就会把flag打印出来,网上有无敌挂和透视挂两种做法
无敌挂,把弹出失败框的那部分代码删掉,点到雷游戏也不会退出,然后就无脑暴力点雷,将所有框都点一遍,游戏就赢了,弹出flag

透视挂,首先看一下进入赢分支的判断函数TotalUnrevealedEmptySquares,这个MinesVisible看起来像是表示可见区域的

查看该数组set方法的引用,发现在MineField初始化时,被全部置为了false,也就是不可见,那么如果在这里改为全部置true,变成全部可见,就可实现透视

透视挂效果

程序完整分析

不管是无敌挂还是透视挂,都是破坏了程序本身的逻辑,不讲武德的实现通关。但显然程序是有一套判断雷区的规则的,更具体点,应该是有一个数据结构用来存储雷区,如果能够通过动态调试的方法将这个数据结构给打印出来,那就可以直接拿到通关答案。因为题目比较有意思,所以我把整个程序逆了一遍,顺便找一下这个数据结构
首先,分析这六个最主要的类

SuccessPopup和FailurePopup,表示成功框和失败框的类
MineFiled用来表示方块区域的类,里面的MinesFlag、MinesVisible和MinesPresent,以及set\get方法,猜测是用来表示插上红旗区域、区域可见性和不知道啥的

MineFiledControl定义了用户操作的回调方法,里面最重要的是MouseClick这个方法,获取用户点击方块坐标,判断左右键,改变MineFlagged和MinesVisible这些状态变量,其中点击左键后调用了SquareRevealed,最终调用了上面的SquareRevealedCallback,进入是否点中雷区的判断

再看一下这个初始化函数,初始化了一个imageList,0->未翻面,1->炸弹,2->隐藏,3->插旗,4->安全

再看一下这个paint函数,每次点击以后回调,根据MinesVisible等状态变量重新绘制游戏区域,结合上面imageList编号对应的含义,现在可以确定,MinesVisible用来表示哪些区域可见,MinesFlagged表示哪些区域被插旗,MinesPresent用来表示哪些区域有雷,嗯这个MinesPresent要重点关注

MainForm这个类用来实例化MinesFiled和MinesFiledControl类,在其构造函数里注册了一些回调函数,游戏的主要交互逻辑都在里面

Program类,程序的主函数入口类,实例化了MainForm类

到这里,基本上把程序主要逻辑分析了一遍

不用修改程序的解法

已经确认MinesPresent就是用来表示哪些地方是雷区的结构,现在只要把MinesPresent取出来,就可以得到答案

MinesPresent在MinesFiled类初始化时被全部置为了false,那应该是在后续步骤中被重新赋值,通过静态分析交叉引用,没有发现别的地方调用了MinesPresent的set方法,虽然还不清楚具体是哪里赋的值,但是我们可以通过动调,找到一个确认已经赋值了的地方,把MinesPresent取出来,好,那就打开我的dnSpy

可以确定的是,在第一次点击方块前,MinesPresent已经被赋了值(否则无法判断是否点中雷区),所以我选择了FirstClickCallback函数,因为其位于MainForm类中,可以直接获取到MinesField变量的值,在这里下断点,程序跑起来,随便右击一个方块,程序断了下来,可以看到MinesPresent已经被赋了值

将这几列复制到excel表格,重新排列下,bingo,找到了总共三个非雷区,答案get

MinesPresent到底在哪赋的值

到这里就结束了吗?怎么可能,前面还留有一个疑问,没有找到MinesPresent被赋值的地方,那现在就来跟踪一下

由于没有找到其他对set方法的引用,那只能是通过其他方法对其赋值了,静态分析的思路到这里就断了

既然程序不长,那就从程序开始,一步步调试呗,观察啥时候MinesPresent的值发生变化了

前面说到,MineFiled初始化时对MinesPresent全置false,那我们从初始化后进行观察,首先是AllocateMemory函数,下个断点


可以看到,在调用该函数后,MinesPresent就被赋值了,第一把就找到了...

那点进去看看干了啥,num2是横坐标,num是纵坐标,对整个游戏区域进行了遍历,根据那个if条件判断是否置false

GarbageCollect函数,别被它善良的名字欺骗了,它的set方法才是我们一直找的真正赋值的地方,MinesPresent的set方法只是用来给个初值

看看它怎么判断的,DeriveVallocType函数如下,就是根据横纵坐标进行运算,判断是否等于某常量,是就置false

涉及到的常量

最后循环下来,只有[7,20]、[24,28]、[28,7]这三个地方被置了false,也就是游戏中被设置为无雷的只有这三处

收工

posted @ 2023-06-07 10:47  z5onk0  阅读(138)  评论(0编辑  收藏  举报