Neven Marić
Hidden Investigations CTF 2026

ReversingHidden Investigations CTF 2026

Night Batch

night-batch is a GnuCOBOL 3.2 program (cobc output, libcob and GMP statically linked, stripped). It needs glibc 2.38+, so I ran it in ubuntu:24.04.

  • reversing
  • cobol
  • gnucobol
  • constraint-solving
  • modular-arithmetic
  • gdb-scripting
  • binary-patching

What this one teaches

  • GnuCOBOL-compiled binaries use cob_intr_binop/mod/cmp_llint for arithmetic checks — tracing these library calls under gdb extracts the constraint system without reading COBOL.
  • A system of modular equations with a few nonlinear terms (digit products) is best solved by DFS with early pruning, not SAT or Z3.
  • NOP-patching conditional jumps to the reject path lets you trace the full check sequence in one run, even when the input is wrong.

Task description

* ----------------------------------------------------------------- *
* OPS NOTE -- taped to the 3270 before the floor was cleared         *
* ----------------------------------------------------------------- *
* NIGHTBATCH acct-validation step. the load module is all we kept;   *
* the COPYLIB and source rode out with the frame. it still runs:     *
*                                                                     *
*     ./night-batch <account-key>                                    *
*                                                                     *
* a good key clears the step. everything else bounces. the key was   *
* on a card by the console -- card's gone. recover it from the module.*
* ----------------------------------------------------------------- *

Linux x86-64 ELF.

Key: 481702596318740925

How it works

  • main (0x400940) calls cob_init, then the COBOL program at 0x400a70.
  • The key goes into a 40-byte field. It must be 18 characters long and IS NUMERIC (cob_check_numeric, 0x404bc0). Each digit is moved into its own packed-decimal field D1..D18 at 0x460100.
  • Then 23 checks run in sequence. Each one is built from cob_intr_binop (0x41df30), then cob_intr_mod (0x41e530), then cob_cmp_llint (0x413920). The first failed check jumps to 0x400b77 and prints “account rejected”.
  • Success path (0x402f40): H0 = SHA256(key). Keystream blocks are K_i = SHA256(H0 || be32(i)). The 42-byte ciphertext at 0x44d0c0 is XORed with the keystream. The result is accepted only if its SHA-256 equals an embedded constant, and is then printed. Otherwise the program prints “account rejected”, whose text is XOR 0x5a obfuscated at 0x44d0a0.

Recovering the key

  1. I NOP’d every jne 400b77 in a copy of the binary. I then traced binop, mod and cmp_llint under gdb with a Python script. This logs every equation: weights, digit indices, modulus and target.
  2. The 23 equations are all of the form sum(w * Dx) mod M == c. Two of them use a digit product (D2*D11 = 8 and D2*D10 = 4, both mod 10). The last one is mod 100: 9*D17 + 2*D16 + 5*D11 + 5*D7 + 4*D4 ≡ 94 (mod 100). The other 22 are mod 10. Examples: D4+D5+D9 ≡ 3, D1+D5+D9 ≡ 0, D1+D12+D18 ≡ 7.
  3. A DFS over the digits (ordered so equations are checked as early as possible) finds one solution: 481702596318740925.
  4. Check: ./night-batch 481702596318740925 prints hiCTF{c0b0l_p4ck3d_d3c1mal_n1ghtly_b4tch!}.

Submission

The API returned {"kind":"goodFlag","message":"The flag is correct."}.

The flagRevealhiCTF{c0b0l_p4ck3d_d3c1mal_n1ghtly_b4tch!}

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