ReversingHack The Boo·

Hack The Boo CTF 2025 | Reversing

All Reversing challenges from HackTheBoo 2025

med HackTheBox HackTheBoo 2025 Reversing ELF
7 min reading · edit 2025-10-27

Hack The Boo CTF 2025 was a 2-day CTF hosted by HackTheBox. I competed solo and finished in the top 5 out of 2,893 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.

1ls -l
2file athanor
3cat lead.txt
4xxd 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):

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

Step 2: Baseline Runtime Behavior

Run the binary to observe side effects.

1./athanor
2ls -l
3cat gold.txt

Output snippet:

1Initializing the Athanor...
2The 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.

1strings -a athanor | head -n 50
2objdump -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.

1objdump -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:

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

Overview Diagram

High-level flow of the transformation:

 1+---------------------+           +-----------------------------+
 2| lead.txt            |           | athanor (ELF, stripped)     |
 3|  MTRLLEAD | seed    |  ----->   |  read header + seed (BE)    |
 4|  payload            |           |  stage1: consume 29 bytes   |
 5|                     |           |    - derive 29B key         |
 6+---------------------+           |    - signed sum S of bytes  |
 7                                  |  stage2: for remaining tail |
 8                                  |    state = (0x214F*state+S) |
 9                                  |            mod 0x26688D     |
10                                  |    out[i] = in[i] ^ (state  |
11                                  |                 & 0xF)      |
12                                  +-------------+---------------+
13                                                |
14                                                v
15                                              (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).

 1from pathlib import Path
 2data = Path('lead.txt').read_bytes()
 3payload = bytearray(data[12:])
 4base = 0x40
 5key_len = 29
 6signed_sum = 0
 7idx = 0
 8for i in range(key_len):
 9    b = payload[idx]; idx += 1
10    signed_sum += b if b < 0x80 else b - 0x100
11    # complex per-byte transform (matches key in .rodata)
12    t = base ^ ((base + i + b) & 0xff)
13    h = ((t*3) >> 8) & 0xff
14    t2 = (((t - h) & 0xff) >> 1) & 0xff
15    t2 = (t2 + h) & 0xff
16    t2 = (t2 >> 6) & 0xff
17    t3 = ((t2 << 7) - t2) & 0xff
18    out = (t - t3 + 1) & 0xff
19print('sum =', signed_sum)
20print('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.

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

Output:

1HTB{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.

1file 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.

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

Output snippet:

1A forgotten machine still ticks beneath the stones.
2Its gears grind against centuries of rust.
3
4[ a stranger approaches, and the machine asks for their name ]
5> [ 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.

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

Interesting .rodata excerpt:

 1Contents of section .rodata:
 2 2000 01000200 4f6e2061 20727573 74656420  ....On a rusted 
 3 2010 706c6174 652c2066 61696e74 206c6574  plate, faint let
 4 2020 74657273 20726576 65616c20 7468656d  ters reveal them
 5 2030 73656c76 65733a20 25730a00 4120666f  selves: %s..A fo
 6 ...
 7 20e0 00726561 6400436f 7277696e 2056656c  .read.Corwin Vel
 8 20f0 6c005b20 74686520 67656172 73206265  l.[ the gears be
 9 2100 67696e20 746f2074 75726e2e 2e2e2073  gin to turn... s
10 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.

1gdb -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.

Step 5: Analyze the Decoding Routine

Disassemble the function that prints the final message.

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

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

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

Dump the enc array from memory:

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

Resulting QWORDs:

 10x000000000000fffe
 20x000000000000ff8e
 30x000000000000ffd6
 40x000000000000ff32
 50x000000000000ff12
 60x000000000000ff72
 70x000000000000fe1a
 80x000000000000ff1e
 90x000000000000ff9e
100x000000000000fe1a
110x000000000000ff66
120x000000000000ffc2
130x000000000000fe6a
140x000000000000ffd2
150x000000000000fe0e
160x000000000000ff6e
170x000000000000ff6e
180x000000000000fe4e
190x000000000000fe5a
200x000000000000fe5a
210x000000000000fe1a
220x000000000000fe5a
230x000000000000ff2a
240x0000000000000000

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:

 1ENC_VALUES = [
 2    0x000000000000fffe,
 3    0x000000000000ff8e,
 4    0x000000000000ffd6,
 5    0x000000000000ff32,
 6    0x000000000000ff12,
 7    0x000000000000ff72,
 8    0x000000000000fe1a,
 9    0x000000000000ff1e,
10    0x000000000000ff9e,
11    0x000000000000fe1a,
12    0x000000000000ff66,
13    0x000000000000ffc2,
14    0x000000000000fe6a,
15    0x000000000000ffd2,
16    0x000000000000fe0e,
17    0x000000000000ff6e,
18    0x000000000000ff6e,
19    0x000000000000fe4e,
20    0x000000000000fe5a,
21    0x000000000000fe5a,
22    0x000000000000fe1a,
23    0x000000000000fe5a,
24    0x000000000000ff2a,
25    0x0000000000000000,
26]
27
28MASK64 = 0xFFFFFFFFFFFFFFFF
29
30def decode(values):
31    out = []
32    for v in values:
33        v ^= 0x524E
34        v = ((v >> 1) | ((v & 1) << 63)) & MASK64  # ror 1
35        v ^= 0x5648
36        v = ((v << 7) & MASK64) | (v >> (64 - 7))  # rol 7
37        v >>= 8
38        out.append(v & 0xFF)
39    return bytes(out[:-1])
40
41if __name__ == '__main__':
42    print(decode(ENC_VALUES).decode('ascii'))
43PY
1python3 exploit.py

Output:

1HTB{sk1pP1nG-C4ll$!!1!}