Contents

Hack The Boo CTF 2025 | Reversing

Hack The Boo CTF 2025 was a 2-day CTF hosted by HackTheBox. I competed solo as “Legendary Queue” and finished in the top 5 out of 2892 players. This post covers the Reversing challenges.

Morvidus the alchemist claims to have perfected the art of digital alchemy. Being paranoid, he secured his incantation with a complex algorithm, but left the code rushed and broken. Fix his amateur mistakes and claim the digital gold for yourself!

List the contents and identify file types.

ls -l
file athanor
cat lead.txt
xxd lead.txt

Findings:

  • athanor: 64-bit PIE ELF, stripped.
  • lead.txt: begins with magic MTRLLEAD, then 4 bytes, then a payload.

Header breakdown (from xxd):

00000000: 4d54 524c 4c45 4144 972c ffbc ...  MTRLLEAD.,..
  • Magic: MTRLLEAD
  • Seed (big-endian): 0x97 0x2c 0xff 0xbc0x972cffbc

Run the binary to observe side effects.

./athanor
ls -l
cat gold.txt

Output snippet:

Initializing the Athanor...
The Athanor glows brightly, revealing a secret...

The program writes gold.txt with 7 bytes: J^Mw_~<.

Pull strings and inspect .rodata.

strings -a athanor | head -n 50
objdump -s -j .rodata athanor

