Neurogrid CTF: Human-Only 2025 | Reversing
Neurogrid CTF: Human-Only 2025 was a 4-day CTF hosted by HackTheBox. I competed solo as “Legendary Queue” and finished in the top 4 out of 1337 players. This post covers the Reversing challenges.


Codex of Failures [hard]
Description: The ancient Codex guards its secrets using a peculiar validation scheme. Can you decipher its enchantments?
Points: 975 | Difficulty: Hard
Analysis
A 311KB stripped ELF binary with an interesting validation mechanism. Running it shows ASCII art of a treasure chest and warrior, then prompts for input. Wrong input produces “YOUR ENCHANTMENT ATTEMPT IS RISIBLE…”
Disassembling revealed a validation loop at 0x42cd that iterates through 28 characters, calling functions from a pointer table at 0x4cc80. The key insight came from analyzing what these functions actually do - each one intentionally performs a failing syscall and captures the resulting errno value.
The validation formula is: input[i] == errno + 0x2f
The function pointer table maps to these errno-producing operations:
| Function | Operation | errno | Value |
|---|---|---|---|
| 0x34aa | open("/tmp/free_hackthebox_flags!!!") |
ENOENT | 2 |
| 0x3ccc | waitpid(-1, ...) on orphan |
ECHILD | 10 |
| 0x3886 | open() on socket path |
ENXIO | 6 |
| 0x3493 | setuid(0) unprivileged |
EPERM | 1 |
| 0x34d0 | kill(0xdeadbeef, bad_sig) |
ESRCH | 3 |
| 0x34ec | pause() interrupted by SIGALRM |
EINTR | 4 |
| 0x3a9d | execve() with huge argv |
E2BIG | 7 |
| 0x36e8 | read /proc/self/mem invalid offset |
EIO | 5 |
| 0x3bea | execve() on non-executable |
ENOEXEC | 8 |
| 0x3c82 | read(fd=-1, ...) |
EBADF | 9 |
The critical insight is that successful syscalls do NOT clear errno - they preserve it from the previous failing call. This lets the binary chain operations and extract a single errno value at the end.
Exploitation
Once the mapping is understood, generating the solution is straightforward:
funcs = [0x34aa, 0x3ccc, 0x3886, 0x3493, 0x34aa, 0x34d0, 0x34ec, 0x3a9d,
0x3886, 0x36e8, 0x3bea, 0x3c82, 0x34aa, 0x34ec, 0x3a9d, 0x3886,
0x3ccc, 0x3c82, 0x3bea, 0x36e8, 0x3886, 0x3a9d, 0x34ec, 0x34d0,
0x34aa, 0x3493, 0x34ec, 0x34d0]
errno_map = {
0x34aa: 2, 0x3ccc: 10, 0x3886: 6, 0x3493: 1, 0x34d0: 3,
0x34ec: 4, 0x3a9d: 7, 0x36e8: 5, 0x3bea: 8, 0x3c82: 9
}
solution = ''.join(chr(errno_map[f] + 0x2f) for f in funcs)
print(solution)
Each character encodes an errno value offset by 0x2f. The 28-character input decodes to the flag.
Flag
Flag:
HTB{0bfUsC@t10n_w1tH_3rR0r5}
SilentOracle [medium]
Description: Beneath the temple sits a mute sage: the Silent Oracle. It answers only in sighs and the faint click of the world around it. Most folk hear nothing - but those who learn to listen can read the pattern in the silence. Learn to listen to the silence, and the Oracle will whisper a secret of surviving in these cursed lands. Beware though, trying to lie to it will result in temporal banishment.
Points: 975 | Difficulty: Medium
Analysis
A 371KB stripped ELF binary that validates input character-by-character. Running locally shows the binary sleeps 5 seconds on wrong characters, suggesting a timing oracle. But testing against the remote server revealed no timing differences - all responses came back in ~0.16s regardless of input correctness.
The actual vulnerability is a response content oracle. When analyzing response sizes:
- Correct character: ~90,089 bytes (no warning message)
- Wrong character: ~90,197 bytes (includes “BRUTE-FORCE” warning)
The 108-byte difference is consistent and reliable. The binary modifies its response content based on whether the current character matches, leaking information about correctness.
Key strings in the binary confirmed this:
- Success:
CONTINUE ON WITH YOUR ADVENTURE, O HONORABLE ONE - Wrong:
UH OH! THE ORACLE DETECTS A BRUTE-FORCE ATTEMPT, TRIGGERING DEFENSIVE SPELLS
Exploitation
Character-by-character brute force using the response content oracle:
import socket
def test_char(prefix, char):
s = socket.socket()
s.connect((HOST, PORT))
s.recv(4096) # banner
s.send((prefix + char + '\n').encode())
resp = s.recv(200000).decode()
s.close()
return 'BRUTE' not in resp
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_@!#}'
flag = 'HTB{'
while not flag.endswith('}'):
for c in charset:
if test_char(flag, c):
flag += c
print(f'Found: {flag}')
break
print(f'Flag: {flag}')
The absence of “BRUTE” in the response indicates the character is correct. Starting from the known prefix HTB{, each position is brute-forced until the closing brace is found.
The flag name itself is a hint - “Tim1ng_z@_h0ll0w_t3ll5” references timing/oracle attacks, though the actual oracle is content-based rather than timing-based on the remote server.
Flag
Flag:
HTB{Tim1ng_z@_h0ll0w_t3ll5}
ForgottenVault [easy]
Description: An ancient machine sealed deep within a vault beneath Kageno begins to hum, waiting for a master…
Points: 900 | Difficulty: Easy
Analysis
A 16KB unstripped ELF binary. The main() function prompts for a PIN, calls check_pin() with complex arithmetic, then prints “Invalid code” regardless of input. The check_pin() function is a red herring - its result is calculated but never validated.
The real secret is in the signal handler registered by setup(). When SIGABRT triggers, the handler() function at 0x1290 executes:
- Reads 88 bytes of encrypted data from
0x4080 - Decrypts each 16-bit word through a 6-step algorithm
- Prints 44 characters with 100ms delays
- Exits cleanly
The decryption algorithm for each 16-bit word:
XOR 0x4d4c → ROL 2 → XOR 0x4944 → ROR 5 → SUB 0x41 → AND 0xFF
The XOR keys spell “ML” and “ID” in ASCII - likely intentional.
Exploitation
Extract the encrypted data and reverse the algorithm:
encrypted = [
0x3d5a, 0x2d59, 0x5d59, 0xdd59, 0x2d58, 0x855b, 0x255c, 0xd55a,
0x8559, 0x6559, 0x0d58, 0xd559, 0x5559, 0x9d5b, 0x7d5d, 0x5d5c,
0xfd5b, 0xad5b, 0xf55a, 0x6d58, 0x3d5a, 0xdd5b, 0xb55a, 0x0d5b,
0x1d5a, 0x4559, 0x255a, 0xfd5c, 0x0d5a, 0x8d59, 0x9d5a, 0xe55c,
0x355a, 0xad5b, 0x955d, 0x155a, 0x3d5a, 0x655b, 0xad59, 0x5d5a,
0xe55b, 0x755a, 0xfd5a, 0xed5a
]
def decrypt(word):
val = word
val = (val + 0x41) & 0xFFFF # Undo SUB
val = ((val << 5) | (val >> 11)) & 0xFFFF # Undo ROR (apply ROL)
val ^= 0x4944 # Undo XOR
val = ((val >> 2) | (val << 14)) & 0xFFFF # Undo ROL (apply ROR)
val ^= 0x4d4c # Undo XOR
return chr(val & 0xFF)
flag = ''.join(decrypt(w) for w in encrypted[:44])
print(flag)
Alternatively, finding what PIN value triggers SIGABRT and running the binary dynamically would also reveal the flag through the signal handler’s output.
Flag
Flag:
HTB{f0rg0tt3n_s1gn4ls_r3v34l_h1dd3n_s3cr3ts}
IronheartEcho [very easy]
Description: Beneath the Kanayama mountain shrine lies a half-buried dwarven smithy, forgotten by even the oldest shrinekeepers. Resonance stones - crystals once used to synchronize forging mechanisms - pulse softly as Gorō enters. Among rows of clockwork dolls frozen mid-movement stands a broken sentinel, its faceplate gone, chest cavity forced open.
Points: 875 | Difficulty: Very Easy
Analysis
A 16KB unstripped ELF binary - the preserved symbols make this significantly easier to analyze. Key functions identified: stone_shift(), verify_pattern(), forging_cycle_realign(), and deprecated_core_shift().
The binary accepts input and validates it against an encrypted flag. Most functions are red herrings:
verify_pattern()calculates sum of (i*3) for i in [0..4], checks if 30 == 42, but always returns 1 regardlessforging_cycle_realign()computes XOR operations but the result is never used
The actual validation happens in deprecated_core_shift():
- Decrypt flag from
resonance_coredata at0x2150 - Simple XOR with constant key
0x30 - Compare decrypted string with user input via
strcmp()
Encrypted data (24 bytes):
78 64 72 4b 42 03 47 42 01 44 44 03 5e 6f 42 03 43 00 5e 51 5e 53 03 4d
Exploitation
XOR decryption with key 0x30:
encrypted = bytes([
0x78, 0x64, 0x72, 0x4b, 0x42, 0x03, 0x47, 0x42,
0x01, 0x44, 0x44, 0x03, 0x5e, 0x6f, 0x42, 0x03,
0x43, 0x00, 0x5e, 0x51, 0x5e, 0x53, 0x03, 0x4d
])
flag = ''.join(chr(b ^ 0x30) for b in encrypted)
print(flag)
Running the binary with the decrypted flag confirms: “Pattern accepted.”
This is clearly an introductory reversing challenge - the preserved symbols, simple XOR cipher, and red herring functions are designed to teach fundamental binary analysis skills.
Flag
Flag:
HTB{r3wr1tt3n_r3s0nanc3}