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.
Digital Alchemy [medium]
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!
Step 1: Explore the Challenge Files
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 magicMTRLLEAD, 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 0xbc→0x972cffbc
Step 2: Baseline Runtime Behavior
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_~<.
Step 3: Static Recon of the Binary
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 togold.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
Overview Diagram
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)
Step 4: Model the Transform in Python
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
Rusted Oracle [easy]
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?
Step 1: Explore the Challenge Files
List contents and identify the target binary.
file rusted_oracle
Findings:
rusted_oracle: 64-bit PIE ELF, dynamically linked, not stripped.
Step 2: Baseline Runtime Behavior
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.
Step 3: Static Recon for Hints
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.
Step 4: Disassemble Control Flow
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 callsmachine_decoding_sequence.- Else prints a failure message and exits.
Given the .rodata placement, the expected name is Corwin Vell.
Step 5: Analyze the Decoding Routine
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
Step 6: Recreate the Transform and Recover the Flag
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!}