Hack mobile online game? Easy!.

Hi, Habr! Today I will tell you about what you may encounter if you suddenly decide to get into the jungle of someone else’s Android application (in this case, online games). Adventures with viewing Java classes in .dex, learning Dalvik op codes and even binary programming. But first things first.

There is ~ 800kb traffic under the cut, 293 of which are screenshots of the code (!)

This article was written for informational purposes only. The author does not bear any responsibility for any actions of users who have read the article. Any matches in the article are random.

Once, on a rainy summer evening, my girlfriend and I were looking for something to do with ourselves. I didn’t want to watch the film, but there was no desire to get out of bed. The choice fell on a mobile toy. The basic requirements for the game were not too many:

  • must support iOS and Android;
  • must have a single server for both platforms;
  • There must be a joint game, so that it makes sense to play together.

So we found the game from the Google Play top. In order not to reveal the names, we will call it the game N. However, as it turned out later, the third item from the list above is implemented a little more than nothing.

Below I hid a short description of the game under the spoiler, it is not necessary to read it, but it will be useful to complete the picture.

The game is based on monsters. You call them, swing them and their spells, put on them runes, which are covered with various stats. In addition to the usual levels, monsters also have a gradation by stars:

1 star - max level 15
2 star - max level 20
3 start - max level 25

and so on up to 6 stars and a maximum level of 40. Having reached the maximum level, you can raise the monster the number of stars, while its level will be reset to the first. This process in the game is called Evolve. To do this, you will have to "eat" other specific monsters, for example:

to evolve one monster 2s -> 3s, you need to eat 2 existing monsters 2s.
4s -> 5s - 4 monsters must be consumed 4s
5s -> 6s - 5 monsters 5s - this is verylabor intensive by game standards.

Monsters can be invoked in a variety of ways, but ultimately it comes down to three:

  1. knock out in a location (max 3s, the chance is pretty small)
  2. call-up scrolls 1-3 (1s-3s, respectively. 95%, that you’ll get 1s or 2s from the call. Here it is worthwhile to clarify that the 1s-2s monsters are slag, and in 99% of cases they are consumed, because stats lose a lot. Scrolls fall very often, you can get 20-30 pieces a day without much dodging);
  3. 3-5 scroll scrolls (~ 90-95% that you get 3s, 4s fall rarely, 5s monsters never fell from these scrolls. Scrolls can be purchased in unlimited quantities for red crystals. Fall very rarely)

Now about the game currency:

Energy - needed for trips to PvE locations and dungeons. Consumption - from 3 to 8, depending on location. It accumulates one at a time in 5 minutes, often falls directly from the killed mobs. There is a ceiling of energy that increases with the level of the player (not to be confused with the level of monsters) and with the help of a special building.

Arena Energy - Used to go to the arena, PvP. About why PvP in quotes, you will learn a little lower. 1 trip to the arena spends 1 unit of the arena of energy, a maximum of 10, accumulates once every half hour.

Blue crystals are the main currency of the game. She bought things from the store, most of the buildings. Accumulated by various buildings, falls from killed mobs, give as a reward for tasks.

Red crystals are a minor currency that you can buy for real money. They can be spent on those same scrolls 3-5, on updating energy and the arena of energy and buying blue crystals for them. Very rarely, they fall from dead mobs or in the arena. On the day you can earn about 30-40, by the way, one scroll 3-5 costs 75.

Fame points are the currency given for winning an arena. A lot of interesting buildings and objects are bought for it. This currency cannot be purchased for any other or donat.

Donut in the game do not crush. Everything is good and calmly passed / bought without an injection of money. Donat, in fact, gives you only more scrolls, the chances of getting useless monsters from them are the same. You can’t buy a specific monster for a donut (and indeed it’s impossible at all).

The battle system resembles Final Fantasy 7-10 or, if you like, HoMM - turn-based combat, a choice of 2-4 spells. In dungeons from 3 to 10 levels (most often 3 or 5), at each level there is a pack of mobs, kill - go further, do not kill - you get everything that you managed to earn (crystals, energy, experience).

Regarding "PvP" and "joint play." As it turned out, you have no opportunity to play with or against a person. You play either yourself or on an “auto attack” and always against the computer. Therefore, PvP is quite boring here. It consists in the following: each player puts on a def 4 monsters and buys towers for defense. Once in the arena, you are fighting against an AI that is pretty stupid. To balance this, after some time, the very towers start firing at you, each time more and more often.

