( بِسْمِ اللَّـهِ الرَّحْمَـٰنِ الرَّحِيمِ )
CAUTION#FreePalestine
Brod & Co. — Android CTF Write-Up - BrunnerCTF 2025
1) Recon: it’s Flutter + native
From the APK:
AndroidManifest.xmlshows a standard FlutterMainActivity, no obvious networking components or custom services.- App bundles three native libs:
lib/x86_64/libapp.so,lib/x86_64/libflutter.so,lib/x86_64/libnative.so.
Logcat during coupon check:
DEBUG: validateCoupon called with: 'test code'
DEBUG: _callNativeValidation called with: 'test code'
Native library loaded successfully
Main function: process_data_complete
VULNERABILITIES ACTIVE: Buffer overflow, weak crypto, format string
DEBUG: Native library returned for coupon validation: 'INVALID_COUPON'When placing an order (with a long input):
Order data length: 256 bytes
--------- beginning of crash
FORTIFY: strcpy: prevented 257-byte write into 256-byte buffer
Fatal signal 6 (SIGABRT)Takeaway: the app calls into libnative.so for both validation and order handling. It even tells us vulnerable patterns exist.
2) Static: skim libnative.so in Ghidra
Useful exports I found:
process_data_complete
util_func_a / b / c / d
hidden_encode_function / hidden_decrypt_function
get_client_version / version_info
force_data_preservation
test_all_functionsTwo key functions (decompiled snippets I pulled):
- Coupon/flag entry point:
char *process_data_complete(char *in) {
if (strncmp(in, "COUPON:", 7) == 0) {
return FUN_001023a0(in + 7) ? strdup("VALID_COUPON") : strdup("INVALID_COUPON");
} else if (strncmp(in, "FLAG:", 5) == 0) {
if (FUN_001023a0(in + 5)) {
// build "FLAG|%s" using FUN_00103520()
}
return strdup("FLAG|INVALID_COUPON");
} else {
// default path uses FUN_00103520() to build "OK|%s"
}
}- Secret builder (what ultimately becomes the
%sabove):
undefined * FUN_00103520(void) {
// Mixes three embedded data blobs, rotates, XORs, permutes bits,
// PRF-like rounds with constants 0xcafebabedeadbeef and 0x8765432112345678,
// then copies only printable bytes into a global buffer and returns it.
// First call lazily initializes a global; subsequent calls return the same string.
return &DAT_00106090;
}And the helper I later used:
void *util_func_c(int x) {
if (x == 0x1337) {
char *s = FUN_00103520();
return strcpy(malloc(strlen(s) + 1), s);
}
return NULL;
}So: util_func_c(0x1337) hands you a heap-allocated copy of the final secret string that process_data_complete() would otherwise wrap as OK|%s or FLAG|%s.
3) Dynamic: confirm the overflow and its call site
I traced libc copies (safer Frida hooks) and reproduced the crash:
=== __strcpy_chk hit ===
destlen=256 srclen=250
returnAddress=... module=libnative.so offset=0x1868 <-- callsite
=== __strcpy_chk hit ===
destlen=256 srclen=256
... Abort: FORTIFY: strcpy: prevented 257-byte write into 256-byte bufferOffset 0x1868 is the __strcpy_chk return site inside libnative.so where the 256-byte limit triggers.
NOTEOn Ghidra address translation: Frida gave module offset
0x1868. In Ghidra, image base was0x00100000, so jump to0x00101868.
Around that address, my listing (function FUN_00101820) shows:
local_168at[rsp+0x160]is the 256-byte dest buffer.- The vulnerable call:
mov [rsp+local_30], 0x100 ; destlen = 256
mov rdi, [rsp+local_28] ; rdi = &local_168
mov rsi, [rsp+local_38] ; rsi = input
mov rdx, [rsp+local_30] ; rdx = 256
call __strcpy_chk ; → aborts if srclen+1 > 256- Then it copies into a bigger temp buffer, XORs each byte with
0xAA(“weak crypto”), and finally heap-copies with__strcpy_chk(..., -1)(no check).
So the overflow is real, but FORTIFY blocks it. I could have bypassed that check with Frida and continued, but… I found something faster.
4) The fastest working PoC (unintended, I guess, but valid)
Idea
Don’t fight validation or overflow. The native lib already exports a function that returns the final secret string: util_func_c(0x1337) → internally calls the secret builder and returns a heap string. Read it and print.
PoC
get_flag.js:
// frida -U -n dk.brunnerne.masterbaker -l get_flag.js
// or: frida -U -f dk.brunnerne.masterbaker -l get_flag.js --no-pause
(function () {
function waitForLib(name, cb) {
const tryFind = function () {
const m = Process.findModuleByName(name);
if (m) return cb(m);
setTimeout(tryFind, 100);
};
tryFind();
}
waitForLib("libnative.so", function (m) {
console.log("[+] libnative.so base:", m.base);
const util_func_c = Module.findExportByName("libnative.so", "util_func_c");
if (!util_func_c) { console.log("[!] util_func_c not found"); return; }
const freePtr = Module.findExportByName(null, "free");
const UtilFuncC = new NativeFunction(util_func_c, "pointer", ["int"]);
console.log("[*] Calling util_func_c(0x1337) ...");
const p = UtilFuncC(0x1337);
if (p.isNull()) { console.log("[!] util_func_c returned NULL"); return; }
try {
const s = Memory.readUtf8String(p);
console.log("\n===== SECRET / FLAG STRING =====");
console.log(s);
console.log("================================\n");
} catch (e) {
console.log("[!] Failed to read string:", e);
} finally {
if (freePtr) new NativeFunction(freePtr, "void", ["pointer"])(p);
}
});
})();How to run
Run the script:
frida -U -f dk.brunnerne.masterbaker -l get_flag.js
Output:
[+] libnative.so base: 0x7xxx...
[*] Calling util_func_c(0x1337) ...
===== SECRET / FLAG STRING =====
brunner{wh0_kn3w_dart_c0u1d_h4nd13_C?!}
================================FLAG = brunner{wh0_kn3w_dart_c0u1d_h4nd13_C?!}
5) Line-by-line PoC explanation
waitForLib("libnative.so", cb)Flutter loads libs lazily; this waits untillibnative.sois mapped before continuing.Module.findExportByName("libnative.so", "util_func_c")Resolves the exported symbol inside the target library. No offsets, no gadget hunting.new NativeFunction(util_func_c, "pointer", ["int"])Wraps the C function: returns a pointer, takes oneintparameter.UtilFuncC(0x1337)The magic value the library checks. If it matches, it callsFUN_00103520()and returns a heap-allocated C string (char *).Memory.readUtf8String(p)Reads the returned C string from target memory.free(p)Clean up the heap allocation (nice-to-have; not strictly required for a quick one-shot).
That’s it. No bypassing FORTIFY, no ROP chain, no coupon math.