Interesting data in .rodata:

  • USMWO[]\iN[QWRYdqXle[i_bm^aoc (29 bytes)
  • Filenames: lead.txt, gold.txt
  • Messages, and the magic MTRLLEAD

Disassemble to locate the main logic.

objdump -M intel -d athanor | sed -n '300,420p'

Key observations (addresses approximate):

  • 0x1251–0x1310: reads lead.txt, checks header, loads 4-byte seed big-endian.
  • 0x13bb–0x148f: Stage 1 loop processes 29 bytes, accumulates a signed sum, and reconstructs the 29-byte key above (verification path via strcmp).
  • 0x14d4–0x15c5: Stage 2 allocates a 0x28 buffer, copies 7 bytes from the remaining payload, then for each byte computes: state = (0x214f*state + sum) mod 0x26688d; out[i] = in[i] ^ (state & 0xf) and writes 7 bytes to gold.txt.

Attempts to use tracing were sandbox-blocked, so analysis stayed static:

gdb -q ./athanor      # no symbols; ptrace blocked here
strace -s 80 ./athanor # ptrace blocked in sandbox

High-level flow of the transformation:

+---------------------+           +-----------------------------+
| lead.txt            |           | athanor (ELF, stripped)     |
|  MTRLLEAD | seed    |  ----->   |  read header + seed (BE)    |
|  payload            |           |  stage1: consume 29 bytes   |
|                     |           |    - derive 29B key         |
+---------------------+           |    - signed sum S of bytes  |
                                  |  stage2: for remaining tail |
                                  |    state = (0x214F*state+S) |
                                  |            mod 0x26688D     |
                                  |    out[i] = in[i] ^ (state  |
                                  |                 & 0xF)      |
                                  +-------------+---------------+
                                                |
                                                v
                                              (flag)

Recreate Stage 1 to confirm the embedded 29-byte key and compute the signed sum of the first 29 payload bytes (result: 2245).

from pathlib import Path
data = Path('lead.txt').read_bytes()
payload = bytearray(data[12:])
base = 0x40
key_len = 29
signed_sum = 0
idx = 0
for i in range(key_len):
    b = payload[idx]; idx += 1
    signed_sum += b if b < 0x80 else b - 0x100
    # complex per-byte transform (matches key in .rodata)
    t = base ^ ((base + i + b) & 0xff)
    h = ((t*3) >> 8) & 0xff
    t2 = (((t - h) & 0xff) >> 1) & 0xff
    t2 = (t2 + h) & 0xff
    t2 = (t2 >> 6) & 0xff
    t3 = ((t2 << 7) - t2) & 0xff
    out = (t - t3 + 1) & 0xff
print('sum =', signed_sum)
print('stage1 consumed =', idx)

Stage 2 on the next 7 bytes reproduces gold.txt but we can also apply it to the entire remaining payload to get the flag.

from pathlib import Path
data = Path('lead.txt').read_bytes()
seed = int.from_bytes(data[8:12], 'big')
payload = bytearray(data[12:])
base, key_len = 0x40, 29
signed_sum = sum((b if b < 0x80 else b-0x100) for b in payload[:key_len])
tail = payload[key_len:]
state = seed
res = bytearray()
for b in tail:
    state = (0x214f*state + (signed_sum & 0xffffffff)) & 0xffffffff
    state %= 0x26688d
    res.append(b ^ (state & 0xf))
print(res.decode('latin1'))

Output:

HTB{Sp1r1t_0f_Th3_C0d3_Aw4k3n3d}\x0c
  • strip the \x0c

An ancient machine, a relic from a forgotten civilization, could be the key to defeating the Hollow King. However, the gears have ground almost to a halt. Can you restore the decrepit mechanism?

List contents and identify the target binary.

file rusted_oracle

Findings:

  • rusted_oracle: 64-bit PIE ELF, dynamically linked, not stripped.

Run the binary to see prompts and interaction.

./rusted_oracle
printf 'test\n' | ./rusted_oracle

Output snippet:

A forgotten machine still ticks beneath the stones.
Its gears grind against centuries of rust.

[ a stranger approaches, and the machine asks for their name ]
> [ the machine falls silent ]

Observation: It asks for a name, then falls silent if the input is wrong.

Look for embedded strings and constants.

strings -a rusted_oracle | head -n 50
objdump -s -j .rodata rusted_oracle

Interesting .rodata excerpt:

Contents of section .rodata:
 2000 01000200 4f6e2061 20727573 74656420  ....On a rusted 
 2010 706c6174 652c2066 61696e74 206c6574  plate, faint let
 2020 74657273 20726576 65616c20 7468656d  ters reveal them
 2030 73656c76 65733a20 25730a00 4120666f  selves: %s..A fo
 ...
 20e0 00726561 6400436f 7277696e 2056656c  .read.Corwin Vel
 20f0 6c005b20 74686520 67656172 73206265  l.[ the gears be
 2100 67696e20 746f2074 75726e2e 2e2e2073  gin to turn... s
 2110 6c6f776c 792e2e2e 205d0a00 5b207468  lowly... ]..[ th

The name Corwin Vell appears near other UI text, suggesting a magic input.

Disassemble main to confirm the check and follow-on logic.

gdb -batch -ex 'file rusted_oracle' -ex 'disassemble main'

Key points from main:

  • Reads up to 0x3f bytes into a 0x40 buffer, trims trailing newline.
  • strcmp(input, CONST_AT_0x20E6) — if zero, prints “[ the gears begin to turn… ]” and calls machine_decoding_sequence.
  • Else prints a failure message and exits.

Given the .rodata placement, the expected name is Corwin Vell.

Disassemble the function that prints the final message.

gdb -batch -ex 'file rusted_oracle' -ex 'disassemble machine_decoding_sequence'

Pseudo-logic reconstructed from the assembly (loop over 24 QWORDs at enc = 0x4050):

for i in range(24):
    v = enc[i]
    v ^= 0x524e
    v = ror64(v, 1)
    v ^= 0x5648
    v = rol64(v, 7)
    v >>= 8
    out[i] = v & 0xff
print("On a rusted plate, faint letters reveal themselves: %s" % out)

Dump the enc array from memory:

gdb -batch -ex 'file rusted_oracle' -ex 'x/24gx 0x4050'

Resulting QWORDs:

0x000000000000fffe
0x000000000000ff8e
0x000000000000ffd6
0x000000000000ff32
0x000000000000ff12
0x000000000000ff72
0x000000000000fe1a
0x000000000000ff1e
0x000000000000ff9e
0x000000000000fe1a
0x000000000000ff66
0x000000000000ffc2
0x000000000000fe6a
0x000000000000ffd2
0x000000000000fe0e
0x000000000000ff6e
0x000000000000ff6e
0x000000000000fe4e
0x000000000000fe5a
0x000000000000fe5a
0x000000000000fe1a
0x000000000000fe5a
0x000000000000ff2a
0x0000000000000000

Implement the exact bitwise pipeline in Python and run it over the constants. A trailing padding byte is discarded.

Create the decoder:

ENC_VALUES = [
    0x000000000000fffe,
    0x000000000000ff8e,
    0x000000000000ffd6,
    0x000000000000ff32,
    0x000000000000ff12,
    0x000000000000ff72,
    0x000000000000fe1a,
    0x000000000000ff1e,
    0x000000000000ff9e,
    0x000000000000fe1a,
    0x000000000000ff66,
    0x000000000000ffc2,
    0x000000000000fe6a,
    0x000000000000ffd2,
    0x000000000000fe0e,
    0x000000000000ff6e,
    0x000000000000ff6e,
    0x000000000000fe4e,
    0x000000000000fe5a,
    0x000000000000fe5a,
    0x000000000000fe1a,
    0x000000000000fe5a,
    0x000000000000ff2a,
    0x0000000000000000,
]

MASK64 = 0xFFFFFFFFFFFFFFFF

def decode(values):
    out = []
    for v in values:
        v ^= 0x524E
        v = ((v >> 1) | ((v & 1) << 63)) & MASK64  # ror 1
        v ^= 0x5648
        v = ((v << 7) & MASK64) | (v >> (64 - 7))  # rol 7
        v >>= 8
        out.append(v & 0xFF)
    return bytes(out[:-1])

if __name__ == '__main__':
    print(decode(ENC_VALUES).decode('ascii'))
PY
python3 exploit.py

Output:

HTB{sk1pP1nG-C4ll$!!1!}

Related Content