Reverse Engineering minesweeper.online To Electrocute Myself — And More

I had an insane Minesweeper phase where I could not stop binge-playing the game. I would literally see Minesweeper patterns in my head and imagine solving them when I wasn’t playing. I’ve since become less fanatical and now only play Minesweeper on occasion. In fact, my personal best (69 seconds on expert) was accomplished during a short return to the game, which tells me I’m far from reaching my true limit, so I’ll be back for more.

In any case, during my Minesweeper escapades, I started playing on a site called Minesweeper Online due to its exclusive “No Flag” mode, where you’re only given boards that you can always deterministically solve. The site is simple with regards to the modern web, but maybe a bit much for Minesweeper with all the collectibles and gems and events and whatnot. I actually wanted to implement my own Minesweeper with my own No Flag mode and solver for fun. I tried doing that until it was no longer fun. Also, shoutout to Minesweeper Online for giving me premium for life after reporting a minor bug. I appreciate you guys.

A couple of years ago, I had an idea to shock myself as punishment when I failed at something in video games, inspired from a video I watched. I thought it would be pretty funny to strap myself into a figurative electric chair while I played Minesweeper and shock myself whenever I clicked a bomb, and frame the entire thing as a “science experiment” on whether shock punishment was effective for as a conditioning mechanism for improving at Minesweeper. My friend and I jerry-rigged a dog shock collar with a Raspberry Pi to allow shocks to be delivered through an HTTP request on the Pi. I needed a Minesweeper client to hack around with so I could hook up clicking bombs to sending the request to the Pi, but the ones publicly available and easily findable and open source had some peculiarities that made it function slightly different from the way Minesweeper Online did, which I had gotten very used to. So, I figured I’d just mess with the Minesweeper Online browser client.

I found that the Minesweeper Online client communicated with the server via a websocket, but the message formats they used were incredibly cryptic. Fortunately, it wasn’t too hard to identify the packets that signified the game was over, but I still had to figure out how to intercept their message and tell the Pi to shock me. I was still kind of new to web development at the time, so after a good amount of searching and head scratching, I found a way to override the default WebSocket class in JavaScript by loading some code before the page loaded using Tampermonkey and forcibly added custom event listeners to every new instance of WebSocket to handle message received events. I must have been really proud of the code because I posted this code into a PasteBin and named it “fuck shit piss taser” for some reason. You don’t even have to know anything about programming to understand how cursed this code is just from seeing these lines:

This actually worked. With much reluctance from me and much goading from my friend, I did a few test runs with the dog shock collar on my leg. It didn’t take many fails to come to a conclusion for the science experiment. By shocking yourself when you fail, you actively want to avoid failure at all costs. I mean, we can joke about shocking yourself for fun, but if you’ve ever had a shock device strapped to you and you have the controls to it, your body actively refuses to press the button to actually go through with it, and it takes a moment of pure resolve — and idiocy — to override your survival instincts and press the button. So, in terms of improving performance, your pace grinds to a complete halt as you double-check and triple-check every single tile before you make a move. Maybe you could say that this improves your accuracy at the cost of your speed, but frankly, it just made me scared to play Minesweeper because I knew I’d still have to make guesses on some boards. Did I need to tell you all that? Hopefully not. But now you know 🙂

All of that was a little side story to bring us to where we are today. I’ve been learning iOS development with Swift recently and also wanted to improve my reverse engineering skills so I figured it might be a neat little project to make a mobile client for Minesweeper Online. While I haven’t made the mobile client yet, I have made very solid progress on reverse engineering the messages and I’ll be detailing them for my own reference as well as for educational purposes.

To start, the client requests a session id with a GET /authorize?session=. The server replies with JSON message with the following format:


    
{
    "authKey": "xxxx", // 128 bit hex string
    "session": "xxxx", // 128 bit hex string
    "userId": 1234,
    "user": {
        "createdAt": "2023-11-21T19:46:10.730Z",
        "country": "CA",
        "id": 1234,
        "hidden": 0,
        "premiumTo": null,
        "patronState": null,
        "storage": null,
        "hasVIP": 0
    },
    "geolocation": [
        lat,
        lng
    ],
    "serverTime": 1700595970731,
    "isNewUser": true
}

