← Back to Writeups
CUSTOMMediumReversing

Only Give Me Flag

XESXOR9/2/20266 min read
#reversing#custom#medium#static_reverse_engineering#aes_cbc_decryption#format_constraint_recovery#loop_inversion

Only Give Me Flag

Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: static_reverse_engineering, aes_cbc_decryption, format_constraint_recovery, loop_inversion

Summary

Walkthrough of the CUSTOM Reversing Only Give Me Flag. 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, reconst

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:

  • GetWindowTextW
  • MessageBoxW
  • wcstoul
  • iswxdigit
  • wcsncmp ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Important UI strings:

  • CODEBY
  • PASS:
  • FLAG:
  • Check
  • CODEBY{
  • Noooo...
  • FLAG!!!
  • It's a flag! Congratulations! :)

This strongly suggests a classic GUI flag checker with two layers:

  1. a passphrase gate,
  2. 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: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

  1. Convert UTF-16 input to bytes.
  2. Pad to 16 bytes with 0x01.
  3. XOR the block with the IV.
  4. Encrypt one block with AES-128.
  5. 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
key = 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
CODEBY_PASSCODE\x01

So the required passphrase is:

text
CODEBY_PASSCODE

Equivalent pseudocode:

c
bool 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
CODEBY{XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX}

Where:

  • each X is 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
n == 0x0101234F

Arithmetic Loop

The checker derives a constant from the stored ciphertext bytes:

c
uint32_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
edx = 0x780

Then the program runs a long 32-bit loop n times: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

c
bool 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 == 0x5e0fc1d7
  • b == 0xd3dac7a5
  • c == 0x68742ffb
  • n == 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:

  • 8F24778F
  • ABBB4468
  • BFAD02A0
  • 0101234F

So the full flag is:

text
CODEBY{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
#!/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
PASS: 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 REDACTED produces exactly the target state checked by the binary.

That matches the recovered passphrase and the final accepted flag. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key Takeaways / Lessons

  • Mastering static_reverse_engineering, aes_cbc_decryption, format_constraint_recovery, loop_inversion was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-04-06 • CUSTOM • Only Give Me Flag • Difficulty: Medium