Contents

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.

Neurogrid CTF: Human-Only 2025

Team Solves Progress


Description: The ancient Codex guards its secrets using a peculiar validation scheme. Can you decipher its enchantments?

Points: 975 | Difficulty: Hard

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.

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: HTB{0bfUsC@t10n_w1tH_3rR0r5}


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

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

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: HTB{Tim1ng_z@_h0ll0w_t3ll5}


Description: An ancient machine sealed deep within a vault beneath Kageno begins to hum, waiting for a master…

Points: 900 | Difficulty: Easy

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:

  1. Reads 88 bytes of encrypted data from 0x4080
  2. Decrypts each 16-bit word through a 6-step algorithm
  3. Prints 44 characters with 100ms delays
  4. 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.

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: HTB{f0rg0tt3n_s1gn4ls_r3v34l_h1dd3n_s3cr3ts}


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

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 regardless
  • forging_cycle_realign() computes XOR operations but the result is never used

The actual validation happens in deprecated_core_shift():

  1. Decrypt flag from resonance_core data at 0x2150
  2. Simple XOR with constant key 0x30
  3. 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

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: HTB{r3wr1tt3n_r3s0nanc3}

Related Content