Multi-Stage Password Construction and Byte-Level Obfuscation
Problem
The Challenge
This Root Me challenge presents a password-protected Linux binary that employs sophisticated obfuscation techniques to hide its validation password. Unlike simple password checks, this binary constructs its expected password through multiple stages of transformations, making it impossible to extract through simple string analysis.
The challenge demonstrates real-world obfuscation techniques commonly found in malware and copy-protection schemes: multi-stage password construction, runtime memory modifications, and the use of non-printable characters to complicate both static and dynamic analysis.
Primary Objective: Reverse engineer a password-protected binary to extract the correct password by analyzing the multi-stage construction process, tracking byte-level transformations across multiple functions, and properly handling non-printable characters.
Challenge Details
Platform: Root Me
Category: Reverse Engineering
Binary Type: Linux ELF (password-protected)
Key Techniques: Multi-stage obfuscation, runtime modifications, non-printable
characters
Reconnaissance
My approach began with static analysis using a decompiler (likely Ghidra or IDA Pro) to understand the password validation logic without executing the binary. This revealed a complex multi-stage construction process that would have been difficult to analyze through debugging alone.
Initial Binary Inspection
Running the binary without arguments revealed the expected usage:
./crackme [password] (*) -Syntaxe: ./crackme [password]The binary expects exactly one command-line argument: the password to validate.
Decompiling the Main Function
Loading the binary into a decompiler revealed the main() function with several
critical operations that immediately stood out:
int main(int argc, char **argv) {
if (argc != 2) {
printf("(*) -Syntaxe: %s [password]\n", argv[0]);
exit(1);
}
// Stage 1: Allocate memory for password
char *__dest = malloc(0x1d); // Allocate 29 bytes
// Stage 2: Copy initial password data
memcpy(__dest, &DAT_08048910, 0x1f); // Copy 31 bytes from data section
// Stage 3: Modify specific byte positions
*(__dest + 5) = 0x63; // 'c'
*(__dest + 8) = 0x5f; // '_'
*(__dest + 9) = 0x2e; // '.'
*(__dest + 0x16) = 0; // null terminator at position 22
// Stage 4: Call validation function
WPA(argv[1], __dest);
return 0;
}This revealed the first key insight: the password is not stored as a simple string but is constructed through multiple operations.
Stage 1: Initial Data Load
The binary allocates 29 bytes and copies data from address 0x08048910:
__dest = malloc(0x1d); // Allocate 29 bytes
memcpy(__dest, &DAT_08048910, 0x1f); // Copy 31 bytes from data sectionUsing a hex viewer, I extracted the data at address 0x08048910:
xxd binary | grep 08048910 5f3063476a33356d3956355433c38738434a30c38039483935683378646800This 32-byte hex string represents the initial encoded password data before any modifications.
Stage 2: Character Replacements in main()
After copying the initial data, main() modifies specific byte positions:
These modifications replace specific bytes in the copied data with new values. The null terminator at position 22 indicates the password length is 22 bytes.
Stage 3: Final Modifications in WPA()
The WPA() function performs the final validation, but with a crucial twist—it
modifies the password immediately before comparison:
void WPA(char *param_1, char *param_2) {
int iVar1;
// Critical: Modify password just before comparison!
param_2[0xb] = '\r'; // Carriage return at position 11
param_2[0xc] = '\n'; // Newline at position 12
// Perform comparison
iVar1 = strcmp(param_1, param_2);
if (iVar1 == 0) {
blowfish(); // Success
exit(0);
}
RS4(); // Failure
exit(1);
}CRITICAL FINDING: The password is modified immediately before the strcmp() comparison! Positions 11 and 12 are overwritten with non-printable
control characters (\r and \n), meaning we must account for
these final transformations to reconstruct the correct password.
Key Findings
The reconnaissance phase revealed a sophisticated multi-stage obfuscation scheme:
- • Multi-Stage Construction: Password is built through 3 separate transformation stages
- • Data Section Storage: Initial password stored at address 0x08048910
- • Runtime Modifications: Specific bytes modified in main() before validation
- • Last-Minute Changes: WPA() modifies positions 11-12 immediately before strcmp()
- • Non-Printable Characters: Password contains \r, \n, and non-ASCII bytes
- • Fixed Length: Null terminator at position 22 indicates 22-byte password
Attack & Steps
Extracting Raw Binary Data
First, I needed to extract the raw password data from address 0x08048910.
Using xxd to dump the binary's hex content:
xxd binary | grep 08048910 08048910: 5f30 6347 6a33 356d 3956 3554 33c3 8738 _0cGj35m9V5T3..8
08048920: 434a 30c3 8039 4839 3568 3378 6468 00 CJ0..9H95h3xdh.Extracting just the hex values (removing addresses and ASCII representation):
5f3063476a33356d3956355433c38738434a30c38039483935683378646800This 32-byte sequence represents the password data before any transformations.
Developing Password Reconstruction Script
I created a Python script to apply all three stages of transformations in the correct order:
#!/usr/bin/env python3
def reconstruct_password():
"""Reconstruct password by applying all transformations."""
# Stage 1: Original hex data from binary at 0x08048910
hex_string = "5f3063476a33356d3956355433c38738434a30c38039483935683378646800"
password = bytearray(bytes.fromhex(hex_string))
print(f"Stage 1 (Initial data): {password.hex()}")
print(f"Stage 1 (ASCII): {repr(password)}\n")
# Stage 2: Apply main() modifications
password[5] = 0x63 # 'c'
password[8] = 0x5f # '_'
password[9] = 0x2e # '.'
print(f"Stage 2 (main() mods): {password.hex()}")
print(f"Stage 2 (ASCII): {repr(password)}\n")
# Stage 3: Apply WPA() modifications (before strcmp)
password[0xb] = 0x0d # '\r' carriage return
password[0xc] = 0x0a # '\n' newline
print(f"Stage 3 (WPA() mods): {password.hex()}")
print(f"Stage 3 (ASCII): {repr(password)}\n")
# Stage 4: Truncate at null terminator (position 0x16 = 22)
password[0x16] = 0x00
final_password = password[:0x16] # 22 bytes
print(f"Final password (hex): {final_password.hex()}")
print(f"Final password (len): {len(final_password)} bytes")
return final_password
if __name__ == "__main__":
password = reconstruct_password()
# Generate bash command format
bash_format = "$'"
for byte in password:
if 32 <= byte < 127 and byte not in [ord("'"), ord("\\")]:
bash_format += chr(byte)
else:
bash_format += f"\\x{byte:02x}"
bash_format += "'"
print(f"\nBash command format:\n./crackme {bash_format}")This script systematically applies each transformation stage, tracks the changes, and outputs the final password in a format suitable for command-line input.
Executing the Reconstruction
Running the script revealed the complete transformation process:
python3 reconstruct_password.py Stage 1 (Initial data): 5f3063476a33356d3956355433c38738434a30c38039483935683378646800
Stage 1 (ASCII): bytearray(b'_0cGj35m9V5T3\xc3\x878CJ0\xc3\x809H95h3xdh\x00')
Stage 2 (main() mods): 5f3063476a63356d5f2e5433c38738434a30c38039483935683378646800
Stage 2 (ASCII): bytearray(b'_0cGjc5m_.T3\xc3\x878CJ0\xc3\x809H95h3xdh\x00')
Stage 3 (WPA() mods): 5f3063476a63356d5f2e350d0ac38738434a30c38039483935683378646800
Stage 3 (ASCII): bytearray(b'_0cGjc5m_.5\r\n\xc3\x878CJ0\xc3\x809H95h3xdh\x00')
Final password (hex): 5f3063476a63356d5f2e350d0ac38738434a30c38039
Final password (len): 22 bytes
Bash command format:
./crackme $'_0cGjc5m_.5\x0d\x0a\xc3\x878CJ0\xc3\x809'Analyzing the Final Password
Breaking down the 22-byte password by content type:
The password contains three types of data:
- • Printable ASCII: Standard text characters (_0cGjc5m_.5)
- • Control characters: Carriage return (\r) and newline (\n)
- • Non-ASCII bytes: 0xc3, 0x87, 0x80 (potentially UTF-8 encoded characters)
The presence of bytes like 0xc3 0x87 suggests potential UTF-8 encoding,
but for password comparison via strcmp(), these are treated as raw bytes.
Formatting for Command Line
Bash's $'...' syntax allows embedding escape sequences and hex values:
# Format: $'...' allows \xHH escape sequences
./crackme $'_0cGjc5m_.5\x0d\x0a\xc3\x878CJ0\xc3\x809'This format properly represents:
- • Literal printable characters: _0cGjc5m_.5, 8CJ0, 9
- • Hex-escaped control characters: \x0d (\r), \x0a (\n)
- • Hex-escaped non-ASCII bytes: \xc3, \x87, \xc3, \x80
Capturing the Flag
Running the binary with the reconstructed password:
./crackme $'_0cGjc5m_.5\\x0d\\x0a\\xc3\\x878CJ0\\xc3\\x809' [SUCCESS] Password accepted!
[FLAG_REDACTED]Success! The binary executed the blowfish() function and revealed the flag,
confirming that our multi-stage password reconstruction was correct.
Solution & Root Cause
Root Cause Analysis
While this challenge demonstrates sophisticated obfuscation techniques, the password validation mechanism suffers from fundamental security flaws that make it vulnerable to static analysis attacks.
- Security Through Obscurity: Password protection relies entirely on obfuscation rather than cryptography
- Hardcoded Secrets: The complete password is embedded in the binary's data section
- Plaintext Comparison: Uses strcmp() to compare raw bytes instead of cryptographic hashing
- Static Analysis Vulnerability: All transformations are visible in decompiled code
- No Cryptographic Protection: Password stored as plaintext (albeit encoded) rather than hashed
- Predictable Transformations: Byte replacements follow simple, reversible patterns
Why Multi-Stage Obfuscation Failed
The binary employs several obfuscation techniques that would slow down casual reverse engineers:
- • Fragmented Construction: Password built across multiple functions
- • Runtime Modifications: Changes applied just before validation
- • Non-Printable Characters: Complicates string extraction
- • Mixed Encoding: Combines ASCII and non-ASCII bytes
However, these techniques provide zero cryptographic security:
- • Reversible Operations: All transformations can be replayed in order
- • Visible Logic: Decompiled code reveals exact transformation steps
- • No Key Derivation: Password construction doesn't require external secrets
- • Static Data: All password bytes ultimately stored in binary
Attack Chain Breakdown
Static Analysis with Decompiler
↓
Identify main() Function
↓
Extract Data Section Address (0x08048910)
↓
Discover Multi-Stage Modifications
↓
Analyze WPA() Last-Minute Changes
↓
Extract Raw Hex Data from Binary
↓
Apply All Transformations in Order
↓
Format with Non-Printable Characters
↓
Execute Binary with Reconstructed Password
↓
Capture FlagThe Fundamental Flaw: Obfuscation ≠ Security
This challenge perfectly demonstrates why obfuscation is not security:
// WRONG: "Security" through complexity
memcpy(__dest, &DAT_08048910, 0x1f); // Stored in binary
__dest[5] = 0x63; // Visible transformation
__dest[8] = 0x5f; // Visible transformation
param_2[0xb] = '\r'; // Visible transformation
strcmp(user_input, __dest); // Plaintext comparison
// RIGHT: Cryptographic security
unsigned char hash[32];
SHA256(user_input, strlen(user_input), hash);
if (constant_time_compare(hash, stored_hash, 32) == 0) { ... }Obfuscation only increases the time cost of reverse engineering—it doesn't make it cryptographically infeasible. A determined attacker with a decompiler can extract all secrets from an obfuscated binary given enough time.
Remediation
How to Properly Protect Password Validation
For production software, password validation must use cryptographic primitives that make password extraction computationally infeasible, even with complete access to the binary.
Fix 1: Use Cryptographic Hash Functions
- 1 Replace plaintext password storage with one-way cryptographic hashes
- 2 Hash user input and compare hashes, never store or compare plaintext
- 3 Use SHA-256 or SHA-512 for basic hashing (not MD5 or SHA-1)
- 4 Implement constant-time comparison to prevent timing attacks
// WRONG: Current implementation
memcpy(__dest, &DAT_08048910, 0x1f); // Password in binary!
strcmp(param_1, param_2); // Plaintext comparison
// CORRECT: Cryptographic hash
#include <openssl/sha.h>
// Store only the hash (computed offline)
const unsigned char stored_hash[SHA256_DIGEST_LENGTH] = {
0xa3, 0x5f, 0x2e, 0x... // Precomputed hash
};
void validate_password(const char *user_input) {
unsigned char input_hash[SHA256_DIGEST_LENGTH];
// Hash the user input
SHA256((unsigned char*)user_input, strlen(user_input), input_hash);
// Constant-time comparison
if (CRYPTO_memcmp(input_hash, stored_hash, SHA256_DIGEST_LENGTH) == 0) {
blowfish(); // Success
} else {
RS4(); // Failure
}
}Fix 2: Add Salt and Key Derivation Functions
- 1 Use modern password hashing algorithms (bcrypt, scrypt, Argon2)
- 2 Add per-password random salts to prevent rainbow table attacks
- 3 Implement key stretching to make brute-force attacks expensive
- 4 Never use fast hashes (MD5, SHA-1, plain SHA-256) for password storage
// Using Argon2id (winner of Password Hashing Competition)
#include <argon2.h>
#define HASHLEN 32
#define SALTLEN 16
int validate_password(const char *password) {
// Salt and hash stored offline (not hardcoded here!)
uint8_t salt[SALTLEN] = { /* random salt */ };
uint8_t stored_hash[HASHLEN] = { /* precomputed hash */ };
uint8_t computed_hash[HASHLEN];
// Hash user input with Argon2id
argon2id_hash_raw(
2, // iterations
65536, // memory (64 MB)
1, // parallelism
password, strlen(password),
salt, SALTLEN,
computed_hash, HASHLEN
);
// Constant-time comparison
return (memcmp(computed_hash, stored_hash, HASHLEN) == 0);
}Fix 3: Avoid Embedding Secrets in Binaries
- 1 Never hardcode passwords, keys, or sensitive data in executable code
- 2 Load cryptographic material from external secure storage
- 3 Use hardware security modules (HSM) for critical applications
- 4 Implement server-side validation for sensitive operations
// WRONG: Hardcoded in binary
memcpy(__dest, &DAT_08048910, 0x1f); // Embedded password!
// BETTER: Load from secure storage
#include <sys/stat.h>
unsigned char stored_hash[32];
FILE *hash_file = fopen("/etc/app/.password_hash", "r");
if (hash_file) {
fread(stored_hash, 1, 32, hash_file);
fclose(hash_file);
// Set restrictive permissions (owner read-only)
chmod("/etc/app/.password_hash", 0400);
}
// BEST: Server-side validation
// Don't validate passwords in client-side binaries at all!
// Send authentication requests to a secure server via TLSFix 4: Understand the Limits of Obfuscation
Code obfuscation can be part of a defense-in-depth strategy, but it should never be the primary security mechanism:
- 1 Obfuscation increases reverse engineering TIME, not impossibility
- 2 Use as a supplementary defense, not the foundation of security
- 3 Combine with cryptographic primitives for actual protection
- 4 Understand that skilled reverse engineers can defeat any obfuscation
- 5 Focus on cryptographic security over algorithmic complexity
Key Principle: Security should rely on the secrecy of cryptographic keys, not the secrecy of algorithms or code structure (Kerckhoffs's principle).
Secure Implementation Example
Here's a complete example of proper password validation using modern cryptography:
#include <sodium.h> // libsodium for modern cryptography
#define PASSWORD_HASH_SIZE crypto_pwhash_STRBYTES
int main(int argc, char **argv) {
if (argc != 2) {
printf("Usage: %s [password]\n", argv[0]);
return 1;
}
// Initialize libsodium
if (sodium_init() < 0) {
fprintf(stderr, "Failed to initialize cryptography library\n");
return 1;
}
// Load stored hash from secure file (NOT hardcoded!)
char stored_hash[PASSWORD_HASH_SIZE];
FILE *hash_file = fopen("/etc/app/.password_hash", "r");
if (!hash_file || fread(stored_hash, 1, PASSWORD_HASH_SIZE, hash_file) != PASSWORD_HASH_SIZE) {
fprintf(stderr, "Failed to load password hash\n");
return 1;
}
fclose(hash_file);
// Verify password using Argon2id (via libsodium)
if (crypto_pwhash_str_verify(stored_hash, argv[1], strlen(argv[1])) == 0) {
printf("Password accepted!\n");
// Success action
return 0;
} else {
printf("Password rejected!\n");
return 1;
}
}
// To generate the hash (done offline with actual password):
// char hash[crypto_pwhash_STRBYTES];
// crypto_pwhash_str(
// hash,
// password, strlen(password),
// crypto_pwhash_OPSLIMIT_INTERACTIVE,
// crypto_pwhash_MEMLIMIT_INTERACTIVE
// );
// // Store hash in secure file with 0400 permissionsThis implementation:
- • Uses Argon2id, the winner of the Password Hashing Competition
- • Never stores or compares plaintext passwords
- • Loads cryptographic material from external files, not hardcoded
- • Makes brute-force attacks computationally expensive (memory-hard)
- • Includes built-in salt generation and management
- • Provides protection even if the binary is fully reverse engineered
Key Learnings
Obfuscation provides security through time cost, not cryptographic impossibility. Multi-stage password construction, runtime modifications, and non-printable characters can slow down analysis, but any determined reverse engineer with proper tools can extract all secrets from a binary. Real security requires cryptographic primitives (hashing, key derivation) that remain secure even when the algorithm is fully known.
Key Takeaways
- Multi-Stage Obfuscation: Password construction across multiple functions/stages requires systematic tracking of all transformations
- Runtime Modifications Matter: Always analyze functions that modify data AFTER initial construction but BEFORE validation
- strcmp() on Raw Bytes: Password comparison treats all data as raw bytes, including non-printable characters
- Non-Printable Characters: Control characters (\r, \n) and non-ASCII bytes require proper escaping in shell commands
- Hex Analysis Essential: Tools like xxd reveal raw binary data that decompilers might not show clearly
- Obfuscation ≠ Security: Complex transformations slow analysis but don't provide cryptographic protection
- Static Analysis Wins: Decompilers can extract all hardcoded secrets without executing the binary
- Automation is Critical: Python scripts eliminate manual errors when applying multiple transformations
Reverse Engineering Techniques Learned
- • Multi-Function Analysis: Track data flow across multiple functions (main → WPA)
- • Memory Address Extraction: Use hex viewers to extract raw data from specific addresses
- • Byte-Level Tracking: Monitor individual byte modifications through transformation stages
- • Decompiler Workflow: Analyze code structure before extracting raw data
- • Non-Printable Character Handling: Properly format control characters and non-ASCII bytes
- • Systematic Reconstruction: Apply transformations in exact order as binary performs them
- • Validation Testing: Test reconstructed passwords to confirm correctness
Why This Challenge is Valuable
This challenge demonstrates real-world obfuscation techniques commonly found in:
- • Malware Analysis: Malware often constructs C2 URLs or decryption keys through multi-stage processes
- • Copy Protection: Software licensing systems use similar password construction techniques
- • Anti-Reverse Engineering: Commercial software employs runtime modifications to hide logic
- • CTF Challenges: Competition binaries frequently use multi-stage obfuscation
Understanding how to systematically deconstruct these techniques is essential for:
- → Developer-First Security Advocates analyzing malware
- → Penetration testers assessing binary protection
- → Software developers understanding attack vectors
- → CTF competitors solving reverse engineering challenges
Tools & Resources
Further Reading
- → Kerckhoffs's Principle - Security through key secrecy, not algorithm secrecy
- → Schneier's Law - "Anyone can invent a security system so clever that they can't imagine a way to break it"
- → OWASP Password Storage Cheat Sheet - Best practices for password hashing
- → Binwalk - Firmware analysis tool for extracting embedded data