Tinkoff CTF 2024: demo analysis

Initially, the proxy listens on the localhost, the emulator will not be able to connect to it. Proxy → ⚙️ Proxy Settings → Proxy listeners → Edit → select any IPv4 except 127.0.0.1.

2. Register a proxy in Android.

In the advanced settings of the AndroidWifi virtual Wi-Fi network, we configure the use of a proxy. The IP address is the one you just selected in Burp, port 8080.

3. Add a trusted certificate.

To intercept encrypted traffic (HTTPS), Burp re-encrypts it with its own key. So that Android doesn’t mind, let’s add the Burp certificate to the trusted ones.

In the browser on the phone we go to http://burp and download the certificate in the corner. In the settings we find the item CA certificate and add the downloaded certificate as a CA.

4. Check what happened.

We go to a website in a mobile browser and see the browser traffic in the tab HTTP history.

Now let's launch the application with thimbles and see its HTTP traffic – it communicates with the server t-trickster-jbi8aw9z.spbctf.ru via HTTPS.

Peeping into the thoughts of a swindler

Peeping into the thoughts of a swindler

Each time before mixing the thimbles, the application receives an array of lists of three numbers (0, 1, 2) from the API handle /next/. It is reasonable to assume that this is the secret sequence for shuffling the thimbles in this round.

After observing what numbers come from the server, how the thimbles are mixed and where the ball ends up, you can get the following picture:

  1. At the beginning of each round, the thimbles are numbered in order: 0, 1, 2.

  2. Each list in the array is one shuffle position. For example, if the first element [1,0,2]this means that the left thimble (id 0) will go in the middle, and the middle one (id 1) will go on the left.

  3. The last element in this array is the final order of thimbles. At the start of the round, we are shown which of the thimbles (0, 1 or 2) the ball is under. This means that we just need to find the position of this thimble in the final location.

Method 2: Patching the application. The second way to radically bypass security measures is to edit the application and remove them from there.

In our case, the means of defense that we want to neutralize are the various tricks that the sharper uses to confuse us: quick mixing, hand blocking. All this is stored in the application itself, and there are convenient methods to modify it.

Let's arm ourselves with a tool buildapp for rebuilding Android applications. Install on Linux:

root@vosus:/mnt/f# pip install buildapp --upgrade && buildapp_fetch_tools
Collecting buildapp
  Downloading buildapp-1.4.0-py3-none-any.whl (8.9 kB)
		<...>
downloading apktool ...
downloading completed!

And we’ll sort out ours for them .apk into components:

