Multi-Stage Password Construction and Byte-Level Obfuscation

Medium
November 4, 2025
#ReverseEngineering#BinaryAnalysis#Decompilation#PasswordCracking#RootMe
01

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.

OBJECTIVE

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.

TARGETS
Password Construction Logic
Multi-Stage Transformations
Non-Printable Character Handling

Challenge Details

Platform: Root Me
Category: Reverse Engineering
Binary Type: Linux ELF (password-protected)
Key Techniques: Multi-stage obfuscation, runtime modifications, non-printable characters

02

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:

$
Binary Syntax
./crackme [password]
Code (Output)
(*) -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:

Code (C (Decompiled))
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:

Code (C)
__dest = malloc(0x1d);  // Allocate 29 bytes
memcpy(__dest, &DAT_08048910, 0x1f);  // Copy 31 bytes from data section

Using a hex viewer, I extracted the data at address 0x08048910:

$
Extract Raw Password Data
xxd binary | grep 08048910
Code (Hex Data)
5f3063476a33356d3956355433c38738434a30c38039483935683378646800

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

Byte Modifications in main()
BYTE VIEW
ADDR Position 5
VALUE 0x63
ADDR Position 8
VALUE 0x5f
ADDR Position 9
VALUE 0x2e
ADDR Position 22 (0x16)
VALUE 0x00

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:

Code (C (Decompiled))
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
03

Attack & Steps

1
Step 1 ⏱ 5 min

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:

$
Extract Password Data
xxd binary | grep 08048910
Code (Hex Dump)
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):

Code (Hex String)
5f3063476a33356d3956355433c38738434a30c38039483935683378646800

This 32-byte sequence represents the password data before any transformations.

2
Step 2 ⏱ 15 min

Developing Password Reconstruction Script

I created a Python script to apply all three stages of transformations in the correct order:

Code (Python)
#!/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.

3
Step 3 ⏱ < 1 min

Executing the Reconstruction

Running the script revealed the complete transformation process:

$
Execute Reconstruction Script
python3 reconstruct_password.py
Code (Output)
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'
4
Step 4 ⏱ 5 min

Analyzing the Final Password

Breaking down the 22-byte password by content type:

Final Password Structure
BYTE VIEW
ADDR Bytes 0-10
VALUE _0cGjc5m_.5
ADDR Bytes 11-12
VALUE \r\n (0x0d 0x0a)
ADDR Bytes 13-21
VALUE 0xc3 0x87 '8CJ0' 0xc3 0x80 '9'

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.

5
Step 5 ⏱ 2 min

Formatting for Command Line

Bash's $'...' syntax allows embedding escape sequences and hex values:

Code (Bash)
# 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
6
Step 6 ⏱ < 1 min

Capturing the Flag

Running the binary with the reconstructed password:

$
Execute with Correct Password
./crackme $'_0cGjc5m_.5\\x0d\\x0a\\xc3\\x878CJ0\\xc3\\x809'
Code (Output)
[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.

04

Solution & Root Cause

HIGH

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

Code (Attack Flow)
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 Flag

The Fundamental Flaw: Obfuscation ≠ Security

This challenge perfectly demonstrates why obfuscation is not security:

Code (C)
// 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.

05

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
Code (C)
// 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
Code (C)
// 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
Code (C)
// 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 TLS

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

Code (C)
#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 permissions

This 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
06

Key Learnings

KEY INSIGHT

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

  • Ghidra - Free reverse engineering framework with excellent decompiler
  • Radare2 - Open-source reverse engineering toolkit with hex viewer
  • libsodium - Modern cryptographic library (recommended for password hashing)
  • Root Me - Cybersecurity training platform with RE challenges

Further Reading