The session id is then stored on the browser. If we wish to retrieve these details again, we can just call the same endpoint and fill in the session id query.

Next, I believe the client uses the geolocation from the /authorize endpoint as well as a list of servers in some JavaScript to determine the closest server, usually with a name like los1 or atl1. You can find how the server is determined in their JavaScript, but we may as well hardcode it. After the server name is obtained, we call GET https://${server}.minesweeper.online/mine-websocket/?authKey=${authKey}&session=${session}&userId=${userId}&transport=polling. The response will be something like

94:0{"sid":"xxx","upgrades":["websocket"],"pingInterval":300,"pingTimeout":5000}2:40

We need the sid, which we can get here, or from the io cookie set from this endpoint. I’m guessing this is asking the server to create a new socket, hence sid. We call the endpoint again, with &sid=${sid} appended to the query, and we have to do this within 5000 milliseconds, as per the pingTimeout field. If all is good, we’ll get a response back like:

19:42["authorized",[]]

Afterwards, we may initiate a websocket connection to wss://${server}.minesweeper.online/mine-websocket/?authKey=${authKey}&session=${session}&userId=${userId}&transport=websocket&sid=${sid}. Their protocol requires a 3 way handshake upon websocket initialization otherwise the connection will terminate.

  1. Client sends 2probe
  2. Server replies 3probe
  3. Client sends 5

After the handshake is complete, we must ping the server with 2 according to pingTimeout, which is 5 seconds, to which the server acknowledges with 3. If the server does not receive any pings in 5 seconds from us, it will terminate the connection with code 1005.

Great, we managed to establish a connection after all of this! I think Minesweeper Online uses a library, socket.io, to manage these socket messages and I considered learning it to help this process, but I decided against it. I wanted to do this blind, to simulate reverse engineering a completely foreign network protocol. So, we carry on to encoding and decoding messages for playing a game.

From what I can tell, all (outgoing and incoming) messages regarding Minesweeper games start with a 42, followed by a JSON array with the following format:

[
  direction,   // "request" or "response"
  [
    opcode/function,    // see below
    [payload],          // depends on opcode/function
    something           // the client always seems to include "494" here, while the server
                        // doesn't have anything here. maybe version number?
  ]
]

opcode is represented as a string when logging it to console, but when converting it back to binary, it seems to be 4 bytes long for outgoing messages and 7 bytes for incoming messages, which is weird because it’s not a multiple of 2. I don’t think this should be a byte alignment issue from converting it to a string and back, because the byte before opcode should correspond to " in the JSON array.

It doesn’t really matter how to make sense of these opcodes since you can just treat them as magic strings, but I kind of wanted to. I thought that maybe there would be a table of opcodes in the JavaScript or that it was actually some binary code converted to a string, but no, I could literally search the JavaScript for the opcode string and find results. Furthermore, the opcodes that were sent by the server kind of looked like symbols in the minified JavaScript, e.g. G68.t18. Sure enough, I found an object called G68 with a function t18 with the number of parameters matching the length of the payload array for the G68.t18 opcode.

Wow, what a discovery! So in fact, opcode isn’t really an opcode, but a function, which is what we’ll be calling it from now on. This makes referencing how to handle incoming messages so much easier. And to think I just stumbled upon this while satiating my curiosity. After thinking about it though, what I really should have done was to figure out where and how incoming messages were being handled in the client code, and I probably would have gotten to the same conclusion, though with much more headache while navigating the minified code.

Anyway, back to business. To make things easier to read, I’ll only write the function name and payload, but you should keep in mind the full message format, especially that clients must send the additional "494".

