Reverse engineering an old Windows game for fun and compatibilityDecember 31, 2022
A few months ago I decided to revisit The Last Express, one of the most unique adventure games of the 1990s. Since the version I have (from 1997) provided a Windows executable, I decided to run it using the Wayland driver for Wine. Eager to treat myself to a nostalgic journey, I ran the game and saw... nothing! I could hear the ominous menu music, but all I got was a minimized window which completely ignored my attempts at restoration. I tried the game with the Wine X11 driver and it worked as expected.
Disappointed, but also determined to get this working with the Wayland driver, I
ran the game with Wine debug logging turned on. The strange minimization
behavior led me to turn my attention to the
calls, hoping that I would get some insight about what was affecting the window
state. Here is what I got for the game start up, with some additional comments:
### Game window being created and resized to 640x480 ) ) ) ) ) ) ) ) ) ) ) ### Game window is minimized (6=SW_MINIMIZE) ) )
When trying to restore the minimized window I got:
### Game window is restored to 640x480... ) ) ) ) ) ) ### ...but then immediately minimized again! ) )
It seemed that some logic was forcing the window to always become minimized. After briefly exploring the possibility of the Wayland driver or Wine core doing something fancy, I reached the conclusion that the minimization logic lived in the application itself.
Why did the application minimize itself when running with the Wayland driver? A mystery befitting the theme of the game itself!
In order to gain more insights, I had to take a peek into the inner workings
of the game. I loaded the
_le.exe executable in Ghidra, analyzed it with
the default options and I searched for symbols references to
The first entry was for the location within the Import Address Table
which the dynamic linker will fill in with the address of the
CWnd::ShowWindow function from
The second entry was the
CWnd::ShowWindow thunk function within
which transfers control to the real
CWnd::ShowWindow function by using
the address from the IAT location described above:
004a8996 JMP dword ptr [->MFC42.DLL::CWnd::ShowWindow]
This import immediately provided a hint that the game was using Microsoft Foundation Classes, i.e., object-oriented wrappers around the Win32 API, and as such I should be on the lookout for standard object-oriented patterns.
The thunk function is what the application actually calls internally, and in
the list above I could see several references to it. Alas, I checked the code
at each reference location, but none was passing
SW_MINIMIZE) as the
show command. Could there be other references to the
that Ghidra was missing?
I decided to get a second opinion from
$ radare2 -A _le.exe [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze function calls (aac) [x] Analyze len bytes of instructions for references (aar) [x] Finding and parsing C++ vtables (avrr) [x] Type matching analysis for all functions (aaft) [x] Propagate noreturn information (aanr) [x] Use -AA or aaaa to perform additional experimental analysis. [0x004a8cf0]> axt 0x004a8996 (nofunc) 0x40fbe8 [CALL] call sub.MFC42.DLL_Ordinal_6215 (nofunc) 0x45a37e [CALL] call sub.MFC42.DLL_Ordinal_6215 fcn.0045a480 0x45a54a [CALL] call sub.MFC42.DLL_Ordinal_6215 (nofunc) 0x45af0e [CALL] call sub.MFC42.DLL_Ordinal_6215 (nofunc) 0x45afa7 [CALL] call sub.MFC42.DLL_Ordinal_6215 (nofunc) 0x45b018 [CALL] call sub.MFC42.DLL_Ordinal_6215 fcn.0045b940 0x45b9a4 [CALL] call sub.MFC42.DLL_Ordinal_6215 fcn.0045b940 0x45b9ea [CALL] call sub.MFC42.DLL_Ordinal_6215 fcn.0045ba20 0x45bb92 [CALL] call sub.MFC42.DLL_Ordinal_6215
There were indeed more references to the
CWnd::ShowWindow thunk. I noted
that the references Ghidra was missing didn't belong to any known function
(nofunc) in radare2), and that was the reason they weren't detected by
default. The solution was to rerun Ghidra's analysis with the "Aggressive
Instruction Finder" option turned on, which gave me the following references:
Note that the
0x45a37e reference was still missing from Ghidra. I was able to
to add it by manually marking the region as code (in fact the relevant function
seems to start at
0x45a340). In the end it didn't matter, though, since that
particular invocation was using the
9 (SW_RESTORE) command, so was not
relevant for our investigation.
From the updated reference list above there were two that involved a
minimization command, at
0x45afa7. The function at
which contained the
0x45af0e reference was decompiled by Ghidra as:
Although the high-level flow of this function was clear enough, I must say I didn't become more enlightened about the condition(s) that could lead to minimization. A quick look at some of the invoked functions indicated that a deeper investigation was required to understand how all this works, so I decided to move to the next function and revisit later, if needed.
The second reference
0x45afa7 was part of the of the
This was more promising, with a very clear check affecting minimization at
the start of the function! As I could see from the decompiler output, the first
argument was effectively a
CWnd *. The unconditional call to
CWnd::OnDisplayChange at the end allowed me to reasonably (and ultimately
correctly) assume that this function was in fact an override of the
OnDisplayChange method for a custom class inheriting from
CWnd, so I
named this function
The documentation for
CWnd::OnDisplayChange was (and still is) surprisingly
scarce online, but in a Microsoft C/C++
I found the following:
CWnd::OnDisplayChange changed to (UINT, int, int) instead of (WPARAM, LPARAM) so that the new ON_WM_DISPLAYCHANGE macro can be used in the message map.
Since the game predated these changes, I determined that
param_2 were really
lparam of the corresponding
WM_DISPLAYCHANGE message. From the
The new image depth of the display, in bits per pixel.
The low-order word specifies the horizontal resolution of the screen.
The high-order word specifies the vertical resolution of the screen.
The pieces of the puzzle finally started to fall into place! Taking into account the new information I updated the decompiled source like so:
There was a lot missing still, but the main takeway, in the highlighted part of the code above, was that if the display mode was changed to one that did not use 16 bits per pixel, the application unceremoniously minimized itself!
Indeed, in that older version of the Wayland driver for Wine we were choosing a
32-bit mode regardless of what the application specified. This worked for most
DirectX games since Wine internally deals with the inconsistency. However, The
Last Express explicitly checks the bits per pixel value sent with the
WM_DISPLAYCHANGE message and our ploy is exposed!
I updated the Wayland driver to properly set the
WM_DISPLAYCHANGE bits per
pixel parameter, and verified that this fixed the minimization problem. I was
finally able to continue my (train) trip down memory lane!
You can see The Last Express in action with the Wayland driver here.
Although this partial analysis was enough to allow me to resolve the task at
hand, I remained curious about some of the details of display configuration and
the unexplained aspects of the
LExpressCWnd::OnDisplayChange function. To
explore further, I tracked symbol references to the
USER32.DLL and came across two invocations in
_le.exe. I will
spare you the details, but one reference was from a function that I determined
configured the display mode for the game (which I named
0x45ba20), and the other from a function
that restored the original mode (
One interesting point about these invocations was that the code was tracking
the kind of display configuration operation the game was performing, by using
LExpressCWnd. For example, in
there was the following decompiled snippet (after giving some more descriptive
LExpressCWnd::configuring_display field and the corresponding
LExpressCWnd::restoring_display used in
two of the mysterious fields used in
LExpressCWnd::OnDisplayChange. The idea
ChangeDisplaySettingsA sends the
synchronously, and thus
LExpressCWnd::OnDisplayChange is called in the
context of that invocation, reading the values of these fields.
With all this new information and a lot of additional exploration in the
executable, I reached this more complete version of
Fortuitously, the discovery of the
LExpressCWnd::ConfigureDisplay/RestoreDisplay functions also helped to shed
some light on the mysterious function at
0x45ae10 which we encountered
earlier in this post. My best guess about that function was that it handles the
(de)activation of the game window, so I named it
activation it increases the game thread priority and configures the display
mode (if in fullscreen mode). On deactivation it sets the game thread priority
to normal, restores the display mode and minimizes the window. My final
decompilation looks like: