HTB: The Art of Reversing Challenge

The Art of Reversing - HackTheBox Challenge Writeup

Challenge Information

FieldValue
Challenge NameThe Art of Reversing
CategoryReversing
DifficultyEasy
Authord3vn0mi

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:

  1. Decompiling the .NET binary to extract the keygen logic
  2. Identifying the algorithm components: username permutation and days-to-Roman numeral encoding
  3. Reversing both transformations to recover the original inputs
  4. 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 file
pe = dnfile.dnPE("TheArtOfReversing.exe")
mt = pe.net.mdtables
# Extract metadata tables to identify key methods
print("=== 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 math
from 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 total

Step 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 x

With 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 = 365

Step 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}"
# Verify
result = 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

  1. Factorial Number System (Lehmer Code): Permutations can be encoded as indices in the factorial base system, enabling efficient mapping between permutation orders and indices.

  2. 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.

  3. .NET Binary Analysis: Understanding CIL bytecode and metadata tables allows extraction of algorithm logic from compiled assemblies without source code.

  4. Bidirectional Algorithm Design: Successful reversal required implementing both forward and inverse operations (permutation encoding/decoding, Roman numeral conversion).

  5. 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