To initiate a game, we send a message like so:
// function: gn16
[
  gameType, // beginner=1, intermediate=2, expert=3
  null, null, null, null,
  number, // not sure
  1, // seems to always be 1
  null,
  countryCode, // 2 letters, e.g. "CA"
  null, null, null, null
]
The server will reply with a long message giving lots of details.
// function: G69.i41
[
 {
    id, // game id
    originalId,
    server,
    level,
    sizeX,
    sizeY,
    mines,
    state,
    nf,
    exp,
    diff,
    hints,
    userId,
    ping,
    clickType,
    mobile,
    timeGameCreated, // Unix timestamp, milliseconds
    timeStart, // same format
    requests, // array of numbers for when requests were made
    startX,
    startY,
    user {
      username,
      country
    },
    // other stuff
  },
  // board state. each is a key of numbers from 0 to sizeX*sizeY-1
  // goes down and then right
  {
    t: {}, // picture id — 0-11
    o: {}, // opened
    f: {}, // flagged
    g: {}, // not sure
    g2: {}, // not sure
  },
  [click history]
  // other stuff
]
You also get the same reply format as above if you restore a game using this:
// function: "gj4"
[
  gameId,
  null,
  countryCode,
  0
]
Here’s the message format for client clicks.
[
  "request",
  [
    "gu57",
    [
      numClicks,
      gameId,
      clickType, // probe=0, flag=1, chord=2
      x, // 0 indexed, y increases going down
      y,
      timeElapsed, // since the start time, in milliseconds
      [ [x,y] ], // array of coordinates to be changed
      blackMagicString, // no idea, only non-empty when flagging. likely binary.
      null, // usually, not sure what it is
      null, // usually, not sure what it is
    ],
    "494"
  ]
]
And here’s the message format in response.
// function: "G68.t18"
[
  numClicks,
  gameId,
  {
    x, y,
    timeElapsed,
    type,
    touchCells: [
      x, y,
      numBombs, // picture id — 0-11
      isRevealed, // 0 or 1
      isFlagged, // 0 or 1
    ]
  },
  null, // usually, not sure what it is
  bool, // whether the server sent this message, I think
]

The game complete message from the server is fairly similar to the initiate/restore game message, but I’ll omit it here because your eyes have probably already glazed over. If you’re interested, check out the repository at the end of the post.

With this information, it’s not too difficult to build a custom client for the game. In fact, I built a simple terminal interface as a proof of concept.

To wrap things up, here are some of the things I learned.

Firstly, to reverse engineer any protocol, it’s incredibly important to set up a good sniffing tool. Without some way to sniff, filter, and match packets to actions, none of this would be possible. For websockets, this was fortunately very easy since I could just override the browser’s default WebSocket class by loading some code early on, and so I don’t even have to figure out how to decrypt messages. Thank you, JavaScript for having the jank to allow this to happen so easily.

After the sniffer is set up, we have the difficult job of figuring out the client message format. I don’t think there’s any step-by-step process that will work every time, but generally, we want to repeat actions and see what changes or doesn’t change in the corresponding messages. After some baseline is established for the message format of an action, we should slightly change the action to see how the message responds. For example, what happens to the message format if I left click instead of right click an unopened tile? What will happen if I left/right click an opened tile? I was lucky enough to have messages be encoded as a JSON object, but I can imagine tools like xxd would be very helpful to view the message as hex/binary.

For decoding server messages, it’s still a similar process. Repeat actions that you expect should produce similar results, find the kernels of similarity, etc. With server messages, however, you have access to the code that handles them, unlike client messages. Most of the time, the client code will be obfuscated so it’ll be difficult to read, but this is where we can break out our debugging skills and step through the code line by line. Even with JavaScript, it’s usually minified to the point where it’s completely unreadable and you have to trace call stacks and pause execution often to get any understanding of the code.

And really, that’s it. Afterwards, you might want to build some tools to let you send and receive messages like I did with the terminal interface, but it can be a lot of work, especially for more complex protocols. I had a lot of fun with this project, and I hope you learned a thing or two. Happy hacking!

Repository link — contains all the tools I wrote for this project.

Leave a Reply

Your email address will not be published. Required fields are marked *