How we hacked an Android game to top the global leaderboard without even playing the game.

Recently, we came across an Android game of Minesweeper. The game has been nicely developed and was fun to play. Although it was very tough to win the game and even tougher to be in top ranks on the leaderboard. That’s when it struck us, why not “play” with the game some other way. So we started analysing the game.

What is Minesweeper game

Minesweeper has a very basic gameplay style. In its original form, mines are scattered throughout a board. This board is divided into cells, which have three states: uncovered, covered and flagged. A covered cell is blank and clickable, while an uncovered cell is exposed, either containing a number (the mines adjacent to it), or a mine. When a cell is uncovered by a player click, and if it bears a mine, the game ends. A flagged cell is similar to a covered one, in the way that mines are not triggered when a cell is flagged, and it is impossible to lose through the action of flagging a cell. However, flagging a cell implies that a player thinks there is a mine underneath, which causes the game to deduct an available mine from the display. In order to win the game, players must logically deduce where mines exist through the use of the numbers given by uncovered cells. To win, all non-mine cells must be uncovered and all mine cells must be flagged. At this stage, the timer is stopped. When a player left-clicks on a cell, the game will uncover it. If there are no mines adjacent to that particular cell, the mine will display a blank tile or a “0”, and all adjacent cells will automatically be uncovered. Right-clicking on a cell will flag it, causing a flag to appear on it. Note that flagged cells are still covered, and a player can click on it to uncover it, like a normal covered cell.

Introduction

Initially, our goal was to win the game irrespective of the time required. During the analysis we found that it was possible to reverse engineer the application and change values of some of the functions and win the game. We were able to achieve this task using two different methods.

Our next goal was to top the global leaderboards of all the difficulty levels, i.e. Beginner, Easy, Intermediate and Expert. In order to do that, we started analysing the application dynamically and checked the network traffic between the application and the server it was communicating with. We analysed source code of the apk even further and tampered the request accordingly to top the global rankings for every difficulty level.

Note: The name of the game has been redacted on purpose. The game has 1M+ downloads on play store.

How we did it

Find the package name of installed application and decompiling it

There are multiple ways to achieve this, whether with ADB or from playstore URL https://play.google.com/store/apps/details?id=<app package name>

To extract the APK from the device, we used the adb tool. Connect the device to the computer and make sure debugging is enabled. Start adb server and pull the apk using the following command. adb pull $(adb shell pm path <app package name> | cut -d':' -f2) mv base.apk game.apk To decompile the application, we will use apkx tool. apkx is a Python wrapper to popular free dex converters and Java decompilers. Extracts Java source code directly from the APK. Useful for experimenting with different converters/decompilers without having to worry about classpath settings and command line args. apkx game.apk Reference: Download apkx here

Different methods we hacked the application

METHOD 1: Hook the application at run-time and toggle return value of GameActivity.finishgame function.

Hook the application at run-time and toggle return value of function. METHOD 2: Hook the application at run-time and print game board from game.GameBoard function.

Hook the application at run-time and print game board from function. METHOD 3: Hack the application by just sending a success message to server by tampering time required and the checksum.

Observation is the key to hack these kind of applications

TOOLS USED

Method 1 & 2: Frida

Method 3: BurpSuite or Curl (command line utility on Linux OS)

METHOD 1

Observe what actions are performed when you click on a bomb. The following observations are helpful

Game ends Timer is stopped

We look for functions that trigger these operations.

finishgame function expects a boolean argument. Print the boolean argument to see what is passed into the function.

setTimeout ( function ( ) { Java . perform ( function ( ) { var GameActivity = Java . use ( "<app package name>.GameActivity" ) ; GameActivity . finishgame . implementation = function ( bl2 ) { console . log ( ">>>>> Hacking minesweeper game: finsihgame return value = " , bl2 ) ; this . finishgame ( bl2 ) ; } } ) } , 10 ) ;

The below commands spawns a new process

frida -f <app package name> -U -l hook.js --no-pause

Now, observe what happens when a mine is clicked. A false value is passed to finishgame function.

Maybe that’s how it knows, we lost the game. Instead we will try to toggle the value of bl2 passed to finishgame function.

setTimeout ( function ( ) { Java . perform ( function ( ) { var GameActivity = Java . use ( "<app package name>.GameActivity" ) ; GameActivity . finishgame . implementation = function ( bl2 ) { console . log ( ">>>>> Hacking minesweeper game: finish game value = " , bl2 ) ; this . finishgame ( true ) ; } } ) } , 10 ) ;

Execute the following command to start the application and hook Frida script

frida -f <app package name> -U -l hook.js --no-pause

We can see that we won the game when we click on the bomb, instead of losing the game.

METHOD 2

In the first method, we hooked the function that sends the message to finish game.

Now, we have to see where the bombs are located. Observe the decompiled java files to locate where the bombs are placed. We have to look for the function that places these bombs.

Open the game, start a new game and hook the above js scriptt to existing process.

var game_board_instance ; console . log ( ">>>>>>>>> Init frida script" ) ; setTimeout ( function ( ) { Java . perform ( function ( ) { console . log ( ">>>>>>>>> Init Java perform module" ) ; Java . choose ( "<app package name>.states.game.GameBoard" , { "onMatch" : function ( instance ) { game_board_instance = instance ; console . log ( "Captured game board instance" ) ; } , "onComplete" : function ( ) { } } ) ; console . log ( "Height: " , game_board_instance . height . value ) ; console . log ( "Width: " , game_board_instance . width . value ) ; console . log ( "Extracting the game board sequence >>>>>>" ) ; for ( var i = 0 ; i < game_board_instance . height . value ; i ++ ) { var horizontal_sequence = "" ; for ( var j = 0 ; j < game_board_instance . width . value ; j ++ ) { horizontal_sequence = horizontal_sequence + " " + game_board_instance . tiles . value [ i ] [ j ] ; } console . log ( horizontal_sequence ) ; } } ) } , 10 ) ;

Run the following command to execute app and hook our Frida script

frida -U <app package name> -l hook.js --no-pause

We can see that the entire board is printed along with the pints. The value 9 is the bomb!

METHOD 3

After we finish the game, there the application provides us with a global rank. These requests to the server can be intercepted with a proxy tool like BurpSuite.

Reference: https://portswigger.net/support/configuring-an-android-device-to-work-with-burp

Note: The application had implemented SSL Pinning, which we were able to bypass using Objection. Since this is a blog showing how we hacked the app, we decided not to show how to bypass SSL pinning using Objection or Frida. We will write a new blog with step by step instructions on how to use Frida and Objection.

Upon intercpeting the request we realised, simpleDbTime parameter contains the time we took to finsih the game. If we can intercept this request and send a fake simpleDbTime to the server, we win. But its not easy. When we did that the result isn’t persistent, our scores weren’t reflected in global scoreboard. We realised there is a checksum that validates the request.

We analyzed the code more and found the code corresponding to checksum.

We can see the implementation of the checksum algorithm. Parameters like itemName , deviceId and simpleDbTime have been used with dot separated to calculate the md5sum of the request.

We computed the value of checksum locally that corresponds to our new simpleDbTime value by using the same logic as performed in application code above.

Let’s get MD5 checksum of the required parameters

We will now send request using Burp with tampered parameter values.

Send another request to check the leaderboard scores. We will see that we are ranked 1st on the leaderboard

Upon submitting this request, we can see that our data was successfully registered in the database and we can see it in the global highscores list as well.

RESULT

Expert level challenge solved in 0.01 seconds without even playing the game!

These bugs are fixed in the latest version of the application.

RESEARCHERS

TIMELILNE