After unarchiving the password protected .7Z, we end up with a Windows x86 binary:
File Type: PE32
Magic: PE32 executable for MS Windows (GUI) Intel 80386 Mono/.Net assembly
Size: 1.1 MB (1190912 bytes)
After unarchiving the password protected .7Z, we end up with a Windows x86 binary that's compiled with .NET. This adds a layer of complication as IDA pro doesn't handle this easily. Taking a quick stab at the file dynamically we can see it's a 30x30 minefield that doesn't take kindly to random mine clicks!
Quickly realizing it was time for the static analysis route, I decided to use dnSpy as it's an excellent .NET decompiler. I don't deal with a ton of .NET binaries, but when I do this tools is fantastic because it has a great looking GUI, super easy to use, allows for on the fly code changes/re-compilation, debugging and it's free!
Similar to a non .NET compiled PE file the goal is to figure out where our code begins executing or where MAIN is, this is called MainForm in .NET. Following the 3 steps below will you there. You can also click on the very top parent entry for UltimateMinesweeper (188.8.131.52) on the left most pane (assembly explorer) then click on UltimateMinesweeper.Program.Main and then Application.Run(new MainForm()); (line 15) and click on MainForm to get there.
Once inside main we can see 7 classes that we'll need to review than make up the core of the application. I've broken them down as:
- initialize an object
- create a new minefield object
- allocate that minefield object into memory
- assign the minefield to the controller
- set callbacks (for user interaction in the GUI)
- create a timer
- initialize flag count and list of revealed cells
After some poking around I figured inspecting the SquareRevealedCallback was a good enough place to start. Luckily Nick Harbour was nice enough to name things is a meaningful way, something that helped a lot. Once there I immediately saw SuccessPopup that needed this.GetKey that used revealedCells as a parameter.
I next went to the GetKey function and noticed that this was the "mathy" part of the challenge and ultimately where the key was created that I likely needed to solve the challenge. Looking at the top of the function we can see the revealedCells is passed as a list of unsigned integers to GetKey. It's then sorted on line 5 and passed to a new class Random which takes the first 3 values of the list from revealedCells. Next a bunch of math in the form of logic and array creation/manipulation is done and ultimately kicked out on line 52 -- return Encoding.ASCII.GetString(array2); where our flag will be revealed.
Ultimately there are many ways to solve this, I'd guess at least 5 and likely more. My approach was to work my way back through the program where I spent way too much time trying to debug SquareRevealedCallback, the only class that directly calls GetKey. My goals was to try to fall through to pass the initial IF this.MineField.BombRevealed loop.
After not getting where I needed to be I revisited MainForm to actually follow the code :) I figured that following the MineField class would be my best in hope of figuring out where the non-mines lived. Once there I saw a bunch of fields for minesPresent, minesVisable and some other objects like minesFlagged, TotalMines and BombRevealed. TotalMines looked like a great place to start but I ended up focusing on BombRevealed as a saw a logical comparison that grabbed my interest -- "return true" on line 132 below. After looking at this code, breaking here would give me the mines that were created as seen in memory.
To do this, I added a breakpoint on line 132 in the MineField class. I then hit F5/play to start debugging and hit OK.
The minefield will pop up and you will need to click a mine to stop on your breakpoint. Once there, navigate to the bottom local variables pane and navigate to this.minesPresent. If our theory is correct we'll have false values in at least three locations on our minefield. My working theory was that these matched up with the integer values we spoke about in GetKey.
After slowly scrolling through a ton of entries, I arrived at 3 false values for the following entries which are in the [row, column] format. [7,20], [24,28], [28, 7]. Could this finally be what I was looking for?
After some trial and error, I figured out that the minefield creation while 30x30 really started on 0 for counting purposes. This left me with 0 through 29 for my row and columns. Let's try some mine sweeper based on our false values.
Booooom. Yeah, it felt good even though no one cares.
Oh and I wrote some C# code around the GetKey function to solve this thing. I had to convert the 3 [row, column] values into integers. I did that by multiplying the row by 30 (since it's a 30x30 field) and adding the column value.
[7,20] row * 30 + column = 230
[28,7] row * 30 + column = 847
[24,28] row * 30 + column = 748
The rest you can see below in the C# code snippet. Not sure I've ever written C# before but this gave me an excuse. I started with text decompilation of GetKey and went from there.