I am a remote programmer, so I work at home. At home, there is always good wi-fi, and I didn’t really think about how and when the game interacts with the server.

Until I decided to enter the game from the mobile Internet. Having run through all the levels in the next dungeon, I received the message “Network connection is delayed. Resend Battle Results? (If Battle Results are not sent, the battle will count as loss.). " After I clicked on the “Yes” button, the results still went to the server.

If you read the description of the game, then for you a lot fell into place: why there is no normal PvP or a game together - the game rarely communicates with the server, most likely via HTTP, no sockets. And most importantly - apparently, in this game the client calculates the results of the battle, and the server only receives them.

Having experimented immediately with disconnecting the Internet at various points in the game, I found out the following:

  • when entering the location, you send a request to the server. If it passes, and you get an answer, the download to the location begins;
  • the server does not control your movements between levels of location;
  • when you kill the final pack of mobs, another request is sent. Your loot comes in response.
  • if you can send the results of the battle, but can’t get an answer - they will show you the same message - "Network connection is delayed ...". But if you try to do it again (and the request-response will pass), you will get the message “Can't find match data”. This makes you think that when loading into the location, the response id of your battle is sent to you, which is used at the end of the dungeon to send its results.
  • as I said in the description, not only blue crystals fall from mobs. Quite often, energy drops and occasionally red crystals, and you see what and how much fell during the murder of a mob. And here the thought arises: does the client decide how much and what will fall, and then sends it to the server? If so, then by sending the “right” request, we will not only be able to “win” the dungeon, which is too tough for us, but also take a couple of hundreds of red crystals and energy from it.
  • with all this, you can not be afraid for the further passage of the squad through the dungeon. After completing the walkthrough (one way or another), we just get the message “Can’t find match data”, the results have already been sent.

Idea 1. Fake a request

The most logical idea that may come after the one described above.
In order to fake a request, we need the original. There are many options for monitoring traffic, I chose the simplest one for myself - to send traffic through a laptop and look at all this using WireShark.

Instructions for turning your computer into an access point can be found here .
In order to have less unnecessary “noise” in the logs, close all applications and turn off synchronization.
During loading, the game loads information on promotions, partners, and friends from Facebook - in general, there is a lot of traffic, and it goes to different servers, so we are not interested. We go to the location:

POST /api/gateway.php HTTP/1.1
User-Agent: Dalvik/1.6.0 (***)
Host: ***
Connection: Keep-Alive
Accept-Encoding: gzip
Content-Type: application/x-www-form-urlencoded
Content-Length: 556

HTTP/1.1 200 OK
Server: nginx
Date: Sat, 19 Jul 2014 15:04:16 GMT
Content-Type: application/octet-stream; charset=utf-8
Content-Length: 1048
Connection: close
X-Powered-By: PHP/5.4.11
Cache-Control: no-cache, must-revalidate
Pragma: no-cache

At first glance it is clear that this is Base64. But having driven the text into the first decoder that came across, I got complete nonsense, although I expected JSON (just kidding, it would be too naive - the application has 10kk + downloads).

We need to go deeper ©

Download apk games (there are many options, I took advantage of this ). The APK file is a regular ZIP archive, it contains a lot of things, but first of all we are interested in the classes.dex file. This is the Dalvik executable format. Essentially, compiled Java classes. To open them, we need dex2jar and jd-gui . The first converts dex to jar, the second tries to restore the source code from jar.
What will restore jd-gui looks pretty dreadful and read-only. You should not try to compile it. Sources from jd-gui can be saved and opened in your favorite editor. I downloaded the 30-day IDEA trial from JetBrains, because I really like the way their products are searched (I use PyCharm and PHPStorm purchased myself).

Warning for those who prefer this editor - do not set the SDK, you will fail with errors.

From programming for android, I knew only the basics and did not have a clue where to begin my search. So I ran a search for “base64” on the project, and found a class that implements Base64 decode and encode. I was very surprised at this, because these methods were not just a wrapper for library ones, but, judging by the code, they really implemented Base64 encoding and decoding.

