Only Give Me Flag
Only Give Me Flag
Platform: hackerlab | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Windows | Author: XESXOR | Date: 2026-04-06 | Status: Solved Techniques: static_reverse_engineering, aes_cbc_decryption, format_constraint_recovery, loop_inversion
Summary
Task: a PE32+ Windows GUI crackme validates both a passphrase and a strictly formatted CODEBY flag made of four hex groups. Solution: recover the AES-CBC passphrase from embedded key material, reconstruct the flag constraints, invert the long arithmetic loop, and verify the recovered flag offline.
Only Give Me Flag — hackerlab
Description
only_give_me_flag
English summary: we are given a Windows GUI executable with separate PASS and FLAG inputs. The goal is to recover the hidden passphrase, understand the flag checker, and reconstruct the exact CODEBY{...} flag.
Analysis
Recon
The binary only_give_me_flag.exe is a PE32+ Windows GUI x86-64 executable. Early triage already revealed the challenge structure.
Important imports:
GetWindowTextWMessageBoxWwcstouliswxdigitwcsncmp
Important UI strings:
CODEBYPASS:FLAG:CheckCODEBY{Noooo...FLAG!!!It's a flag! Congratulations! :)
This strongly suggests a classic GUI flag checker with two layers:
- a passphrase gate,
- then a strict flag validator.
The main checker function is fcn.140001ba0.
Passphrase Check
The helper fcn.140001400 and the setup code in .text match AES-128 key expansion plus one-block encryption.
The entered wide string is converted with wcstombs, then handled as follows:
- Convert UTF-16 input to bytes.
- Pad to 16 bytes with
0x01. - XOR the block with the IV.
- Encrypt one block with AES-128.
- Compare against a stored 16-byte ciphertext.
Recovered embedded material:
- Key dwords:
0xefb823d6,0xc3ce2662,0x12554ce2,0xe773e87d - IV dwords:
0x2d314218,0xb6daeffc,0x0df149b9,0xbd7e7e03 - Expected ciphertext dwords:
0x4d0ad111,0x20d334e5,0x1ac837b4,0x30efd5bc
Interpreting those dwords in little-endian byte order gives:
text read-only • copy allowedkey = d6 23 b8 ef 62 26 ce c3 e2 4c 55 12 7d e8 73 e7 iv = 18 42 31 2d fc ef da b6 b9 49 f1 0d 03 7e 7e bd ct = 11 d1 0a 4d e5 34 d3 20 b4 37 c8 1a bc d5 ef 30
Decrypting that single AES-CBC block yields:
text read-only • copy allowedCODEBY_PASSCODE\x01
So the required passphrase is:
text read-only • copy allowedCODEBY_PASSCODE
Equivalent pseudocode:
c read-only • copy allowedbool check_pass(wchar_t *pass_w) { uint8_t key[16] = { 0xd6,0x23,0xb8,0xef, 0x62,0x26,0xce,0xc3, 0xe2,0x4c,0x55,0x12, 0x7d,0xe8,0x73,0xe7 }; uint8_t iv[16] = { 0x18,0x42,0x31,0x2d, 0xfc,0xef,0xda,0xb6, 0xb9,0x49,0xf1,0x0d, 0x03,0x7e,0x7e,0xbd }; uint8_t want[16] = { 0x11,0xd1,0x0a,0x4d, 0xe5,0x34,0xd3,0x20, 0xb4,0x37,0xc8,0x1a, 0xbc,0xd5,0xef,0x30 }; bytes p = wcstombs(pass_w); pad p to 16 bytes with 0x01; p[0:16] ^= iv; AES128_encrypt_block_in_place(p[0:16], expanded_key(key)); return memcmp(p[0:16], want, 16) == 0; }
Flag Format Recovery
The flag parser is very strict.
It requires exactly 43 characters in this form:
text read-only • copy allowedCODEBY{XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX}
Where:
- each
Xis a hexadecimal digit, - there are exactly three hyphens,
- the final character is
}.
The program parses the four groups with wcstoul(..., 16):
- group 1 ->
a - group 2 ->
b - group 3 ->
c - group 4 ->
n
The last group is additionally constrained to:
text read-only • copy allowedn == 0x0101234F
Arithmetic Loop
The checker derives a constant from the stored ciphertext bytes:
c read-only • copy alloweduint32_t edx = 0; for (byte x : want_ciphertext_16_bytes) edx += x; for (byte x : want_ciphertext_16_bytes) edx ^= x;
Using the ciphertext bytes 11 d1 0a 4d e5 34 d3 20 b4 37 c8 1a bc d5 ef 30, this becomes:
text read-only • copy allowededx = 0x780
Then the program runs a long 32-bit loop n times:
c read-only • copy allowedbool check_flag(wchar_t *s) { if (wcslen(s) != 43) return false; if (wcsncmp(s, L"CODEBY{", 7) != 0) return false; if (s[42] != L'}') return false; if (count('-', s) != 3) return false; uint32_t a = wcstoul(s+7, NULL, 16); uint32_t b = wcstoul(s+16, NULL, 16); uint32_t c = wcstoul(s+25, NULL, 16); uint32_t n = wcstoul(s+34, NULL, 16); if (n != 0x0101234f) return false; uint32_t edx = 0; for (byte x : want_ciphertext_16_bytes) edx += x; for (byte x : want_ciphertext_16_bytes) edx ^= x; for (uint32_t i = 0; i < n; i++) { a ^= b; uint32_t t = a + c; a -= 0x539; c = (b ^ 0x33333) + a; t -= 0x22b; c ^= t; c ^= edx; a ^= edx; a ^= 0x15b6; b ^= edx; b ^= 0x33333; b += 0x531b92; c += 0xbcb0bbbb; } return a == 0x5e0fc1d7 && b == 0xd3dac7a5 && c == 0x68742ffb && n == 0x0101234f; }
Final target values after the loop:
a == 0x5e0fc1d7b == 0xd3dac7a5c == 0x68742ffbn == 0x0101234f
Reversing the Loop
Since the binary checks the final state after n rounds, the direct path is to invert one loop iteration and run that inverse n times from the known target state.
Doing that reconstructs the original four hex groups:
8F24778FABBB4468BFAD02A00101234F
So the full flag is:
text read-only • copy allowedCODEBY{REDACTED}
Solution
The following Python script recovers the passphrase by AES-CBC decryption, inverts the arithmetic loop, reconstructs the flag, and verifies that the recovered flag satisfies the forward checker.
python read-only • copy allowed#!/usr/bin/env python3 from Crypto.Cipher import AES MASK = 0xFFFFFFFF def u32(x): return x & MASK def recover_passphrase(): key = bytes.fromhex("d623b8ef6226cec3e24c55127de873e7") iv = bytes.fromhex("1842312dfcefdab6b949f10d037e7ebd") ct = bytes.fromhex("11d10a4de534d320b437c81abcd5ef30") pt = AES.new(key, AES.MODE_CBC, iv).decrypt(ct) return pt.rstrip(b"\x01").decode(), ct def derive_edx(ct: bytes) -> int: edx = 0 for x in ct: edx = u32(edx + x) for x in ct: edx ^= x return edx def invert_round(a1, b1, c1, edx): b0 = u32((b1 - 0x531B92)) ^ edx ^ 0x33333 a3 = a1 ^ edx ^ 0x15B6 a2 = u32(a3 + 0x539) a0 = a2 ^ b0 c2 = u32(c1 - 0xBCB0BBBB) x = u32((b0 ^ 0x33333) + a3) y = x ^ c2 ^ edx c0 = u32(y + 0x22B - a2) return a0, b0, c0 def forward_round(a, b, c, edx): a ^= b t = u32(a + c) a = u32(a - 0x539) c = u32((b ^ 0x33333) + a) t = u32(t - 0x22B) c ^= t c ^= edx a ^= edx a ^= 0x15B6 b ^= edx b ^= 0x33333 b = u32(b + 0x531B92) c = u32(c + 0xBCB0BBBB) return a, b, c def main(): passphrase, ct = recover_passphrase() edx = derive_edx(ct) assert edx == 0x780 n = 0x0101234F a, b, c = 0x5E0FC1D7, 0xD3DAC7A5, 0x68742FFB for _ in range(n): a, b, c = invert_round(a, b, c, edx) parts = [a, b, c, n] flag = "CODEBY{" + "-".join(f"{x:08X}" for x in parts) + "}" # Verify by running the original forward logic va, vb, vc = parts[:3] for _ in range(n): va, vb, vc = forward_round(va, vb, vc, edx) assert (va, vb, vc, n) == (0x5E0FC1D7, 0xD3DAC7A5, 0x68742FFB, 0x0101234F) print("PASS:", passphrase) print("FLAG:", flag) if __name__ == "__main__": main()
Expected output:
text read-only • copy allowedPASS: CODEBY_PASSCODE FLAG: CODEBY{REDACTED}
Verification
Independent verification used during solving confirmed both parts:
- AES-CBC decryption of the stored block returns
CODEBY_PASSCODE\x01. - Running the forward arithmetic loop on
REDACTEDproduces exactly the target state checked by the binary.
That matches the recovered passphrase and the final accepted flag.