Challenge Overview
Binary: bedrockbank.apk — Android app
Goal: Find valid account key in format BRCK-XXXX-XXXX-XXXX-XXXX
Flag: dach2026{y4bb4_d4bb4_d00_1t!}
Recon
Unpacked the APK. Interesting components:
KeyValidator.smali— Kotlin-side preprocessing and format checkingNativeValidator.smali— JNI wrapper with two native methodslibbedrockbank.so— native C validation (the real logic)
Validation Flow
User key "BRCK-g1-g2-g3-g4"
↓
KeyValidator.validateFormat (regex sanity)
↓
KeyValidator.performFullValidation
→ crc = crc16(g1, g2)
→ g3' = g3 XOR 0xA5C3
→ interleaved = interleave(g1, g2)
→ mixedToken = mix(g1, g2, g3)
↓
NativeValidator.validateFull(g1, g2, crc, g3', interleaved, g4, mixedToken)
↓ (if true)
NativeValidator.deriveFlag(g1, g2, g3, g4, kotlinToken)
↓
"dach2026{...}"
Reversing the Kotlin Side
Three helper functions translated from smali:
computeChecksum(g1, g2): CRC-16/CCITT-FALSE over (g1 << 16) | g2, poly 0x1021, init 0xFFFF.
interleaveValues(g1, g2): Weaves nibbles of g1 and g2 alternately into a 32-bit word, then rotates left 5 bits.
mixValues(g1, g2, g3): 7-round mixer:
state = 0x5A5A5A5A ^ g1
for r in range(7):
state = rotl32(state, 3)
state = state ^ (g2 + r)
state = state + rotr32(g3, r*2)
state = state ^ ((state >> 16) | (state << 16)) # halves-swap XOR
Gotcha: Dalvik’s shr-int is arithmetic (sign-extending). My first Python port was wrong on this detail. Used Frida to calibrate against real JVM output.
Reversing the Native Side (Ghidra)
Two exports: validateFull and deriveFlag.
Notable internal functions:
FUN_00010f10(x)—~(x*x + x) & 1— always returns 1 (decoy)FUN_00010d70()— reads/proc/self/status, checksTracerPid: 0(anti-debug)
validateFull Constraints
After mapping every FUN_00010e40(x, y) != 0 as x == y:
- Weighted nibble sum on g1 == 0x5E
popcount((g1 ^ g2) & 0xFFFF) == 5crc(g1, g2) == 0xC430rotl16(g3', 3) XOR 0xDEAD == 0xF646mixedToken == 0x12788169g4derived from g1, g2, crc, interleaved
Constraint 4 solves immediately:
rotl16(g3', 3) = 0xDEAD XOR 0xF646 = 0x28EB
g3' = rotr16(0x28EB, 3) = 0x651D
g3 = g3' XOR 0xA5C3 = 0xC0DE
g3 = 0xC0DE. The “code” in the “code block.” Nice touch.
deriveFlag — The Second Gate
This was the key anti-bypass technique: deriveFlag computes flag bytes from g1..g4, then runs a weighted sum integrity check. If wrong, returns "INVALID". So dach2026{INVALID} is returned for any wrong key — even if you bypass validateFull.
This forces you to find the real key. No shortcuts.
Solving for the Key
Strongest constraint is C5: mix(g1, g2, 0xC0DE) == 0x12788169 — a 32-bit equation in (g1, g2).
Calibrated mixValues against the real JVM via Frida:
var m = Class.forName('com.bedrockbank.app.KeyValidator')
.getDeclaredMethod('mixValues', ...);
m.setAccessible(true);
// mixValues(0x1234, 0x5678, 0xC0DE) = 0x55E38A3C
Brute-forced C5 in C (2^32 iterations, ~45 seconds):
for (uint32_t g1 = 0; g1 < 0x10000; g1++)
for (uint32_t g2 = 0; g2 < 0x10000; g2++)
if (mix_values(g1, g2, 0xC0DE) == 0x12788169)
printf("%04X %04X\n", g1, g2);
Yielded 2560 candidate pairs. Filtered through nibsum + popcount + CRC — exactly one survivor.
Computed g4 from the C6 formula, assembled the key, entered it.
ACCOUNT KEY: BRCK-XXXX-XXXX-C0DE-XXXX
App response: Yabba Dabba Doo! Vault Opened!
FLAG: dach2026{y4bb4_d4bb4_d00_1t!}
Key Takeaways
- Always calibrate against ground truth. One Frida reflective call saved hours of wrong-track debugging on the signed-shift bug.
- The second gate pattern is a clean anti-bypass technique. Splitting “does the key validate” from “does the output look right” forces attackers to find the real solution.
- Brute + filter beats analytical inversion. C5 has ~2560 collisions in 2^32 space; the other constraints prune to one. Pragmatic and fast.
- Decoy constraints exist.
FUN_00010f10always returns 1 — looks like a check, does nothing.