The first thought that came to me - the creators wrote something of their own, which is similar to Base64, but encoded differently. Since the code looks awful (methods over a thousand lines, goto's, instructions in the methods immediately after return, and other joys of life), I could not rewrite this. Then I remembered that the server was written in PHP, and decided not to despair, because it was pretty expensive to develop two base64 native implementations on two very different platforms. A little later, I googled Base64 in Java and realized that there is no base64 encoding in standard Java libraries (versions 6 and 7), which finally drove away my fears about an alternative implementation.

After searching for the use of this class, I went to another - StringEncrypter, which implemented several methods, but the main ones - decrypt and encrypt. Quickly looking at the decrypt method, I realized that this is what I need. The data was decrypted from base64, run through AES / CBC / PKCS7Padding and returned. It only remained to find the key that was used for Cipher and the initial vector (Initialization vector).

To do this, I began to look for the use of these methods. And I found that the StringEncrypter class is not used anywhere. It surprised me a lot, but I thought it was the flaws of jd-gui.

I started the search for the project again, this time I immediately looked for Cipher. There were many results, and, wandering around them, I came across a file whose source code, apparently, could not be restored. Instead of the code hung "INTERNAL ERROR". Having launched the search for this same “INTERNAL ERROR" in the project, I got 55 results. It became clear why I could not find the use of some classes. Among these files, there was one with the interesting name ActiveUserNetwork.

We need to go deeper ©

I guessed that next - only assembler. And so it happened.

Dalvik VM has a lot of opcodes, and in fact, smali code is pretty user-friendly to read, especially if you were picking it in assembler.

This time we need smali and baksmali . Backsmali converts the classes.dex file to a source folder, while preserving the hierarchy and names of folders and files. At first it will be much easier to understand smali code by opening the same java file (unless, of course, jd-gui was able to decompile it). There are enough resources on the Internet where you can find code examples, here , for example, it shows how arrays in smali look and instructions for / switch.

But back to our game, and specifically to the ActiveUserNetwork file that interested me. Everything was found here - Cipher, encrypt and decrypt methods, Base64, (by the way, here it was used from the android.utils library) and even the constant “http: //***.com/gateway.php”. Yes, this is not /api/gateway.php, but at least something. By the way, the search for “api / gateway.php” didn’t give anything even by smali code, but I wasn’t really upset because I saw that StringBuilder is often used.
There is no illumination of smali code on the hub (we’ll be frank, there are few places there), so I will upload large pieces of this code with screenshots.

So the method decrypt:

Explanations of the code: the first line contains a standard description of the method: what it accepts and what returns. The method takes 2 parameters - a string and a byte array (here it looks like [B). Returns a byte array.
The .locals directive indicates the number of registers that the method will use, not counting its parameters. In addition to this directive, there is a similar one called .registers , it determines the number of registers that are used by the method, including method parameters. Those. in general, .registers = .locals + params. At the same time, if you declare the number of registers through the .registers directive, the method parameters fall into the last registers. Access to the registers is through v0, v1, v2 and so on, to the parameters - p0, p1 and so on.
This method is static and is called without an object, otherwise there would be 3 parameters, the first of which would be the object for which the (this) method is called. The following two directives may be missing; these are parameter names. The .annotaions directive declares additional information about the method, in this case, the thrown exception. The .prologue directive says that the body of the method goes further.
Given all this, the first 11 lines are converted to a single line of Java code:

public static byte[] decrypt(String key, byte[] data) throws Exception {

Looking at the opcode table and remembering that the object itself is always passed as the first parameter in invoke-virtual, unlike invoke-static, we “rewrite” the method “in word terms”:

public static byte[] decrypt(String key, byte[] data) throws Exception {
    String v1 = "AES/CBC/PKCS7Padding";
    Cipher cipher;
    cipher = Cipher.getInstance(v1);
    int v1_1 = 2;
    SecretKeySpec v2 = createSecretKey(key);
    AlgorithmParameterSpec v3 = spec;
    cipher.init(v1_1, v2, v3);
    byte[] v1_2 = cipher.doFinal(data);
    return v1_2;

Here specis a static variable of the ActiveUserNetwork class, it is initialized in the class constructor

.line 78
new-instance v0, Ljavax/crypto/spec/IvParameterSpec;
const/16 v1, 0x10
new-array v1, v1, [B
invoke-direct {v0, v1}, Ljavax/crypto/spec/IvParameterSpec;->([B)V
sput-object v0, Lcom/com2us/module/activeuser/ActiveUserNetwork;->spec:Ljava/security/spec/AlgorithmParameterSpec;

I replaced this constructor code with a method getSpec. We bring the method decryptto normal form:

public static byte[] decrypt(String key, byte[] data) throws Exception {
    String alg = "AES/CBC/PKCS7Padding";
    Cipher cipher = Cipher.getInstance(alg);
    SecretKeySpec secretKeySpec = createSecretKey(key);
    cipher.init(2, secretKeySpec, getSpec());
    return cipher.doFinal(data);

So, we need to deal with the methods createSecretKeyand getSpec.

public static AlgorithmParameterSpec getSpec() {
    byte[] v1 = new byte[16];
    return new IvParameterSpec(v1);

This is the converted code from the constructor. It was already at night, and the number 0x10 my brain translated into the decimal system as "10". It’s good that I decided to double-check in the calculator, otherwise I would be completely disappointed :)

Method createSecretKey(here, by the way, a typo in the name, we will fix it too)

The method is very simple, it is converted to

public static SecretKeySpec createSecretKey(String key) {
    return new SecretKeySpec(key.getBytes(), "AES");

Well, it remains only to find out what is passed to the method with the key and data.
The method is responsible for this processNetworkTask, which simultaneously sends a request (with encrypt and Base64 encode), and receives a response. The method is voluminous (1k lines), therefore I will post only the assembly of the pieces of interest to us ( v18- a class object org.apache.http.HttpResponse)

In short: The
header value is taken REQ-TIMESTAMP, the method is called createHash("MD5", header_value). From the returned string, we get the substring from the first character to the sixteenth, and this substring is passed to the method decryptwith the key. A byte array of Base64.decode().

So, we have everything on hand, except for the method createHash.
smali code:

This method is already harder to perceive: there is a cycle, and exceptions, and conditions. And it is impossible to make mistakes in cryptographic methods. This one design is worth:

const/4 v7, 0x1
new-array v7, v7, [Ljava/lang/Object;
const/4 v8, 0x0
aget-byte v9, v3, v1
invoke-static {v9}, Ljava/lang/Byte;->valueOf(B)Ljava/lang/Byte;
move-result-object v9
aput-object v9, v7, v8

And it turns into a banal. Byte v9 = mdByte[i];
As a hint: almost always, when you see the increment ( add-int/lit8 v1, v1, 0x1) before goto- this is a for loop. Final Java code:

public static String createHash(String algorithm, byte[] data) {
    try {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] mdByte = md.digest();
        String mdString = "";
        int i = 0;
        int len = mdByte.length;
        for (i = 0; i < len; i++) {
            StringBuilder v5 = new StringBuilder(mdString);
            String v6 = "%02x";
            Byte v9 = mdByte[i];
            v6 = String.format(v6, v9);
            mdString = v5.toString();
        return mdString;
    } catch (NoSuchAlgorithmException e) {
        return "";

Putting it all together. I created a new application and dropped everything in MainActivity:

public void onCreate(Bundle savedInstanceState) {
    String b64 = ""; // Base64 request body
    String req_ts = ""; // REQ-TIMESTAMP header
    byte[] decodeBase64Byte = Base64.decode(b64, 4);
    String hash = createHash("MD5", req_ts.getBytes());
    hash = hash.substring(0, 16);
    try {
        Log.d("MYAPP", "Decrypted: " + new String(decrypt(hash, decodeBase64Byte)));
    } catch (Exception e) {
        Log.d("MYAPP", e.getClass() + "-" + e.getMessage());

Now back to why we all do this. An attentive reader might have noticed that there was no REQ-TIMESTAMP header in either the response or the request above. However, there the request goes to / gateway, and not to / api / gateway. Requests to / gateway go during application initialization. There are only two of them, decoding them, I got ... nothing. No, there was data about the device, the MAC address and even about whether the tablet was rutted. But I got nothing worthwhile. Requests to / api / gateway came from somewhere else and were in no way associated with / gateway.

While I was dealing with smali code, I made several more attempts in different directions before I came to the decision to deal with all the methods and rewrite them in Java.

Attempt 1: do not look for how the key is generated, but simply make a request with it to your server. The smali code can be changed and compiled back, so the idea was simple - before the decrypt method, we make a request to our server, passing the key as a GET parameter, and then we look at the web server logs.
Looking for how to compile smali code, I found apktool . This tool is able to parse the apk file immediately into smali code, as well as collect it all back into apk.

./apktool decode ~/Downloads/***.apk ~/Documents/out/
./apktool build ~/Documents/out/ ~/Downloads/***.new.apk

But when you try to install a new application, you will get an error:

./adb install -r ~/Downloads/***.new.apk

On stackoverflow, it is advised to remove the application manually, and then install it again, but that did not save me either. We make a new key, for this we need keytool and jarsigner (included in the openjdk package)

keytool -genkey -keystore ~/debug.keystore -validity 10000 -alias debug
jarsigner -keystore ~/debug.keystore -verbose ~/Downloads/***.new.apk debug

An important note - jarsigner behaves differently in versions 6 and 7 of jdk, and on version 7 the team swears at alias. I did not find a solution to this problem and installed myself an additional 6 version.

After that, the installation of the application will pass. But, unfortunately, the application (even if you did not change it, but simply decompile-compile-subscribe-install) immediately crashed. I guess that the server somehow checks the signatures, but I will be glad if someone in the comments clarifies. This idea had to be abandoned.

Attempt 2: if there is no desire to poke around in smali code (but I didn’t have it, I thought it was a task for ~ 5 hours), then it’s easier to do. In your application, create an empty method with the same interface as the one you want to copy, create apk, decompile, copy the body of the method, assemble it back. Each such iteration takes a lot of time. Therefore, it will be faster to study opcodes. It’s clear that you will not be able to open the source code of the assembled application.

This article was not written in one day, and I write this remark a week after the paragraph above. The method of describing the interface and copying smali code into the body of the method helped me a lot when I was not able to restore the source code of the key generation method. To simplify your life and reduce iteration time, you can combine everything into one team

apktool build ~/myapp/ ~/myapp.apk &&
jarsigner -keystore ~/debug.keystore -verbose ~/myapp.apk debug && adb install -r ~/myapp.apk &&
adb shell am start -n "com.example.myapp/com.example.myapp.MyActivity" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER &&
adb shell logcat MYAPP:D *:S

Idea 2. We make a supermonster.

Well, if it does not work out through the request, we will go the other way.
New monsters are loaded into the game not through an update to google play, but through an in-game update. It means that they are stored somewhere outside of apk, and theoretically we can modify them.
I didn’t have to look for the folder for a long time - /sdcard/Android/data/com.***/files/patch/
Here all the sprites and sounds were found, and most importantly - a lot of files with the names of monsters and the extension .dat. We open it with a hex editor and quickly look through it - no headers and lines for which the eye would catch. I took the first level one monster that came across, looked at its HP and started looking for / replacing these bytes in the hope that the file was not encrypted. There were 7 matches. Replacing them sequentially, I got 4 game crashes and 3 “nothing has changed”. Sorry, encrypted.
But something must decrypt it! We are looking in the regexp code "\ .dat \ b" (in order to exclude methods starting with "data" in the results). Only the CommonData.dat file is found. This file is hidden in the /data/data/com.***/ folder (if you have an empty folder in the / data folder, you need root access).
The file is encrypted and has a size of 1kb. It is clear that there is nothing worthwhile there, however, the encryption algorithm may be the same. This time I will not upload the code, it takes about a thousand lines. Most importantly, a key based on ANDROID_ID is passed to the decrypt key. After opening the file, the MAC address was found (again). Having tested the same algorithm on monster files, I got an error.
Sadness is longing.

Hello assembler!

However, in the same /data/data/com.***/lib/ folder, I came across a .so library. I already saw them in apk and saw their connection in MainActivity (honestly, all this time I really hoped that I would not have to pick them). There were two libraries - libgame.so, libcom ***. So. The second one weighed very little and did not carry any value to me. I opened the first with a hex editor and after half a minute I found the line “http: //***.com/gateway/api.php”.

We need to go deeper ©

I hope you are not tired :) Because we start almost from the very beginning.
Frankly, at this stage I spent about 20-30 hours of my time. If you are not familiar with processor opcodes, registers and memory, you will also be stuck here for a long time. I was saved only by my persistence and desire to prove that a person is cooler than some application there.
A lot of time was spent on the selection of tools and techniques. And if the remaining article will save someone a few hours in the future - this will be great.

Ida Pro 6.1+ is the main debugging tool. Since version 6.1, it comes complete with the android_server file and the ability to remotely debug android applications.
gdbserver is another tool for remote debugging.
At this point, root access to the device is required.

Download both servers to the device:

adb push gdbserver /data/local/tmp
adb push android_server /data/local/tmp

Configure port forwarding to the localhost:

adb forward tcp:5039 tcp:5039
adb forward tcp:23945 tcp:23945

We expose the necessary rights:

adb shell
chmod 755 /data/local/tmp/gdbserver
chmod 755 /data/local/tmp/android_server

The main idea (thanks to this topic , it saved me a ton of time):
  1. connect via android_server, find the library, remember the offset;
  2. load the library in Ida with the specified offset;
  3. Ida analyzes, exposes function names, call schedules, and even tries to reproduce the source code;
  4. run the application on the device;
  5. load the already analyzed library into the application via gdbserver
  6. set breakpoints, analyze. If the application crashes goto 4.

Why are two servers used for remote debugging? android_server can show beautifully loaded libraries and the offset for the desired library is very fast in it. But breakpoints do not work in it. But they work fine in gdb. It seems that you can also look for offsets via the gdb client using info sharedlibrary, but it didn’t work for me.

Location of the necessary settings in Ida
You can download the file with the offset through File > Open > Выбор файла > Manual load. In the same window, it is worth specifying the type of processor.
Selecting the remote debugger: Debugger > Switch debugger.
Configuring the connection to the debugger: Debugger > Process options. Here we set localhost and the server port that is currently in use.

Another important point:
If the application crashes, you (with some probability) will have to do all the steps from the first. It's all about ASLR technology . To disable it, execute in the shell:

echo 0 > /proc/sys/kernel/randomize_va_space

Attention! This greatly affects the security of your device. Do not forget to remember the value of this parameter and return it to its place after your experiments.

So, a more detailed action plan:
  1. Run android_server:

    adb shell

  2. We open Ida without loading anything, we go in Debugger > Attach to > Remote ARM/Android Debugger.
  3. Select the desired application from the list.
  4. We are looking for our library (there are many options, but the fastest thing I could do was find it with my eyes - scrolling and browsing. Searching works slowly, jump to the mark does not always work.
  5. Remember the library offset ( 5D699000in my case).
  6. We are disconnected from the process ( Debugger > Detach from process). The process on the device remains alive.
  7. Open the file, set the desired offset ( 0:5D699000in my case).
  8. While Ida parses the file, kill android_server and prepare gdbserver:

    adb shell
    /data/local/tmp/gdbserver --attach :5039 1234

    (the --attach option tells the server to join the already running process: 5039 - port number, 1234 - pid of the process, which can be seen familiar psin the shell).
  9. Change Ida settings to work with gdbserver.
  10. We are connected to the process.

Now, if everything is done correctly, the library code that Ida analyzed will fall into place.

Having studied the list of functions from the library, I found a very interesting group that was responsible for interacting with the server. Here are some of the features:
  • battleArenaStart
  • battleArenaResult
  • battleDungeonStart
  • battleDungeonList
  • battleDungeonResult

All these functions formed JSON, populated it, and called the function sub_5D839994. This feature is the cornerstone of communication with the server. It encrypts strings, packs it in base64, and sends the data.
Fiddling with cryptography was difficult even in the more or less clear smali code. It was hell right there. Although I found a key that is used for encryption, I still got lost in search of IV.
But since we are already hanging on the application with a debugger, it’s enough for us to intercept the line before encryption, change it and continue the application. And the modified string will go to the server.
As I said, this function ( sub_5D839994) is used everywhere (or almost everywhere) where there is communication with the server, so putting a breakpoint on it is almost useless, it will work right away, since the game has chat.
Fumbling a bit, I found a solution. I set 2 breakpoints: one before the call sub_5D839994in the desired function, the second, in the disconnected form, immediately before the function AESConvertEncodethat is responsible for encrypting the string.

So, the moment of truth, we enter the arena against a strong opponent. With defeat, we lose it, the application freezes - the breakpoint has worked. Turn it off, turn on the breakpoint before AESConvertEncode, launch the application and ... Hurray! The debugger took control right before the encryption. Register contents R0:

    "command": "BattleArenaResult",
    "id": 1234567,
    "session_key": "***",
    "win": 2,
    "unit_status": [
        {"unit_id": 1,"result": 2},
        {"unit_id": 2,"result": 1},
        {"unit_id": 3,"result": 1},
        {"unit_id": 4,"result": 1}
    "unit_list": [
        {"unit_id": 123456781, "pos_id": 1},
        {"unit_id": 123456782, "pos_id": 2},
        {"unit_id": 123456783, "pos_id": 3},
        {"unit_id": 123456784, "pos_id": 4}
    "position": {"island_id": 1, "pos_x": 14, "pos_y": 22}

The guess I was 99% sure of. (9)% was confirmed - in this game the client decides whether the battle is won and the server only gets the result.
I switched the breakpoints back and, just in case, did not change anything - there were data that I did not understand, in particular unit_status. In that battle, I managed to kill one of the four enemy monsters, so I guessed that a bunch of id-result sent to the server data about the killing of a monster (2 - dead, 1 - alive). A little later, I realized that this is true. Perhaps this data is used as an additional check for victory and the search for attackers, but, apparently, their main goal is loot from monsters. If you remember, at the very beginning I wrote the following:
как я говорил в описании, с мобов падают не только синие кристаллы. Довольно часто падает энергия и изредка красные кристаллы, причем вы видите, что и сколько упало при убийстве моба. И вот тут возникает мысль: неужели клиент решает, сколько и чего упадет, а потом отправляет на сервер? Если так, то отправив «правильный» запрос, мы не только сможем «выиграть» подземелье, которое нам не по зубам, но еще и забрать с него пару-тройку сотен красных кристаллов и энергии.

It turns out that when you enter the dungeon, the server creates a list of monsters, binds to each loot and sends data to the client. Therefore, you immediately see in the game what and how much has fallen - the client simply displays the previously received information. Later, he does not send materials that you earned, but only the status of monsters - dead or alive (I will explain why this is necessary in case of victory: the game has locations that can be reached without killing all monsters at the level, for example, a boss + two monsters In this case, you only need to kill the boss and you will go further). The server receives this information, compares it with its loot table and assigns you experience / resources depending on the killed monsters. Just to get experience you send a list of your monsters participating in the battle.

In theory, everything that we do with the help of the debugger looks quite legitimate for the game - victory, all monsters are dead. It seems that there is nothing to ban us from. However, a little later I found out one unpleasant moment. As I said, locations are divided into levels - three or more. Each level has its mobs. For example, imagine this situation:

Stage 1 - 3 monsters (we killed them, went further)
Stage 2 - 4 monsters (here we killed one and died)
Stage 3 - 3 monsters (we did not enter this level, since we died earlier)
Now, if we intercept JSON before encryption, we get roughly the following results:

"unit_status": [
    // Stage 1
    {"unit_id": 1,"result": 2},
    {"unit_id": 2,"result": 2},
    {"unit_id": 3,"result": 2},
    // Stage 2
    {"unit_id": 4,"result": 2},
    {"unit_id": 5,"result": 1},
    {"unit_id": 6,"result": 1},
    {"unit_id": 7,"result": 1}

As you can see, due to the fact that we did not reach the third level, we did not upload data about monsters on it. Even if we change the request to “win” and the statuses of existing monsters to “dead”, we still will not be able to get to the three remaining mobs on the third level. And for the server they are alive, even in the answer it comes that we won. Theoretically, they can catch and ban on this. But I'm still alive :)
At the same time, there is only one level in the arena, and the request from it looks 100% true.

Now everything has become clear. Although we can’t get an unlimited number of red crystals, we can still win any battle we wish. However, for this you have to keep the device connected to the computer, constantly switch breakpoints and manually redo the request before encryption. This is not very convenient. In theory, we do not need too much: always write the unit in “win”, and in the “result” two for all mobs.

We are looking for code that adds “win” to JSON. Having tinkered a bit with breakpoints, I found this piece.

MOVS   R0, R6
BLX     __floatsidf
BL      cJSON_CreateNumber
LDR     R1, =(unk_5D8C3240 – 0x5D84666E) ; "win"
MOVS    R2, R0
LDR     R0, [SP,#0x30+var_2C]
ADD     R1, PC
BL      cJSON_AddItemToObject

Apparently, in R6is what we need - the meaning of victory. Now we need to change this instruction to automatic victory assignment.
It is worth noting that the game has the ability to exit the battle at any time, and defeat is counted. That is, we always have the opportunity to lose, and we can expect that to R6always 2. But it was not very clear to me whether there would simply be 2 (0x2) or “2” (0x32 ASCII).
The point is small - change the instructions. Unfortunately, Ida does not allow changing the ASM code, so you have to change the bits of the instructions.

Do you want to learn how to program on zeros and ones? I have them!

Indeed, I got some special pleasure programming with two buttons and learning the instructions. Right hereand here are good materials and tips on instructions.
Go to the hex editor, see how it all looks from the inside.
MOVS R0, R6 ; 321С
We turn over the instruction, we get 1C32. In binary form:
0001 1100 0011 0000
As you can see, double-byte instructions are used. This is not ARM (four-byte ones are used there), but Thumb or Thumb-2.
However, you will not find instructions MOVSthat would look like this. In fact, the instruction looks like ADDS R0, R6, #0. During the analysis, Ida transforms the instructions into a more convenient way, which may cause minor editing problems.

0001110 000 110 000
ADDS    Imm Rn  Rd

ADDS- this part is constant for this instruction
Imm- immediate value. The immediate meaning that we want to add.
Rn- the register to which we will add.
Rd- destination register. A register that will store the amount after the execution of the instruction.

So, we need to replace this instruction. Due to the fact that I did not know which particular deuce is used, I chose the following option: The
SUBS R0, R6, #1
instruction will SUBSsubtract from R6unity and put the result in R0. As a result, no matter which deuce is in R6, the R0necessary unit will be in.
We translate in binary format:

0001111 001 110 000
SUBS    Imm Rn  Rd

In hexadecimal - 1E70. Flip - 701E. Now replace this instruction in the library.
It was:
It has become:
SUBS R0, R6, #1

Just in case, with the help of a breakpoint we check the registers at the entrance to the function AESConvertEncodeand make sure that everything is correct.

It remains to replace only the instruction responsible for assigning status to monsters.
She is right there, just below.

ADD     R8, PC          ; "unit_id"
MOV     R9, R3
ADD     R9, PC          ; "result"
BL      cJSON_CreateObject
MOVS    R4, R0
LDMIA   R7!, {R0,R1}
BLX     __floatundidf
BL      cJSON_CreateNumber
MOV     R1, R8
MOVS    R2, R0
MOVS    R0, R4
BL      cJSON_AddItemToObject
LDMIA   R6!, {R0}
BLX     __floatsidf
BL      cJSON_CreateNumber
MOV     R1, R9
MOVS    R2, R0
MOVS    R0, R4
BL      cJSON_AddItemToObject
ADDS    R5, #1
MOV     R0, R10
MOVS    R1, R4
BL      cJSON_AddItemToArray
CMP     R5, R11
BNE     loc_5D8466E2

Here the array is traversed in a for loop.

In R11the length of the array is stored, the loop is recognized by three lines:

ADDS R5, #1       ; инкремент счетчика
CMP  R5, R11      ; сравнение счетчика с длинной массива
BNE  loc_5D8466E2 ; если не равны - в начало цикла

We need a register - R6. The instruction LDMIAreads one byte from R6, moves R6one byte further and writes the value of the received byte to R0. We don’t need such difficulties, we need to write in R0two -MOVS R0, #2

00100 000 00000010
MOVS  Rd  Imm

Hex - 2002. Turn over ( 0220) and replace.
Moment of truth: turn off all breakpoints, go into the dungeon ...

Profit! The most important thing is not to forget that you must always leave the battlefield. If you accidentally win, a defeat will be sent to the server. Now you can even disconnect the device from the computer. Until the application restarts, a modified library will hang in its memory.

Instead of a conclusion

Hacking of this application became possible largely due to the fact that the authors themselves actually revealed that they consider the result of the battle on the client. At the same time, in a mobile online game, it is necessary to observe the line between security and user convenience. If the game will require a constant connection, its audience will decrease. The developers tried to complicate the life of potential crackers: they made data encryption and removed the main game code in a shared object. But one thing confuses me - why aren't function names obfuscated? I did not program in C / C ++ and I do not know if there is such an option in the compiler. But if all the functions were called " sub_xxxxxxxx", then the time spent on hacking the application would increase significantly. I will be glad to hear the answer in the comments.

Thanks to those who read to the end.
About grammar or spelling errors, please write in PM.
About logical or crude technical - better in the comments.

Also popular now: