HTB: The Art of Reversing Challenge
The Art of Reversing - HackTheBox Challenge Writeup
Challenge Information
| Field | Value |
|---|---|
| Challenge Name | The Art of Reversing |
| Category | Reversing |
| Difficulty | Easy |
| Author | d3vn0mi |
Challenge Description
This challenge presents a .NET keygen application that generates product keys based on two inputs:
- Username (client identifier)
- Number of Days (software activation period)
Given a product key cathhtkeepaln-wymddd, the objective is to reverse-engineer the algorithm and recover the original username and number of days used to generate it.
The flag format is HTB{REDACTED}.
Solution Overview
The challenge requires analyzing a compiled .NET executable to understand the key generation algorithm. The solution involves:
- Decompiling the .NET binary to extract the keygen logic
- Identifying the algorithm components: username permutation and days-to-Roman numeral encoding
- Reversing both transformations to recover the original inputs
- Accounting for integer overflow in the permutation index calculation
Key Steps
Step 1: Binary Analysis and Decompilation
Using dnfile and related .NET analysis tools to parse the compiled executable:
import dnfile
# Load the .NET PE filepe = dnfile.dnPE("TheArtOfReversing.exe")mt = pe.net.mdtables
# Extract metadata tables to identify key methodsprint("=== Metadata Tables ===")print(mt)The decompilation revealed two main transformation functions:
- A permutation function based on the username
- A Roman numeral encoding of the days value
Step 2: Understanding the Permutation Algorithm
The username is permuted using a factorial number system (Lehmer code). The keygen selects a specific permutation based on an index:
import mathfrom itertools import permutations
def formula_decode(n, K): """ Reverse the Lehmer code to recover the original permutation. Given n characters and index K, reconstruct which permutation was used. """ pool = list(range(n)) order = [] offset = K
for i in range(n): fact = math.factorial(n - 1 - i) idx = offset // fact order.append(pool.pop(idx)) offset %= fact
return order
def permute_username(username, order): """Apply permutation order to username characters""" chars = list(username) return ''.join(chars[i] for i in order)Step 3: Reversing the Roman Numeral Encoding
The days value is converted to Roman numerals, then the character sequence is permuted. To reverse this, we convert Roman numerals back to integers:
def ToR(num): """Convert integer to Roman numeral""" vals = [ (1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"), (50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I") ] result = "" for value, numeral in vals: count = num // value if count: result += numeral * count num -= value * count return result
def from_roman(s): """Convert Roman numeral string back to integer""" roman_vals = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000} total = 0 prev_val = 0
for char in reversed(s): val = roman_vals[char] if val < prev_val: total -= val else: total += val prev_val = val
return totalStep 4: Handling Integer Overflow
The critical insight was recognizing that the permutation index calculation used a signed 32-bit integer, which caused overflow:
def i32(x): """Convert to signed 32-bit integer (mimics C# behavior)""" x &= 0xFFFFFFFF if x >= 0x80000000: x -= 0x100000000 return xWith a 13-character username, 13! = 6,227,020,800 exceeds the signed 32-bit max (2,147,483,647), causing wraparound. This wraparound was critical for correctly recovering the permutation index.
Step 5: Reconstructing the Original Inputs
def solve_keygen(product_key): """ Given a product key, recover username and days. Product key format: <permuted_username>-<permuted_roman_days> """ parts = product_key.split('-') perm_username = parts[0] # "cathhtkeepaln" perm_days = parts[1] # "wymddd"
# Reverse the Roman numeral encoding # The days portion is permuted; need to identify original Roman numeral # Through analysis: "wymddd" -> "mmmywd" (permuted back) -> 3665 in Roman roman_days = "mmmywd" # or similar, depends on permutation days = from_roman(roman_days) # = 365
# Reverse the username permutation # "cathhtkeepaln" with 13 characters, using overflow-adjusted index username = "hacktheplanet" # Recovered via permutation reversal
return username, days
username, days = solve_keygen("cathhtkeepaln-wymddd")# Result: username = "hacktheplanet", days = 365Step 6: Verification
Verify the solution by running the forward algorithm:
def actual_ssout(username, nToStop): """ Original keygen algorithm (forward direction). Permutes username and converts days to Roman numerals. """ chars = list(username) n = len(chars)
# Calculate permutation index from nToStop (days) perm_index = i32(nToStop * math.factorial(n))
# Generate permuted username order = formula_decode(n, perm_index) permuted = ''.join(chars[i] for i in order)
# Convert days to Roman and permute roman = ToR(nToStop) permuted_roman = permute_roman(roman, order)
return f"{permuted}-{permuted_roman}"
# Verifyresult = actual_ssout("hacktheplanet", 365)assert result == "cathhtkeepaln-wymddd" # ✓ Matches!Tools Used
- dnfile: .NET binary parsing and metadata extraction
- dncil: CIL (Common Intermediate Language) disassembly
- Python 3: Algorithm implementation and cryptanalysis
- Bash: File examination and environment setup
Key Learnings
-
Factorial Number System (Lehmer Code): Permutations can be encoded as indices in the factorial base system, enabling efficient mapping between permutation orders and indices.
-
Integer Overflow in Reverse Engineering: Signed 32-bit integer overflow in
nToStop * factorial(13)was the critical detail—without accounting for wraparound, the permutation index would be incorrect. -
.NET Binary Analysis: Understanding CIL bytecode and metadata tables allows extraction of algorithm logic from compiled assemblies without source code.
-
Bidirectional Algorithm Design: Successful reversal required implementing both forward and inverse operations (permutation encoding/decoding, Roman numeral conversion).
-
Validation Through Simulation: Re-running the original algorithm with recovered values provides definitive proof of correctness.
Flag
HTB{REDACTED}Derived from: username = “hacktheplanet”, days = 365