root@vosus:/mnt/f# apktool decode robotrickster.apk
I: Using Apktool 2.9.3 on robotrickster.apk
I: Loading resource table...
I: Decoding file-resources...
I: Loading resource table from file: /root/.local/share/apktool/framework/1.apk
I: Decoding values */* XMLs...
I: Decoding AndroidManifest.xml with resources...
I: Regular manifest package...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
I: Copying META-INF/services directory

The tool created the folder robotrickster/, in which the various components of the application are laid out. The most interesting to us:

  • AndroidManifest.xml — a description of the application for the installer, which, for example, lists the privileges requested by the application and the link handlers.

  • smali/ — a folder with application code disassembled into the bytecode language of the Dalvik virtual machine, which runs applications for Android. The assembly code here can be edited, changing the logic of the application.

  • res/ — a folder with all the resources that the application uses: pictures, pieces of text, animations, interface descriptions.

The straight forward way to remove bugs is to find each one in the Smali code and remove it from there. But patching application code we looked at the iZba task in last year’s analysisso we'll take a slightly different approach this time.

Find a picture of a thimble among the application resources:

res/drawable/cup.png

res/drawable/cup.png

And let's make a hole in it!

Let's save the image and rebuild the application using buildapp:

root@vosus:/mnt/f# buildapp -o robotrickster_patched.apk -d robotrickster
Executing `apktool b robotrickster -o robotrickster_patched.apk_prealign`
Executing `zipalign -p -f -v 4 robotrickster_patched.apk_prealign robotrickster_patched.apk`
Executing `apksigner sign --ks-key-alias defkeystorealias --ks /root/.reltools/buildapp-keystore.jks robotrickster_patched.apk`
buildapp completed successfully!

The modified application was built successfully, install it on the emulator and run it:

We see right through all his tricks

Method 3. Decompile and go to the server. It's often not possible to fully understand how an application works just by looking at its traffic and digging into its resources. Fortunately, Android applications are written in Java, and its bytecode is easily decompiled back into readable code.

Our goal in this version of the task is to create a script that will automatically guess the thimble with a ball, without requiring us to interact with the application. This means we need to study how the application interacts with its server.

To do this, it is convenient to use a decompiler JADX – upload the file into it .apkand he does everything himself and shows all the application code in the form of a convenient class tree.

You can determine where the code of the application itself is by looking at the file AndroidManifest.xml. We look for the tag – this means the application code in the namespace com.spbctf.robotrain

Let's read the decompiled code and assemble from it the parts that are related to the logic of communication with the server. Here are the relevant snippets:

package com.spbctf.robotrain.game.domain;

// В этом классе URL сервера
public class GameServiceFactory {
    public GameService create() {
        return (GameService) new Retrofit.Builder().baseUrl("https://t-trickster-jbi8aw9z.spbctf.ru/").addConverterFactory(GsonConverterFactory.create()).build().create(GameService.class);
    }
}

// В этом интерфейсе доступные API-методы
public interface GameService {
    // Начать новую игру, вызывается только при старте приложения
    @PUT("/start/{uuid}")
    Call<ResponseBody> start(@Path("uuid") String str);

    // Запустить новый раунд
    @GET("/next/{uuid}")
    Call<List<List<Integer>>> getNext(@Path("uuid") String str);

    // Отправить попытку угадать напёрсток
    @GET("/accept/{uuid}/{answer}")
    Call<GameState> accept(@Path("uuid") String str, @Path("answer") Integer num);
}

// Пробежимся по местам, где используется каждый метод API.
// Для этого в JADX в меню по правой кнопке на функции есть пункт Find Usage (x)

// 1. start — начало новой игры
public class FlagController {
    private byte[] flag = new byte[0];

    public FlagController(GameService gameService) {
        gameService.start(UUIDController.uuid).enqueue(new Callback<ResponseBody>() {
            @Override // retrofit2.Callback
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                // Метод start присылает ‘флаг’, который позже используется в decrypt
                FlagController.this.flag = response.body().bytes();
            }
        });
    }

    // Функция decrypt берёт сохранённый флаг и расксоривает ключом, переданным в bArr
    public String decrypt(byte[] bArr) {
        byte[] bArr2 = new byte[this.flag.length];
        int i = 0;
        while (true) {
            byte[] bArr3 = this.flag;
            if (i < bArr3.length) {
                bArr2[i] = (byte) (bArr3[i] ^ bArr[i % bArr.length]);
                i++;
            } else {
                return new String(bArr2, StandardCharsets.UTF_8);
            }
        }
    }
}

// 2. next — запуск нового раунда
package com.spbctf.robotrain.game.presentation;

public class GameViewModel extends AndroidViewModel {
    public void getNextTransposition(Callback<List<List<Integer>>> callback) {
        this.blockPlayButton.setValue(true);
        this.gameService.getNext(UUIDController.uuid).enqueue(callback);
    }

    public /* synthetic */ void lambda$initGameObject$5() {
        this.gameViewModel.getNextTransposition(new Callback<List<List<Integer>>>() {
            @Override // retrofit2.Callback
            public void onResponse(Call<List<List<Integer>>> call, Response<List<List<Integer>>> response) {
                // От next приходит только список положений напёрстков,
                // который передаётся дальше в одну из 4 функций перемешивания
                int type = TypeController.getType(MainActivity.this.gameViewModel.getScore().getValue().intValue());
                if (type == 0) {
                    MainActivity.this.fastshuffled(50, response);
                } else if (type == 1) {
                    MainActivity.this.joinShuffle(response);
                } else if (type == 2) {
                    MainActivity.this.closedShuffle(response);
                } else {
                    MainActivity.this.fastshuffled(500, response);
                }
            }
        });
    }
}

// 3. accept — отправка угаданного напёрстка на сервер
public class GameViewModel extends AndroidViewModel {
    public void clickByCup(int i) {
        this.gameService.accept(UUIDController.uuid, Integer.valueOf(this.currentPosition.getValue().lastIndexOf(Integer.valueOf(i)))).enqueue(new Callback<GameState>() {
            @Override // retrofit2.Callback
            public void onResponse(Call<GameState> call, Response<GameState> response) {
                // От accept приходит ‘score’ — сколько раз подряд угадали
                // и ‘key’ — пока не набрался миллион, в нём null
                GameViewModel.this.score.setValue(Integer.valueOf(response.body().getScore()));
                GameViewModel.this.update(response.body().getKey());
            }
        });
    }

