$ cat writeup.md…
$ cat writeup.md…
HackTheBox
ELF 64-bit LSB PIE executable, x86-64, dynamically linked, not stripped, with debug info. Rust binary `rega_town` (~19MB, typical for Rust). The program asks for a "secret passphrase" and validates input in two stages: first through 9 regex patterns, then through ASCII code product verification of s
Welcome to Rega Town, a quaint little place where everyone communicates through the magic of patterns and rules!
ELF 64-bit LSB PIE executable, x86-64, dynamically linked, not stripped, with debug info. Rust binary rega_town (~19MB, typical for Rust). The program asks for a "secret passphrase" and validates input in two stages: first through 9 regex patterns, then through ASCII code product verification of substrings.
$ file rega_town ELF 64-bit LSB PIE executable, x86-64, dynamically linked, with debug_info, not stripped $ strings rega_town | grep -E "Welcome|passphrase|secret" Welcome to our secret town! Enter secret passphrase:
The binary is not stripped and contains debug info — Rust symbols are fully available for analysis.
rega_town::main @ 0x58230 — entry point, prints greeting, reads input
rega_town::filter_input @ 0x578f0 — validation through 9 regex patterns
rega_town::check_input @ 0x57c00 — extracts substrings, verifies ASCII code products
rega_town::multiply_characters @ 0x57bd0 — computes product of ASCII values (u128)
Running strings on the binary revealed a block of 9 POSIX-style regex patterns located between called Result::unwrap() on an Err value and src/main.rs:
| # | Regex | Constraint |
|---|---|---|
| 1 | ^.{33}$ | String length = 33 characters |
| 2 | (?:^[\x48][\x54][\x42]).* | Starts with "HTB" (0x48=H, 0x54=T, 0x42=B) |
| 3 | ^.{3}(\x7b).*(\x7d)$ | Position 3 = { (0x7b), ends with } (0x7d) |
| 4 | ^[[:upper:]]{3}.[[:upper:]].{3}[[:upper:]].{3}[[:upper:]].{3}[[:upper:]].{4}[[:upper:]].{2}[[:upper:]].{3}[[:upper:]].{4}$ | Uppercase at positions 0,1,2,4,8,12,16,21,24,28 |
| 5 | (?:.*\x5f.*) | Contains at least one _ (0x5f) |
| 6 | (?:.[^0-9]*\d.*){5} | Minimum 5 digits |
| 7 | .{24}\x54.\x65.\x54.* | Position 24=T, 26=e (0x65), 28=T |
| 8 | ^.{4}[X-Z]\d._[A]\D\d.................[[:upper:]][n-x]{2}[n|c].$ | Constraints on positions 4-10 and 28-32 |
| 9 | .{11}_T[h|7]\d_[[:upper:]]\dn[a-h]_[O]\d_[[:alpha:]]{3}_.{5} | Constraints on positions 11-27 |
Combining all 9 regex rules, each position (0-32) gets a set of constraints:
Pos Char Source
--- ---- ------
0 H Rule 2 (hex 0x48)
1 T Rule 2 (hex 0x54)
2 B Rule 2 (hex 0x42)
3 { Rule 3 (hex 0x7b)
4 Y Rule 8 [X-Z] + Rule 4 uppercase
5 0 Rule 8 \d
6 u Rule 8 any char
7 _ Rule 5 + Rule 8 separator
8 A Rule 8 [A] + Rule 4 uppercase
9 r Rule 8 \D (non-digit)
10 3 Rule 8 \d
11 _ Rule 9 separator
12 T Rule 9 T + Rule 4 uppercase
13 h Rule 9 [h|7]
14 3 Rule 9 \d
15 _ Rule 9 separator
16 K Rule 4 uppercase + Rule 9 [[:upper:]]
17 1 Rule 9 \d
18 n Rule 9 n
19 g Rule 9 [a-h]
20 _ Rule 9 separator
21 O Rule 9 [O] + Rule 4 uppercase
22 ? Rule 9 \d → digit, but WHICH one?
23 _ Rule 9 separator
24 T Rule 7 (hex 0x54) + Rule 4 uppercase
25 h Rule 9 [[:alpha:]]
26 e Rule 7 (hex 0x65)
27 _ separator
28 T Rule 7 + Rule 8 [[:upper:]]
29 o Rule 8 [n-x]
30 w Rule 8 [n-x]
31 n Rule 8 [n|c]
32 } Rule 3 (hex 0x7d)
Regex constraints give us: HTB{REDACTED?_The_Town} — position 22 remains undetermined (any digit 0-9).
Reverse engineering the check_input function revealed additional validation. The binary extracts 7 substrings and computes the product of ASCII codes for each character (via multiply_characters, using u128 arithmetic), comparing the result with a hardcoded constant:
| Range | Substring | Expected Product |
|---|---|---|
| [4..7) | Y0u | 0x7a070 (499824) |
| [8..11) | Ar3 | 0x5c436 (377910) |
| [12..15) | Th3 | 0x6cc60 (445536) |
| [16..20) | K1ng | 0x27b5776 (41637750) |
| [21..23) | O? | 0x10f9 (4345) |
| [24..27) | The | 0xd76a0 (882336) |
| [28..32) | Town | 0x7465a58 (122051160) |
Key calculation for position 22:
[21..23) = "O?" must yield product 4345ord('O') = 794345 / 79 = 55 = ord('7')7#!/usr/bin/env python3 """ Rega's Town — HackTheBox Reverse Engineering Recovering the flag from regex constraints + ASCII product check """ # 9 regex patterns extracted from the binary via strings regexes = [ r'^.{33}$', # length 33 r'(?:^[\x48][\x54][\x42]).*', # starts with HTB r'^.{3}(\x7b).*(\x7d)$', # pos3={, ends with } r'^[[:upper:]]{3}.[[:upper:]].{3}[[:upper:]].{3}[[:upper:]].{3}[[:upper:]].{4}[[:upper:]].{2}[[:upper:]].{3}[[:upper:]].{4}$', r'(?:.*\x5f.*)', # contains _ r'(?:.[^0-9]*\d.*){5}', # minimum 5 digits r'.{24}\x54.\x65.\x54.*', # pos24=T, pos26=e, pos28=T r'^.{4}[X-Z]\d._[A]\D\d.................[[:upper:]][n-x]{2}[n|c].$', r'.{11}_T[h|7]\d_[[:upper:]]\dn[a-h]_[O]\d_[[:alpha:]]{3}_.{5}', ] # From regex constraints: HTB{REDACTED?_The_Town} # Position 22 is unknown — determine via ASCII product check # Hardcoded product for substring [21..23) = "O?" expected_product = 0x10f9 # 4345 o_ascii = ord('O') # 79 missing_char = expected_product // o_ascii # 4345 / 79 = 55 assert expected_product == o_ascii * missing_char print(f"Position 22 = chr({missing_char}) = '{chr(missing_char)}'") # '7' flag = "HTB{REDACTED}" print(f"Flag: {flag}") # Verification of all product checks substrings = { (4, 7): 0x7a070, # Y0u (8, 11): 0x5c436, # Ar3 (12, 15): 0x6cc60, # Th3 (16, 20): 0x27b5776, # K1ng (21, 23): 0x10f9, # O7 (24, 27): 0xd76a0, # The (28, 32): 0x7465a58, # Town } for (start, end), expected in substrings.items(): substr = flag[start:end] product = 1 for c in substr: product *= ord(c) assert product == expected, f"FAIL: {substr} product={product} expected={expected}" print(f" [{start}..{end}) = '{substr}' → product = {product} ✓") print(f"\nAll checks passed! Flag: {flag}")
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar