Creating a Keygen for Elifoot 98

This is how I created a keygen for an iconic football manager game.

01 - The game

Elifoot 98 was a football manager game very well known in the 2000s in Brazil. The player started as the coach of a random tier-4 team of the Brazilian Football League, and was able to negotiate players, salaries, and adjust lineups. If the team was somewhat successful, the coach could be promoted to a higher-tiered team. The games were simulated and included cards, faults, injured players, penalties and, of course, goals.

Elifoot 98 opening screen!
Elifoot 98 opening screen!
Games in action!!
Games in action!!

The game was a shareware, and the first screen already prompted for a password, given a serial key. In the old days we used to just download a keygen from the Internet and play the game to its fullest. But now my goal was to understand the intricacies of the password generation algorithm. But no code of the keygen would be shared (and this game has been discontinued anyway).

What's the password?
What's the password?

02 - Starting point

In order to run Elifoot 98 (16-bit application) in Windows 10 I used an emulator, winevdm. With winevdm installed I was able to run Elifoot seamlessly.

My first approach for reverse engineering the password generator was the good and old Cheat Engine. I tried searching the memory for the password or the serial number, but in the end I always hit a dead end. One of the main problems was that the game was 16-bit whereas Cheat Engine disassembles to 32 or 64-bits. x64dbg didn’t help either: it wouldn’t load the process (or at least I couldn’t make it to).

I went back to winevdm. Since it can run the game, it should definitely know how to read and interpret its code. My first approach would be to log the game instructions being executed in real-time by the emulator, and somehow detect those performing the password generation.

I cloned the repo and, since the debugger in Visual Studio would crash when setting breakpoints in the emulator code, I had to build and run the project with its debug flags enabled. I would then manually trigger the serial number check, by pressing OK in that box after filling in the password, and check the assembly code being executed. However, this approach failed because there was so much data being printed to the terminal that the game wouldn’t even launch. I needed to filter out some of these instructions.

I started to dig into the code, more specifically into the file doing the actual processor emulation: msdos.cpp. There were 3 important variables:

  • opcode, which is an array of the current opcodes being executed in that particular frame (for example, 26 8a 05 30 e4 8b f0 d1 e0 01 f0 8b d0)
  • opsize, which is the size of the opcode array (13 bytes in the previous case)
  • buffer, which is the disassemble of the opcode array (for example, mov al,es:[di])
msdos.cpp internals
msdos.cpp internals

My next approach was to create a simple memory scan in msdos.cpp:

  • Parse buffer and check any memory accesses. Basically check every instruction that has a dereference symbol ([]).
  • Check if the memory address holds the serial number 014-370-...
  • Print the assembly instructions accessing this address and the next ~10 instructions, so that I could scan for their opcodes in the next step.

My script to run winevdm would look like this:

.\otvdm.exe "%RUN_EXE_PATH%" --find-out-what-accesses "014-370-"

I added the option –find-out-what-accesses to the emulator in order to create a more generic interface, instead of hardcoding the string to be searched.

And the code for the memory scanning was:

static BOOL _scan_memory_for_str
    (
    UINT32 mem_addr,    /* memory address */
    const char* str     /* string to search */
    )
{
    for (size_t i = 0; i < strlen(str); ++i) {
        if (read_byte(mem_addr + i) != str[i]) {
            return false;
        }
    }

    return true;
}

static BOOL _check_findout_what_accesses
    (
    const char *buf,    /* assembly instruction */
    const char *str     /* string to search */
    )
{
    UINT32 mem_addr = _get_mem_address(buf);
    return (mem_addr && _scan_memory_for_str(mem_addr, str));
}

The function _get_mem_addr() contains the logic to get the memory address currently being accessed by buffer. It returns 0 if buffer is not accessing any address. The function _scan_memory_for_str() searches for a given string starting at the memory address mem_addr.

With that I got to the following assembly instruction/opcodes accessing the serial number 014-370-...:

[opcodes]   [instructions]
26 8a 05    mov     al,es:[di] <--
30 e4       xor     ah,ah
03 c2       add     ax,dx
05 03 00    add     ax,3h
99          cwd
b9 0a 00    mov     cx,0Ah
f7 f9       idiv    cx
92 xchg     ax,dx
05 30 00    add     ax,30h
8a d0       mov     dl,al

That’s good progress, because now I could use Ghidra to search the executable for that array of bytes (26 8a 05 30 e4 8b f0 d1 e0 01 f0 8b d0) and get the decompiled function. And that’s what I got (FUN_1058_2f8b, let’s call it function_do_logic):

function_do_logic: the main password generation function!!
function_do_logic: the main password generation function!!

That logic in line 33 looks a lot like an algorithm to generate a password from a serial code. But, in order to confirm this, I had to know what were the parameters being passed to the function. Surely one of the parameters is the serial code, as seen from my memory scanner, but is it always the case?

03 - Getting there

With the aim of printing the contents of what’s being passed to function_do_logic, I had to implement an array of bytes (AOB) scanner in msdos.cpp. Because I needed to print the contents of an address being accessed by a specific sequence of instructions, I first needed to find that exact sequence of opcodes.

For instance, function_do_logic starts with the following opcodes:

function_do_logic initial opcodes: c4 7e 06 26 8a 05 30 e4 3b 46 fc 7d 29
function_do_logic initial opcodes: c4 7e 06 26 8a 05 30 e4 3b 46 fc 7d 29

And I wanted to print the contents of the address accessed by instruction c4 7e 06, which is the parameter param_2:

Instruction c4 7e 06
Instruction c4 7e 06

Since there could be an infinite number of this opcode in the code, but only a single sequence of opcodes, I first scan for the sequence c4 7e 06 26 8a 05 30 e4 3b 46 fc 7d 29 and then print the contents of the address accessed by the desired instruction c4 7e 06.

This is a unique AOB within the executable file!
This is a unique AOB within the executable file!

This is how I called the emulator to run an AOB scan around that opcode sequence:

.\otvdm.exe "%RUN_EXE_PATH%" --aob-scan "c47e06268a0530e43b46fc7d29"

By printing the contents of param_2, I could gather that this function is executed 7 times, each time generating a valid password. The string would have prefixes and suffixes depending on the registration type. The following table summarizes the overall logic:

Iteration # String format Registration type Notes
0 1***serialserial Super Vip serial is the serial code with a length of 23 bytes. Then apply function_do_logic
1 ,+++password***password Friend of authors buf is the previous buf with a length of 19 bytes. Then apply function_do_logic
2 ,+++password***password Big friend of authors buf is the previous buf with a length of 19 bytes. Then apply function_do_logic
3 ,+++password***password Tester buf is the previous buf with a length of 19 bytes. Then apply function_do_logic
4 @1213bufbufXXXbuf Special tester buf is the previous buf with a length of 19 bytes. Then apply function_do_logic
5 @1213bufbufXXXbuf Author buf is the previous buf with a length of 19 bytes. Then apply function_do_logic
6 @1213bufbufXXXbuf Author buf is the previous buf with a length of 19 bytes. Then apply function_do_logic

For each iteration function_do_logic is applied and outputs a valid password.

04 - Wrapping up

With the formatted string and the password generation implemented, I got these results:

The keygen in action!
The keygen in action!

Every single one of these are working passwords!

Big friend of authors!!
Big friend of authors!!