Creating a Keygen for Elifoot 98
This is how I created a keygen for an iconic football manager 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.
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).
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]
)
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
):
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?
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:
And I wanted to print the contents of the address accessed by instruction
c4 7e 06
, which is the parameter param_2
:
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 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.
With the formatted string and the password generation implemented, I got these results:
Every single one of these are working passwords!