HTB: Rega's Town Challenge
Rega’s Town - HackTheBox Challenge Writeup
Challenge Information
| Field | Value |
|---|---|
| Name | Rega’s Town |
| Category | Reversing (labelled Misc/Forensics) |
| Difficulty | Medium |
| Author | d3vn0mi |
Description
Welcome to Rega Town, a quaint little place where everyone communicates through the magic of patterns and rules!
The flavor text is the first hint: “Rega Town” is an anagram of “Regex Town”, and the challenge binary turns out to be built almost entirely around Rust’s regex crate.
Solution
The distributed artifact was a password-protected zip containing a 19MB, non-stripped Rust ELF binary. Once extracted, the binary exposed exactly four challenge-relevant symbols — main, filter_input, multiply_characters, and check_input — sitting on top of the usual Rust/regex/aho-corasick/hashbrown noise. Solving it was a two-stage static analysis: first recover the regex constraints that shape the flag, then recover the exact character values from a hidden multiplication check.
Key Steps
1. Recover the real artifact
The staged output directory initially looked empty (a 0-byte file), but the original zip was still present in /out/. It was password protected:
# List the zip contents to confirm it's not corrupted, just lockedunzip -l a12c7398-130d-4364-bb4d-05844fe2f744.zip
# Common HTB convention: password is "hackthebox"7z x -phackthebox -y a12c7398-130d-4364-bb4d-05844fe2f744.zipThis unpacked a 19MB ELF (rega_town).
2. Triage the binary
chmod +x rega_town
# Confirm it's a Rust binary and spot the regex cratestrings -n 8 rega_town | grep -iE "rustc|regex"
# Filter out std/core/regex internal symbols to find the actual challenge logicnm -C rega_town 2>/dev/null | grep -viE "core::|alloc::|std::|regex|aho_corasick|memchr|hashbrown|serde|_ZN|gimli|addr2l"This surfaced the four interesting functions: main, filter_input, multiply_characters, check_input. A quick sanity run confirmed it reads a candidate flag from stdin and prints a verdict:
echo "test" | timeout 20 ./rega_town3. Stage 1 — extract the regex constraints from filter_input
Disassembling around filter_input showed it references a static [&str; 9] array at a fixed address in .rodata. Rather than reverse the string matching logic by hand, the array of (ptr, len) pairs was read directly out of the ELF’s PT_LOAD segments:
import structfrom elftools.elf.elffile import ELFFile
f = open('rega_town', 'rb')e = ELFFile(f)segs = [(s['p_vaddr'], s['p_offset'], s['p_filesz']) for s in e.iter_segments() if s['p_type'] == 'PT_LOAD']
def read_at(vaddr, size): for v, off, filesz in segs: if v <= vaddr < v + filesz: f.seek(off + (vaddr - v)) return f.read(size)
# Array of 9 (ptr: u64, len: u64) pairs at the recovered addressbase = 0x3d52a0for i in range(9): entry = read_at(base + i * 16, 16) ptr, ln = struct.unpack('<QQ', entry) print(read_at(ptr, ln).decode())This dumped nine regular expressions that together pin down a 33-character HTB{REDACTED} skeleton — an uppercase preamble, digit classes (\d), fixed literals (n), and tight per-position character classes such as [X-Z], [a-h], [n-x]{2}. This narrowed the shape of the flag but left many positions ambiguous — the regexes alone weren’t enough to fully determine it.
4. Stage 2 — recover exact characters from check_input
check_input splits the candidate input into 7 substrings on word boundaries, feeds each through multiply_characters (effectively chars().map(|c| c as u128).product()), and compares the result against 7 hardcoded u128 constants. Critically, these constants are not in .rodata — they’re built at runtime on the stack via a sequence of immediate-mode stores:
mov QWORD PTR [rsp+0x1NN], imm32 ; low 32 bits of the u128 targetmov QWORD PTR [rsp+0x1NN+8], imm32 ; high 32 bitsobjdump -d --start-address=0x57bd0 --stop-address=0x58230 -M intel rega_town \ | grep -E "lea r|call|cmp|mov +e|jmp|j[a-z]+"
objdump -d --start-address=0x57e28 --stop-address=0x58020 -M intel rega_town \ | grep -E "movabs|mov +QWORD PTR \[rsp\+0x1"Reading those stack stores in order reconstructed all 7 target constants. Since each slice’s target is the product of its characters’ ASCII codes, and the regex classes from Stage 1 already narrowed each position to a handful of candidates, a brute-force search recovered the exact substrings:
import itertools, string
# Character-class candidates per position, taken from the Stage-1 regexestargets = [0x7a070, 0x5c436, 0x6cc60, 0x27b5776, 0x10f9, 0xd76a0, 0x7465a58]P = string.printable
def solve_slice(target, length, allowed_per_pos): for combo in itertools.product(*allowed_per_pos): prod = 1 for c in combo: prod *= ord(c) if prod == target: return ''.join(combo) return NoneRunning this against each of the 7 slices, constrained by the regex character classes, recovered the literal fragments — e.g. Y0u, Ar3, Th3, K1ng, O7, The — which slotted directly into the flag skeleton from Stage 1.
5. Assemble and verify
Combining the fixed literals from the regexes with the recovered multiplication-check fragments produced the full flag. Feeding it back into the binary confirmed success:
echo 'HTB{REDACTED}' | ./rega_town# => Correct one of us!!Tools Used
7z— extracting the password-protected distribution zip (hackthebox)strings/nm— triaging a non-stripped Rust binary and filtering framework noiseobjdump(Intel syntax) — disassemblingfilter_inputandcheck_inputaround specific address rangespyelftools(ELFFile) — resolving PT_LOAD segment offsets to read static data (the regex string-slice array) directly from the ELF- Python — reconstructing the
u128comparison constants from raw stack-store immediates, and brute-forcing character-class combinations against the multiplication-product targets
Key Learnings
- The title is the hint. “Rega Town” → “Regex Town” pointed straight at the
regexcrate dependency before any disassembly happened — worth pattern-matching challenge names against anagrams/wordplay early. - Static data doesn’t need manual disassembly. Once a fixed-size
&strarray’s address is known, reading(ptr, len)pairs straight out of the ELF’s PT_LOAD segments withpyelftoolsis far faster and less error-prone than tracing string construction in assembly. - Not all constants live in
.rodata. Rust/LLVM will happily materialize large constants (likeu128literals) via a sequence of immediatemovstores directly into stack slots rather than a data section reference — these have to be recovered by reading the store immediates, not by looking for cross-references into.rodata. - Combine coarse and fine constraints. The regexes alone left too much ambiguity to guess the flag; the multiplication check alone would have required a huge brute force. Intersecting the regex character classes with the multiplication-product targets shrank the search space enough to solve deterministically.
- Mislabeled categories happen. The challenge was tagged as forensics-adjacent (password-protected zip) but the actual puzzle was pure reverse engineering — don’t let the wrapper format anchor your approach to the core binary.