Reverse Engineering a Windows 95 Game
Reversing (Undocumented) Settings
- Reverse Engineering a Windows 95 Game – Reversing Asset Storage (Part I)
- Reverse Engineering a Windows 95 Game – Reversing (Undocumented) Settings (Part II)
- Reverse Engineering a Windows 95 Game – Editor Mode, and Conclusion (Part III)
I recently rediscovered an obscure 1997 Simon & Schuster / Marshall Media edutainment game for Windows 95 that I played as a kid: Math Invaders. In this part, we’ll investigate disassembling and reverse engineering the binary to identify an undocumented settings file format.
As our reverse engineering tool of choice, we’ll be using the National Security Agency’s
Ghidra. This powerful tool allows us to disassemble the MATHINV.EX_
binary that is bundled
on the disk. The first bit of information we get when ingesting the binary in Ghidra is an “Import Results Summary”
dialog, with information about the binary itself. Here’s some excerpts:
Compiler: visualstudio:unknown
Debug Misc: Release/sspyth.exe
PDB File: sspyth.pdb
PE Property[FileDescription]: SSPYTH MFC Application
Interesting - the project in visual studio seems to have originally been called “sspyth”, short for “S.S. Pythagoras”,
the name of the protagonist’s ship within the game. Let’s try and identify the entrypoint. This is a Windows MFC program,
which means the actual entrypoint is “runtime code” that will identify the main MFC module within the program and
initialize it. So instead of looking for this entrypoint (which Ghidra finds for us and names entry
), we will try and
find the main MFC module initializer by searching for something we know happens early in the program’s execution.
When first run, the game checks that DirectX, DirectPlay, and the game CD are inserted. Using Ghidra’s SearchFor Strings... tool we’ll find the “Please insert CD” message.
Clicking the result will select the data in the CodeBrowser, and right-clicking the automatically created symbol allows
us to click ReferencesFind references to s_Please_insert... to find all
references to this particular value within the codebase. Doing so brings up one result at 0x0042cb86. Clicking the
result takes us to the relevant address. The disassembly shows us a function called FUN_0042ca2f(CWinApp *param_1)
,
which we’ll renamed to CWinAppEntrypoint
. As this function is not called anywhere else in the code, we can be fairly
confident that this is only called by runtime code that gets its address programmatically.
Disassembly of CWinAppEntrypoint
(click to expand).
void CWinAppEntrypoint(CWinApp *param_1) {
int iVar1;
undefined4 *puVar2;
FILE *_File;
undefined4 local_28c;
BYTE local_21c [264];
char local_114 [260];
void *pvStack_10;
undefined *puStack_c;
undefined4 local_8;
local_8 = 0xffffffff;
puStack_c = &LAB_0042cc3d;
pvStack_10 = ExceptionList;
ExceptionList = &pvStack_10;
CWinApp::Enable3dControlsStatic(param_1);
CWinApp::LoadStdProfileSettings(param_1,4);
FID_conflict:__mbscpy((char *)local_21c,&DAT_00495378);
FUN_0042d603(s_Version_0049537c,local_21c);
iVar1 = _strcmp(s_1.00-Rel_00495384,(char *)local_21c);
if (iVar1 != 0) {
AfxMessageBox(s_Game_not_installed,_run_the_setu_00495390,0x10,0);
FUN_0042cc47();
return;
}
FID_conflict:__mbscpy((char *)local_21c,&DAT_004953bc);
FID_conflict:__mbscpy(local_114,&DAT_004953c0);
GetPrivateProfileStringA
(s_MazePath_004953dc,s_pakpath_004953d4,&DAT_004953d0,local_114,0x104,
s_.\3d.ini_004953c4);
puVar2 = (undefined4 *)_strlen(local_114);
if (puVar2 == (undefined4 *)0x0) {
FUN_0042d603(s_pakpath_004953e8,local_21c);
FID_conflict:_strcat((char *)local_21c,s_game.pak_004953f0);
while (_File = FID_conflict:__wfopen((char *)local_21c,&DAT_004953fc), _File == (FILE *)0x0) {
iVar1 = AfxMessageBox(s_Please_insert_the_Math_Invaders_C_00495400,0x11,0);
if (iVar1 == 2) {
FUN_0042cc47();
return;
}
}
puVar2 = (undefined4 *)_fclose(_File);
}
AfxSetAllocStop(0x53b0);
local_8 = 0;
if (puVar2 == (undefined4 *)0x0) {
local_28c = 0;
}
else {
local_28c = FUN_0042e186(puVar2);
}
local_8 = 0xffffffff;
*(undefined4 *)(param_1 + 0x1c) = local_28c;
FUN_0042e2e0(*(int **)(param_1 + 0x1c));
FUN_0042cc47();
return;
}
Alright! We can already see some useful things here. FUN_0042d603
gets a value from the game’s Registry key, so that
line just checks that the program is installed. In fact, we can just rename FUN_0042d603
to GetValueFromRegistry
.
Further down we see a GetPrivateProfileStringA
call. I had to look this function up as it’s somewhat esoteric, but it
and the whole GetPrivateProfile*
still supported in today’s Win32 API!
Retrieves a string from the specified section in an initialization file.
GetPrivateProfileStringA
function (winbase.h
) - Win32 apps | Microsoft Learn
This description undersells this singular function call - when called the GetPrivateProfileXxx
family of APIs will
open and read a given *.ini
file, parse it, and return the value in the specified [section]
and key=
. If the given
file does not exist, it will return the default value.
And, using Ghidra’s Symbol Tree, we can find all calls to the GetPrivateProfileXxx
APIs and the parameters used. Doing
so provides us with this list of parameters, expected to be found in .\3d.ini
(relative to the CWD). These are mostly
loaded in another function called by CWinAppEntrypoint
: FUN_0042e2e0
, which we can rename to LoadSettings
:
[MazePath]
pakpath = ; String
datapath = ; String
diskpath = ; String
lastfile = ; String
room = ; String
usepakfile = 0 ; Integer. In practice it is used as a boolean,
; where 0 is FALSE, and anything else is TRUE.
[Render]
fullscreen = 1 ; Integer
winsize = 10 ; Integer
textdetail = 10 ; Integer
Let’s see if this works. Let’s just create a C:\MathInvaders\3d.ini
and as a simple test, we’ll set
[Render]
→fullscreen
to 0
, and…
Well… Sort of. Ok, the game doesn’t actually run, and there’s a weird white space at the bottom of the window. But
we’ve proven it works! But what’s intriguing to me is the [MazePath]
section of the config… I wonder what we could
use those settings for. In particular, the fullscreen
setting is loaded into a global variable that we’ll call
gFullscreen
- this factors into to code processing some very interesting strings about an “editor mode”… I wonder
if we can activate that?
if (gFullscreen == 0) {
if (*(int *)(param_1 + 0x334) == 0) {
_sprintf(local_104,s__Math_Invaders_-_NO_ACTIVE_LEVEL_0049585c);
}
else {
__splitpath(&DAT_0049c7c8,local_1fc,local_1f4,local_12c,local_10c);
_sprintf(local_104,s__Math_Invaders_-_'%s'_004957f4,local_12c);
if (*(int *)(param_1 + 0x3714) == 0) {
FID_conflict:_strcat(local_104,s__-_***_EDITOR_MODE_***_00495810);
}
iVar1 = CSplitterWnd::IsTracking((CSplitterWnd *)(param_1 + 0x2e0));
if (iVar1 == 0) {
FID_conflict:_strcat(local_104,s__-_Running..._0049584c);
}
else {
FID_conflict:_strcat(local_104,s__-_Paused,_press_'p'_to_resume._00495828);
}
}
CWnd::SetWindowTextA(param_1,local_104);
}
Next time!
- Reverse Engineering a Windows 95 Game – Reversing Asset Storage (Part I)
- Reverse Engineering a Windows 95 Game – Reversing (Undocumented) Settings (Part II)
- Reverse Engineering a Windows 95 Game – Editor Mode, and Conclusion (Part III)
Comments from Mastodon
You can leave a comment by replying to this Mastodon post from any ActivityPub-capable social network that can exchange replies with Mastodon.
Comment support inspired by Cassidy James (@cassidy@blaede.family) and some code borrowed from Julian Fietkau (@julian@fietkau.social).