HTB: Rega's Town Challenge

Rega’s Town - HackTheBox Challenge Writeup

Challenge Information

FieldValue
NameRega’s Town
CategoryReversing (labelled Misc/Forensics)
DifficultyMedium
Authord3vn0mi

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:

Terminal window
# List the zip contents to confirm it's not corrupted, just locked
unzip -l a12c7398-130d-4364-bb4d-05844fe2f744.zip
# Common HTB convention: password is "hackthebox"
7z x -phackthebox -y a12c7398-130d-4364-bb4d-05844fe2f744.zip

This unpacked a 19MB ELF (rega_town).

2. Triage the binary

Terminal window
chmod +x rega_town
# Confirm it's a Rust binary and spot the regex crate
strings -n 8 rega_town | grep -iE "rustc|regex"
# Filter out std/core/regex internal symbols to find the actual challenge logic
nm -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:

Terminal window
echo "test" | timeout 20 ./rega_town

3. 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 struct
from 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 address
base = 0x3d52a0
for 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 target
mov QWORD PTR [rsp+0x1NN+8], imm32 ; high 32 bits
Terminal window
objdump -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 regexes
targets = [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 None

Running 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:

Terminal window
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 noise
  • objdump (Intel syntax) — disassembling filter_input and check_input around specific address ranges
  • pyelftools (ELFFile) — resolving PT_LOAD segment offsets to read static data (the regex string-slice array) directly from the ELF
  • Python — reconstructing the u128 comparison 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 regex crate 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 &str array’s address is known, reading (ptr, len) pairs straight out of the ELF’s PT_LOAD segments with pyelftools is 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 (like u128 literals) via a sequence of immediate mov stores 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.