Skip to content

Reverse Engineering a Legacy 2003 CD-Key Installer

Disclaimer: Intellectual Property Redacted

To comply with copyright policies and prevent automated DMCA takedown requests against this repository, all specific references to the target software have been sanitized (at least I tried).

The purpose of this writeup is strictly educational. It focuses on the reverse-engineering methodologies and assembly patching techniques used to analyze legacy x86 validation systems, rather than facilitating the circumvention of copy protection for a specific commercial product.

Historically, many games relied on CD-keys as a form of copy protection. These keys were printed inside the game’s physical packaging and had to be entered during installation or execution, preventing unauthorized copying and redistribution.

The goal of this post is to analyze and bypass this protection mechanism, demonstrating how such legacy systems can be reversed and broken.

This work was done as a project for my Master's Practical and Theory Security Attacks course.

Setup and Reconnaissance

To begin my investigation, I acquired a copy of the game (French version) from an abandonware archive. The distribution came in the form of .bin and .cue disc images, representing the original physical installation media.

Using WinCDEmu, I mounted the images as virtual drives to access the file structure. I then copied the contents to a virtual machine (FlareVM) for safe analysis. As observed in the figure below, the root directory contains the standard installation executable, setup.exe, along with an AutoRun.exe and a Support folder.

View of the mounted disc contents.

View of the mounted disc contents.

Analysis

I executed the installer to observe the protection mechanism in action. After launching the main executable and selecting "Installer", I was presented with a dialogue box requiring a valid serial key.

Launcher interface CD-Key verification prompt

The game's launcher interface and CD-Key verification prompt.

Locating the Validation Routine

My initial strategy was to locate the code responsible for validating the key by intercepting the "Invalid Key" error message. I input an arbitrary wrong key (e.g., "1111-2222-3333-4444-5555"), which triggered a standard Windows error message box.

The error message triggered by an invalid key.

The error message triggered by an invalid key.

I loaded the executable into x32dbg and set a breakpoint on MessageBoxA (from user32.dll). However, upon entering the wrong key again, the breakpoint was not hit.

Subprocess Spawning

Further investigation revealed that the initial process spawns sub-processes. Setup.exe executes AutoRun.exe, which in turn executes another binary located within the Support folder. This explains why my initial breakpoint in the parent process was ignored: the validation logic was occurring in a child process.

I adjusted my strategy by attaching x32dbg to the newly spawned process. By pausing execution and tracing the calls, I successfully intercepted the execution flow before the error message was displayed.

Debugging the process execution flow.

Debugging the process execution flow.

Identifying the Critical Branch

Once I trapped the execution at the error message, I examined the call stack to find the routine that invoked it. By tracing backwards through the assembly instructions, I identified the conditional branch responsible for the validity check.

The logic culminated in a comparison followed by a conditional jump (jne). If the jump is not taken, the program falls through to the error message routine.

The critical conditional jump determining validity.

The critical conditional jump determining validity.

Patching

To verify that this was indeed the sole protection mechanism, I performed a dynamic patch in memory. With the debugger paused at the critical jne instruction, I manually toggled the Zero Flag (ZF) in the CPU registers. This forced the program to treat the comparison as successful, regardless of my invalid input.

Verification

Resuming execution with the modified flag resulted in the installer accepting the invalid key and proceeding to the installation configuration screen.

Successful bypass of the CD-key check.

Successful bypass of the CD-key check.

Permanent Patch

To make this bypass permanent, I modified the binary code. I replaced the conditional jump (jne) with an unconditional jump that ensures the success path is always taken. I then saved the patched executable, effectively removing the requirement for a valid CD key.

Keygen

Now, let's try another approach. Since I already mapped out the key validation function, I might as well reverse it and write a key generator.

To give a quick overview, my input key consists of 20 characters. I observed that it goes through a formatting function and then a validation process. I also noticed that the program closes after 3 failed attempts.

Initial function where key validation is done

Initial function where key validation is done

Validation

The first thing I did was analyze the parameters of the validation function to verify if my input was actually being processed. Using x32dbg, I placed a breakpoint and observed the value was indeed on the stack at 0x0019E580.

This function by itself did not add much to my reversing process; it was essentially just a wrapper that returned either 0 if failed, or 1 if the key was correct. However, all the core logic was being calculated in the subsequent function. This function received two parameters: my input buffer and a numeric value corresponding to 0x1CE0B.

Actual validating function

Actual validating function

This function executed a curious routine: it called a function that I named INPUT_SHUFFLE, which performed fixed permutations on my 20 characters but only retrieved the first 13.

It then proceeded to call a second function, passing this 13-character shuffled value and the number 0x1C30B as parameters. At this point, I was a little bit confused: were only 13 characters being used to validate the entire key?

Just a checksum?

The function being called right after turned out to contain the bulk of the key validation logic. It received the 13 characters and 0x1C30B as input and, after some transformations, looked like the figure below.

Function containing most validation operations

Function containing most validation operations

The operations performed on the input went as follows:

  1. It applied a checksum algorithm, Adler-32, to the 13 characters to generate an integer value.
  2. That integer value was then XORed with 0x1C30B.
  3. The next operation grabbed that output and applied a series of bitwise shifts and substitutions based on fixed tables, converting the input into a 7-character value.
  4. After that, I saw a set of operations that appended the calculated value to the 13 shuffled characters, effectively building a 20-character string. To verify this, I created a breakpoint before the next function call and analyzed the parameters. This is observed below.

    Function Parameter

    Function Parameter

  5. This next function turned out to be a CRC32 (Cyclic Redundancy Check) implementation, which again returned an integer.

  6. Finally, the routine repeated step 3, but utilizing different shifts and different substitution tables to get a final 7-character value.

So, my final buffer held the 13 shuffled values plus 7 calculated values, which went through the shuffle algorithm one last time and returned. This resulting value was then compared against the initially received key. If it differed, the routine returned 0 for an unsuccessful operation.

At a higher level: this function receives a 20-character key, but those characters actually correspond to a 13-character prefix plus 7 checksum characters. Therefore, the algorithm first applies permutations to separate these two groups. It then calculates the expected checksum for those 13 characters and reverses the operation to build the valid key for that specific prefix. If the generated key matches the user input, it is considered valid.

Exploiting

Exploiting this was very straightforward. I chose 13 random characters, replicated every operation done for the checksum in a script, and then applied the final shuffle.

Generated keys

Generated keys


Info

If you liked reading this post, check out Nathan Baggs youtube channel, he was my inspiration for this, and even personally gave me some tips while working on this.