Challenge: Rusted Oracle
Goal: Recover the flag from the supplied ELF binary.
Format: Step-by-step, command-driven walkthrough (suitable for beginners-to-intermediate reverse engineers).

Summary

Recover a flag hidden in the binary's .data section by reversing the small decoding routine found in the disassembly and reproducing it in Python.

Setup

Commands assume a Linux environment with typical reverse-engineering tools installed:

sudo apt update
sudo apt install -y binutils build-essential python3 python3-pip ghidra radare2 gdb
# radare2 and ghidra optional — objdump/readelf/strings are sufficient

Place the provided challenge binary in a working directory:

mkdir -p ~/htb/rusted_oracle && cd ~/htb/rusted_oracle
# copy/upload the file into this directory (here assumed named `rusted_oracle`)
ls -l

1) Basic reconnaissance

Identify file type and architecture:

file rusted_oracle
# example output: rusted_oracle: ELF 64-bit LSB executable, x86-64, ...

Get a quick impression with strings:

strings rusted_oracle | head -n 50

List sections (useful to locate .data or other relevant areas):

readelf -S rusted_oracle

2) Dump the .data section (where encoded data often lives)

Use readelf or objdump to hexdump the .data section. Either works; here are both examples:

With readelf:

readelf -x .data rusted_oracle | sed -n '1,120p'   # print first part

With objdump:

objdump -s -j .data rusted_oracle | sed -n '1,120p'

You will see a block of hex data — in this challenge the encoded values are laid out as 64-bit words (qwords).

Save the hex dump to a file for easier parsing:

objdump -s -j .data rusted_oracle > data_section_dump.txt

3) Inspect the decoding routine in the binary

Disassemble the binary to find the function that decodes the data. Two simple ways:

Option A — objdump:

objdump -d rusted_oracle | less
# search for likely function names or strings printed by the program when run

Option B — r2 (radare2) for quick cross-references:

r2 -A rusted_oracle
# inside r2: afl        -> list functions
# then: pdf @ sym.<function_name>  -> print disassembly of function

What to look for:

  • A loop that reads qwords from .data.

  • Bitwise operations (XOR, shifts, rotates).

  • A final mov or putchar/write that uses the low-order byte of the manipulated qword.

In this challenge, the decoding routine (reconstructed from the disassembly) applies these operations in order to each 64-bit word:

  1. XOR with 0x524e

  2. ROR (rotate right) by 1

  3. XOR with 0x5648

  4. ROL (rotate left) by 7

  5. SHR (logical right shift) by 8 Then the least-significant byte is taken as the plaintext output byte.

4) Extract qwords cleanly

We need the list of 64-bit values (enc array). The objdump hex dump shows bytes; convert those bytes into little-endian 64-bit integers. Use a short Python helper to parse the raw bytes extracted from the .data section.

Save the following Python script as decode.py in the same directory.

#!/usr/bin/env python3
# decode.py -- parse a hexdump of .data (or raw bytes) and apply the decoding
import sys,struct

# Replace this with the raw qwords extracted from the .data section
# You can paste the qwords here as integers or provide a file containing raw bytes.
# Below: example list replaced by actual values in your binary.
qwords = [
    0x0123456789abcdef, # <-- placeholder; replace with actual qwords
    # ...
]

def ror64(x, r):
    r %= 64
    return ((x >> r) | ((x << (64 - r)) & ((1<<64)-1))) & ((1<<64)-1)

def rol64(x, r):
    r %= 64
    return (((x << r) & ((1<<64)-1)) | (x >> (64 - r))) & ((1<<64)-1)

def decode_qword(q):
    # 1) XOR with 0x524e
    q ^= 0x524e
    # 2) ROR 1
    q = ror64(q, 1)
    # 3) XOR with 0x5648
    q ^= 0x5648
    # 4) ROL 7
    q = rol64(q, 7)
    # 5) SHR 8 (logical right shift)
    q = q >> 8
    # take lowest byte as output
    return q & 0xff

def decode_all(qwords):
    out = bytes(decode_qword(q) for q in qwords)
    return out

