Result
- Key:
C0ldB34c0n::unl0ck::7a3f - Flag:
hiCTF{c0ld_b34c0n_cfg_uns34l3d_fr0m_th3_s4mpl3}
$ ./cold-beacon 'C0ldB34c0n::unl0ck::7a3f'
hiCTF{c0ld_b34c0n_cfg_uns34l3d_fr0m_th3_s4mpl3}
Files
extracted/cold-beacon- ELF x86-64, stripped, non-PIE, dynamically linked, compiled from Nim (evidence:syncio.nim,cmdline.nim,IOError,paramStr,readFile, Nim signal handlers).cold-beacon.zip,chall-info.txt.
Triage / analysis
- Takes one argument (
./cold-beacon <key>). Wrong/absent key printsconfig sealed. strings/rodata showed:- a TracerPid anti-debug: it reads
/proc/self/statusand bails ifTracerPidis non-zero (function0x403aec). - an obfuscated 14-byte string at
.rodata 0x405e00(954<3=z)?;6?>P) - decoded at runtime byXOR 0x5a→"config sealed\n"(function0x4041f9). - SHA-256 (K constants at
0x405e80, IV at0x405f80, custom compression routine at0x403de7). - an encrypted config blob at
.rodata 0x405e40(47 bytes) and a 32-byte expected SHA-256 value at0x405e20.
- a TracerPid anti-debug: it reads
Key check (function 0x4043c0 / 0x403cff / 0x403dc4)
Arguments are the Nim argv string; Nim stores a string as {len; data…} so the key bytes live at argv_ptr + 8, length must be 24.
0x403cff is a table-driven validator over 23 constraint groups. For each group it indexes key bytes via a permutation table (0x405fc0) and applies one of three ops against an expected byte (0x405fa0), with counts (0x406050) and opcodes (0x406070):
op 0/3/5→ XOR of the selected key bytes must equal expectedop 1/4→ (mod-256) SUM of the selected key bytes must equal expectedop 2→ product (low byte) of two selected key bytes must equal expected
0x403dc4 adds three more relations (key index = raw offset − 8):
key[10] = (0xa8 - key[9]) & 0xffkey[20] = key[12] ^ key[4](key[12] + key[13]) & 0xff == 0xe3
Solving the whole system (all constraints are linear/byte-local, so any solver works - used with a printable-ASCII constraint):
KEY: C0ldB34c0n::unl0ck::7a3f
Config decryption (function 0x404333)
Once the key passes (and TracerPid == 0):
sha256(key)(0x403de7) is computed into a 32-byte value.- Keystream generated by
0x404119(sha256, 47): repeatedlysha256(state32 || be32(counter))for counter = 0,1,… concatenating digests until 47 bytes are produced (SHA-256-based DRBG/CTR). - Ciphertext =
.rodata 0x405e40(47 bytes); plaintext = ciphertext XOR keystream. - It first compares
sha256(key)against the stored value at0x405e20(integrity/self-check). Then prints the decrypted config, i.e. the flag string.
Commands used
1file extracted/cold-beacon
2readelf -S/-d extracted/cold-beacon
3objdump -d extracted/cold-beacon # located refs to 0x405e00/0x405e20/0x405e40
4objdump -s -j .rodata extracted/cold-beacon # extracted tables + blobs
5
6# Bypass the TracerPid anti-debug for dynamic analysis (LD_PRELOAD shim
7# redirects /proc/self/status to a file with "TracerPid:\t0");
8# then gdb at 0x403cff / 0x403dc4 to confirm the string ABI and constraint logic.
9
10# Solve key constraints (z3)
11python3 solve.py # -> C0ldB34c0n::unl0ck::7a3f
12
13./extracted/cold-beacon 'C0ldB34c0n::unl0ck::7a3f'
Notes
- Non-PIE → absolute addresses in gdb work directly.
- The TracerPid check only matters under a debugger; a normal run passes it, so the real key alone is sufficient (verified without any preload).
The validators are not enough
One catch: the 23 table groups (0x403cff) + 3 relations (0x403dc4) do not pin the
key - they are necessary but not sufficient.
- Enumerating every printable-ASCII key of length 24 that satisfies all 26 relations
yields exactly 4 candidates:
C ltB#$c0n::unl0ck::7a3fCl4Bcdc0n::unl0ck::7a3f`Cpl$Bstc0n::unl0ck::7a3fC0ldB34c0n::unl0ck::7a3f← only one that unlocks
- Confirmed in gdb: the first three pass both
0x403cffand0x403dc4(al=1 for each) yet the binary printsconfig sealed. So the validators alone are a decoy-friendly check. - The real gate is at
0x4043c8: after decrypting.rodata 0x405e40, it computessha256(plaintext)and compares against the 32-byte pin at.rodata 0x405e20. The stored pin is sha256 of the flag, NOT sha256(key) (verified:sha256(b"hiCTF{c0ld_b34c0n_cfg_uns34l3d_fr0m_th3_s4mpl3}")==0502f285…66ad40). Because the keystream is seeded bysha256(key), only the single key whose seed reproduces the pinned plaintext passes — a classic tag-pinned key recovery. - Keystream
0x404119:sha256(key)→ DRBGsha256(state32 || be32(counter)), counter = 0,1,… concatenated to 47 bytes.pt = ct XOR ks→ flag. Reproduced byte-exact offline.
Result
- Key:
C0ldB34c0n::unl0ck::7a3f - Flag:
hiCTF{c0ld_b34c0n_cfg_uns34l3d_fr0m_th3_s4mpl3}