    // А вот мы и добрались до вывода флага на экран
    public void update(byte[] bArr) {
        if (bArr == null || bArr.length == 0) {
            return;
        }
        // Если в ‘key’ пришёл не null, расшифровываем этим ключом флаг
        this.blockPlayButton.setValue(true);
        this.flag.setValue("Шулер-андроид повержен!\nЕго последние слова:\n" + this.flagController.decrypt(bArr));
    }
}

Let’s summarize what the application protocol for working with the server looks like:

  1. Server API URL – https://t-trickster-jbi8aw9z.spbctf.ru.

  2. When starting the application, PUT /start/ jerks — the response body is saved as an encrypted flag.

  3. When a new round starts, GET /next/ is triggered, in response comes an array of arrays of numbers, already familiar to us from method No. 1, a list of thimble positions. The final position is the last element of the array.

  4. When selecting a thimble, GET /accept/ twitches/*, an object with fields arrives score And key. If key not empty, we won. We open the flag with this key.

Let's write a script in Python that will communicate with the server under the guise of an application:

#!/usr/bin/python3
import requests, uuid

API_URL = "https://t-trickster-jbi8aw9z.spbctf.ru"

# 1. 'start' the game
gameid = str(uuid.uuid4())
result = requests.put(f"{API_URL}/start/{gameid}")
encryptedFlag = result.content
print(f"Game started, got encrypted flag: {encryptedFlag}")

# The ball starts under the middle cup (0, _1_, 2)
ballIsUnderCupNo = 1

while True:
    # 2. 'next' round request
    result = requests.get(f"{API_URL}/next/{gameid}")
    shuffles = result.json()
    lastShuffle = shuffles[-1]
    
    # Find ball cup's position in the last shuffle
    ballPosition = lastShuffle.index(ballIsUnderCupNo)
    # Update starting ball cup for the next round
    ballIsUnderCupNo = ballPosition
    
    # 3. 'accept' cup guess
    result = requests.get(f"{API_URL}/accept/{gameid}/{ballPosition}")
    result = result.json()
    print(result)
    
    if result['key'] is not None:
        # 4. Got non-null key, decrypt the flag!
        key = result['key']
        encryptedFlag = bytearray(encryptedFlag)
        for i, c in enumerate(encryptedFlag):
            encryptedFlag[i] ^= key[i % len(key)] & 0xFF  # Convert negative values to unsigned
        decryptedFlag = bytes(encryptedFlag)
        print(f"Flag: {decryptedFlag}")
        break

Let's launch:

root@vosus:/mnt/f# ./trick.py
Game started, got encrypted flag: b'\x80\x00\x88\xb1\xc9\xdbf\xda\xc4\x17\xa3\xa4\xc5\x988\xdc\x98\x06\x8e\x88\xc1\xca$\x8b\xc7\x00\x94\xb2\xc1\xf6"\xd0\xc7\r\xa3\xb5\xc0\xcc7\xd3\xc1<\x98\xe7\xc5\xc7+'
{'score': 1, 'key': None}
{'score': 2, 'key': None}
{'score': 3, 'key': None}
{'score': 4, 'key': None}
{'score': 5, 'key': None}
{'score': 6, 'key': None}
{'score': 7, 'key': None}
{'score': 8, 'key': None}
{'score': 9, 'key': None}
{'score': 10, 'key': None}
{'score': 11, 'key': None}
{'score': 12, 'key': None}
{'score': 13, 'key': None}
{'score': 14, 'key': None}
{'score': 15, 'key': None}
{'score': 16, 'key': None}
{'score': 17, 'key': None}
{'score': 18, 'key': None}
{'score': 19, 'key': None}
{'score': 20, 'key': [-12, 99, -4, -41, -78, -87, 86, -72]}
Flag: b'tctf{▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒}'

What else to watch to practice for tasks

  1. A selection of materials for beginners from the SPbCTF community: https://vk.com/@spbctf-ctf-for-beginners

  2. Analysis of last year's assignment on the Cringe Archive web to understand Burp Suite.

  3. Analysis of last year's assignment with the iZba Android application to consolidate Android skills.

Now let's start the competition

We analyzed one of the demo tasks to make it easier for newcomers to sports hacking to get the hang of it and decide to participate in CTF. If you still have questions, we invite you to look at the competition page – there we provide more information and links to additional materials. And if you are ready to solve problems to win, register quickly!

Similar Posts

Leave a Reply

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