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
movorputchar/writethat 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:
XOR with
0x524eROR (rotate right) by
1XOR with
0x5648ROL (rotate left) by
7SHR (logical right shift) by
8Then 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:
Using
xxdon an extracted raw.datafile:# 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)) PYOr, parse the
objdump -s -j .dataoutput 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/archreadelf -a <binary>— show ELF headers/symbols/sectionsobjdump -d <binary>— disassembleobjdump -s -j .data <binary>— dump .data bytesobjcopy -O binary --only-section=.data <binary> data_section.bin— extract raw data bytesxxd -e -g8 data_section.bin— view qwords little-endian grouped by 8 bytesr2 -A <binary>— radare2 auto-analysis, thenafl,pdf @ sym.*strings <binary>— scan for ASCII stringsgdb <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.