if __name__ == '__main__':
    # If you prefer to load qwords from a binary file:
    # with open('data_section.bin','rb') as f:
    #     raw = f.read()
    #     qwords = list(struct.unpack('<' + 'Q'*(len(raw)//8), raw))
    if len(qwords) == 0:
        print("No qwords defined. Paste qwords into the script or load from a file.")
        sys.exit(1)
    flag = decode_all(qwords)
    print(flag.decode('utf-8',errors='replace'))

Note: Replace the placeholder qwords array in the script with the actual list of qwords extracted from the .data section. Two easy ways to get them:

  1. Using xxd on an extracted raw .data file:

    # extract raw bytes of .data using objcopy
    objcopy -O binary --only-section=.data rusted_oracle data_section.bin
    python3 - <<'PY'
    import struct
    raw = open('data_section.bin','rb').read()
    qcount = len(raw)//8
    qwords = struct.unpack('<' + 'Q'*qcount, raw[:qcount*8])
    print(',\n'.join(hex(q) for q in qwords))
    PY
    
  2. Or, parse the objdump -s -j .data output with a small parser.

5) Run the decoder

After filling qwords in decode.py (or loading data_section.bin), run:

chmod +x decode.py
./decode.py

The output should be the flag, for example:

HTB{sk1pP1nG-<REDACTED>}

6) Cross-check with dynamic runs (optional)

You can run the original binary to see any printed output; sometimes the binary expects an argument or interaction:

./rusted_oracle
# or run under strace to see behavior:
strace -f ./rusted_oracle 2>&1 | less

If the binary performs the decode at runtime, you can also run it under gdb and put a breakpoint in the decoding function to inspect live values:

gdb ./rusted_oracle
(gdb) break *0x401234   # example address from objdump; replace with actual
(gdb) run
(gdb) x/20gx $rdi       # inspect memory or registers depending on calling convention

7) One-liner quick decode (if you prefer)

If you have data_section.bin, you can extract qwords and decode in one Python command:

python3 - <<'PY'
import struct
def ror64(x,r): return ((x>>r) | ((x<<(64-r)) & ((1<<64)-1))) & ((1<<64)-1)
def rol64(x,r): return (((x<<r) & ((1<<64)-1)) | (x>>(64-r))) & ((1<<64)-1)
raw=open('data_section.bin','rb').read()
qs=struct.unpack('<'+'Q'*(len(raw)//8),raw)
out = bytes((( ( ( ( (q ^ 0x524e) >> 1 | ((q ^ 0x524e) << 63) & ((1<<64)-1)) ^ 0x5648) << 7 | ((...)) ) >> 8 ) & 0xff ) for q in qs)
# (The one-liner becomes messy; use decode.py above.)
print(out)
PY

Tip: avoid extremely compact one-liners for bit-rotations — prefer readability to avoid mistakes.

8) Final flag

When the qwords are decoded with the exact sequence discovered in the disassembly, the flag revealed is:

HTB{sk1pP1nG-<REDACTED>}

Appendix — Helpful Command Cheatsheet

  • file <binary> — determine type/arch

  • readelf -a <binary> — show ELF headers/symbols/sections

  • objdump -d <binary> — disassemble

  • objdump -s -j .data <binary> — dump .data bytes

  • objcopy -O binary --only-section=.data <binary> data_section.bin — extract raw data bytes

  • xxd -e -g8 data_section.bin — view qwords little-endian grouped by 8 bytes

  • r2 -A <binary> — radare2 auto-analysis, then afl, pdf @ sym.*

  • strings <binary> — scan for ASCII strings

  • gdb <binary> — dynamic debugging

If you want a ready-to-run decoded script

If you prefer, create data_section.bin as described then modify decode.py to load it dynamically (see commented code in the script).

Closing

This writeup walked you from basic reconnaissance through static analysis to a reproducible Python-based decoding. Use the provided script and commands to reproduce results on a similar challenge.

Good luck on HTB — may your reverse-engineering be relentless.

Keep Reading