Neven Marić
Hidden Investigations CTF 2026

ReversingHidden Investigations CTF 2026

Cold Beacon

Stripped Nim x86-64 ELF. The real main is at 0x404271. The key must be exactly one 24-byte argument. It goes through a system of arithmetic constraints, then is used as the seed of a SHA-256 counter-mode keystream that decrypts the…

  • rev
  • nim
  • z3
  • constraint-solving
  • sha256
  • xor-keystream
  • anti-debug
  • linux

What this one teaches

  • Stripped Nim binaries still have recognisable structure — finding the real main by tracing from the entry point past the Nim runtime init.
  • Modelling mixed-type constraints (XOR, sum mod 256, product mod 256) in z3 recovers the key in seconds when the search space is just 24 printable-ASCII bytes.
  • SHA-256 counter-mode keystream is easily reimplemented once you spot the construction: SHA256(seed || BE32(counter)), truncated to the ciphertext length.

Task description

Evidence tag IR-2231-A, pulled off a workstation mid-engagement. One file, no other artifacts recovered, no notes from the box owner.

It expects one argument: ./cold-beacon <key>. With the right key it prints the config it was carrying; otherwise it just says it’s sealed.

Linux x86-64 ELF.

Key: C0ldB34c0n::unl0ck::7a3f

Analysis

  1. main (0x404271): requires argc==1 and len(key)==0x18, then calls two validators and an anti-debug check (0x403aec, reads TracerPid from /proc/self/status). On failure it prints the XOR-0x5a-decoded string “config sealed” (0x4041f9).
  2. Validator 0x403cff: table-driven, 23 constraints (tables in .rodata: targets @0x405fa0, index lists @0x405fc0 (6 bytes each), counts @0x406050, types @0x406070). Types 0/3/5 = XOR of key[idx], 1/4 = sum mod 256, 2 = product of two bytes mod 256; compared to target byte.
  3. Validator 0x403dc4: key[9]+key[10]==0xa8, key[20]==key[12]^key[4], key[12]+key[13]==0xe3 (mod 256).
  4. Decryption: h = SHA-256(key) (0x403de7, standard SHA-256, K constants @0x405e80). Keystream (0x404119) = SHA256(h || BE32(counter)) for counter 0,1,.. truncated to 0x2f bytes. plaintext = ct(@0x405e40, 0x2f bytes) XOR keystream. It is accepted only if SHA-256(plaintext) == the 32 bytes at 0x405e20, then written to stdout.

Recovery

Modelled the 26 constraints in z3 (24 printable-ASCII bytes). The first model produced C0ldB34c0n::unl0ck::7a3f; re-implementing the keystream in Python gave a plaintext whose SHA-256 matched the embedded digest. Confirmed by running the binary in Docker:

$ ./cold-beacon 'C0ldB34c0n::unl0ck::7a3f'
hiCTF{c0ld_b34c0n_cfg_uns34l3d_fr0m_th3_s4mpl3}

Submitted to rCTF: goodFlag (“The flag is correct.”).

The flagRevealhiCTF{c0ld_b34c0n_cfg_uns34l3d_fr0m_th3_s4mpl3}

This CTF was possibly solved, and the writeup most likely written by an LLM. Apologies for inconsistencies.