Reverse Engineering Pandora’s Box (1)
One game I have quite fond memories of is Microsofts Pandora’s Box (1999), a puzzle game for PC created by the designer of Tetris. The game consists of a total of 361 puzzles of 10 different types, spread over 7 stages with increasing difficulty. Of course, being designed for Windows 95 and 98, the game shows its age: It is hardcoded to use a full-screen resolution of 640×480, writes its save files to the ‘Program Files’ directory, and shows a black screen when the built-in videos are playing, leaving you with audio only.
Wouldn’t it be cool to bring the game to 2023, getting it to run flawlessly under modern Windows (and possibly other operating systems), and making it possible to add more puzzles, while gathering some reverse engineering experience on the way?
The first goal I set is being able to modify the first Focus Point puzzle, specifically, changing the image used by that puzzle.
Setting up the reverse engineering environment #
For my reverse engineering efforts I installed Windows 10 (64-bit, as I already knew the games runs fine under x64 Windows) on a virtual machine. Other tools I will be using in this post are:
- Microsoft Remote Desktop, to access the VM while having decent graphics/UI performance
- Windows Subsystem for Linux, with Debian as the guest distribution
- Python (installed under Debian in WSL)
- Visual Studio Code and its WSL extension
- HxD as my hex editor. As you may notice in the below screenshots, I tried ImHex at first as it looks much more modern and feature-rich, but I couldn’t get to grips with its search function.
Installing the game #
Let’s get started by installing the game. I used the CD that I still have from back in the day, but the ISO is floating around the web on various abandonware websites if you want to follow along. You also want to grab the 1.0a patch (link to archive.org, as the original has long been taken down), as the game will not run on Pentium III and up without it.
I chose to do the ‘Full’ installation, so all game data is copied to the C: drive. Afterwards, install the 1.0a patch linked earlier.
By changing a single registry key, we can trick the game so that the CD is not required for launching, which is particularly useful when you used a physical disk to install the game. Launch the registry editor and navigate to:
# 32-bit Windows:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft Games\Pandora's Box\1.0
# 64-bit Windows:
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Microsoft Games\Pandora's Box\1.0
Change the value of the key ‘CDROMDIR’ to the path where the game was installed, e.g. ‘C:\Program Files (x86)\Microsoft Games\Pandora’s Box’. The game may now be launched without the CD in the (virtual) drive.
First steps #
Let’s start by taking a look at the game files we installed:
If you have played the game, you can map most directories to the various puzzle types by looking at the directory name:
Directory | Puzzle type |
---|---|
3DSKIN | Outer Layer |
BGL | Lens Bender |
FNF | Find and Fill |
FPN | Focus Point |
IMG | Image Hole |
JES | Jesse’s Strips |
OVER | Overlap |
RTT | Rotascope |
SLICE | Slices |
TRIGRAMS | Interlock |
All the above directories initially contain only sound effects in the WAV file format. After playing a few puzzles, ‘.rim’ files started appearing in some of these directories. Next, we have another set of directories containing mostly WAV files:
Directory | Contents |
---|---|
MUSIC | Music played on the map and puzzle screens pandora.htm pandora.mgd |
SCREENS | Menu sound effects |
VO | Voiceovers |
The ‘pandora.htm’ and ‘pandora.mgd’ files in the ‘MUSIC’ directory seem to describe some sort of map or graph. Opening the HTM file in Firefox results in the message ‘(Browser does not support controls.)’. Out of curiosity I tried opening the HTM file under Windows 98 SE/Internet Explorer 5, but it showed the same message.
Finally, we have the ‘HELP’ directory which contains two files: ‘message.m20’ and ‘pandora.m20’. Inspecting them using the ‘string’ command in WSL reveals that they contain text sequences used in the game, like the instructions shown during tutorials.
Jumping back to the root directory, we have the following files:
File | Function |
---|---|
besttime.dat | Presumably contains high scores per puzzle |
bkant.ttf | Book Antiqua font, used in the game |
clcd16.dll | SafeDisc (copy protection) library |
clcd32.dll | SafeDisc (copy protection) library |
credits.dat | Contains the list of people who developed the game |
dplayerx.dll | SafeDisc (copy protection) library |
drvmgt.dll | ? |
EBUEula.dll | ? |
EBUSetup.sem | ? (Empty file) |
EULA.RTF | End User License Agreement |
int01.avi to int07.avi |
Trickster capture videos, played when a chapter is completed |
langenu.dll | ? |
loadbar.png | Shown next to the splash screen when starting the game |
nameenu.dll | ? |
open.avi | Opening movie, played when first starting the game |
pandora.eul | Exact copy of pandora.exe. Interestingly, on the CD this file matches the 1.0a executable, while the 1.0a patch is not even included on my disc! |
pandora.exe | Main game executable, version 1.0a |
pandora.rof | Game data (226 MB) |
puzenu.dll | ? |
puzzle.dat | Game data (70 kB) |
r |
Save data, only created after starting the game. Note: if the game is not launched as administrator, these files will instead be written to ‘%LOCALAPPDATA%/VirtualStore/Program Files (x86)/Microsoft Games/Pandora’s Box/’. |
r |
Save data, see above |
Readme.rtf | Game manual |
screens.rof | Another data file (68 MB) |
SETUPENU.DLL | ? |
splash.png | Game splash screen |
statenu.dll | ? |
tricks.bmp | ? (Not a bitmap image despite the file extension) |
tricks.rtr | ? |
UNINSTAL.EXE | Uninstaller |
I determined the above information by inspecting the files with the ‘file’ and ‘strings’ commands under WSL. and by looking at their filenames.
The main files of interest are of course the large .rof files, as we haven’t seen any of the in-game asssets yet. Presumably these assets are combined into the .rof files.
Let’s take a look at the ‘pandora.rof’ file through ‘strings’:
strings pandora.rof | less
In the output we can immediately spot a few level and puzzle names, as well as a lot of PNG files:
3DSKIN
LEVEL.0
LEVEL.01.MAUI
LEVEL.02.PUCK
LEVEL.03.ERIS
LEVEL.04.COYOTE
LEVEL.05.MONKEY
LEVEL.06.ANANSI
LEVEL.07.RAVEN
LEVEL.08.SCALAWAG
OVER
SLICE
TRIGRAMS
302.PNG
BRACKET0.PNG
BRACKET1.PNG
BRACKET2.PNG
BRACKET3.PNG
DNARROW_D.PNG
DNARROW_DH.PNG
DNARROW_U.PNG
...
Let’s take a look through a hex editor:
At 0x184 – 0x23A we immediately spot something that looks like a directory listing. A bit further in the file, at 0x436, we spot another listing, this time of PNG files:
Let’s write down the following information for now:
- The first listing starts at 0x184 and is 0xB6 bytes long (including the final null terminator).
- The first listing contains 19 directory or file names (separated by null bytes).
- The second listing starts at 0x436 and is 0x14A bytes long.
- The second listing contains 25 directory or file names.
Let’s look at the start of the file next. As the game is designed to run on 32-bit Windows systems, we can assume all integers are 32 bits/8 bytes long, and the byte order is little endian.
The first integer (13 00 00 00 / 0x13 hexadecimal / 19 decimal) seems to denote the length of the directory index. We validate our hypothesis against the ‘screens.rof’ file: it starts with the sequence ’01 00 00 00′, and indeed contains only one directory named ‘SCREENS’.
Immediately next, at 0x4, we find the value 0xB6. This matches the length of our directory listing.
The next five integers seem to describe the first entry from the listing:
- 0x23A: this value matches a position immediately after the listing. This is probably the offset where the referred directory or file starts.
- 0x0 and 0x1: we don’t yet know what these values mean. Let’s ignore them for now.
- 0x7: this value matches the length of the file name, including the null terminator at the end.
- 0x0: this value is the offset of the file name relative to the start of the name table.
Again, we can check these hypotheses against either the ‘screens.rof’ file, or more conveniently, the next entry in the structure:
- 0x580: this again is a offset right after the second listing.
- 0x0 and 0x1: we still don’t know the meaning of those values, but they are the same as for the first entry.
- 0x4: matches the length of the name (‘BGL’, including a null terminator).
- 0x7: indeed matches the offset of the name relative to the name table offset.
We can conclude that the metadata entries thus consist of 5 integers, and as such are 20 bytes long each.
We can conclude that the metadata entries thus consist of 5 integers, and as such are 20 bytes long each.
Now that we can parse directory listings, let’s look at the actual files embedded in the .rof file. In the second listing we see that there are PNG files present in. Wikipedia tells us all PNG files start with the sequence ’89 50 4E 47 0D 0A 1A 0A’. Let’s search the file for this sequence:
We get a first match at offset 0x526F2. It seems the contents .rof files are not compressed or encrypted in any way. Furthermore, notice the file starts immediately after the listing, so we know there is no metadata behind the listings.
Because the second file in the listing is also a PNG file, we can search for the signature again. The second signature occurs at 0x82C28. Assuming there is no metadata between the files, we can dump the region 0x526F2…0x82C27 and the result should be a valid PNG image:
And indeed, the range contains an asset from the game. We can also use the offsets (0x526F2 and 0x82C27) and size (subtracting the offset leaves 0x30536) to look for the metadata describing it:
The offset is found in between the first and second listing; the size is found immediately thereafter. We can also spot the offset of the second file (0x82C28) at 0x256..0x259, so we know the file entries too are 20 bytes in size.
The two integers between the directory listing and the offset of the first file contain values 0x19 and 0x14A, which again correspond to the number of files and the size of the name table.
Using all the above information, we can write a simple parser in Python that can print the directories and the files in them:
Resulting in the following output:
3DSKIN
302.PNG
BRACKET0.PNG
BRACKET1.PNG
BRACKET2.PNG
BRACKET3.PNG
DNARROW_D.PNG
DNARROW_DH.PNG
DNARROW_U.PNG
DNARROW_UH.PNG
REVLF_D.PNG
REVLF_DH.PNG
REVLF_U.PNG
REVLF_UH.PNG
REVRT_D.PNG
REVRT_DH.PNG
REVRT_U.PNG
REVRT_UH.PNG
ROTATE_D.PNG
ROTATE_DH.PNG
ROTATE_U.PNG
ROTATE_UH.PNG
UPARROW_D.PNG
UPARROW_DH.PNG
UPARROW_U.PNG
UPARROW_UH.PNG
...
LEVEL.0
PUZZLE.1.IMH
PUZZLE.10.BGL
PUZZLE.11.TAN
PUZZLE.12.FNF
PUZZLE.2.ZOO
PUZZLE.3.RTT
PUZZLE.4.FSH
PUZZLE.5.SKI
PUZZLE.6.CHIS
PUZZLE.7.JESS
PUZZLE.8.RUB
PUZZLE.9.SLICE
LEVEL.01.MAUI
CITY.06.CAIRO
CITY.10.HONOLULU
CITY.16.MADRID
CITY.21.NEW YORK
CITY.22.PARIS
PUZZLE.1.OVL
...
The files in the puzzle directories (3DSKIN, FNF, FPN etc.) seem to be UI elements only. Looking at the level data, it is a reasonable assumption that the ‘files’ below them are actually be subdirectories.
LEVEL.0 probably contains the tutorial puzzles as there is one puzzle of each type, and LEVEL.01 to LEVEL.07 contain a directory per city (which presumably contain the puzzles for that city) and one to three standalone puzzles for the ‘boss fight’. Interestingly, there is a directory named ‘LEVEL.08.SCALAWAG’, which possibly contains the 40 additional puzzles from the ‘Puzzle Game of the Year Edition‘ re-release. As far as I know however my version of the game is not the Puzzle Game of the Year Edition (it is the European release by Xplosiv), so this raises the question whether the extra puzzles have always been included.
It seems like both directories and files are described by the same 20 bytes of metadata. Let’s look for a directory/file flag. Recall that there is one field that we do not know the meaning of yet. Let’s log the size and the unknown field for every entry:
...
LEVEL.0 / 0 / 1
PUZZLE.1.IMH / 0 / 1
PUZZLE.10.BGL / 0 / 1
PUZZLE.11.TAN / 0 / 1
PUZZLE.12.FNF / 0 / 1
PUZZLE.2.ZOO / 0 / 1
PUZZLE.3.RTT / 0 / 1
PUZZLE.4.FSH / 0 / 1
PUZZLE.5.SKI / 0 / 1
PUZZLE.6.CHIS / 0 / 1
PUZZLE.7.JESS / 0 / 1
PUZZLE.8.RUB / 0 / 1
PUZZLE.9.SLICE / 0 / 1
LEVEL.01.MAUI / 0 / 1
CITY.06.CAIRO / 0 / 1
CITY.10.HONOLULU / 0 / 1
CITY.16.MADRID / 0 / 1
CITY.21.NEW YORK / 0 / 1
CITY.22.PARIS / 0 / 1
PUZZLE.1.OVL / 0 / 1
...
Clearly, the unknown field denotes the type of the entry: 0 means file and 1 means directory. Furthermore, all directories have a size of 0, so we can ignore this field when processing a directory.
The final directory metadata structure thus looks as follows:
Let’s rework our script so it can parse an arbitrary mix of directories and files:
And indeed there are a lot of nested directories below the ‘LEVEL’ folders:
...
LEVEL.01.MAUI:
CITY.06.CAIRO:
PUZZLE.01.FPN:
PUZZLE.ZOO
PUZZLE.02.SLC:
1OBJ_1.PNG
1OBJ_1A.PNG
1OBJ_2.PNG
1OBJ_2A.PNG
...
In the next post, we will take a look at the following tasks:
- Extracting the files we just listed;
- Repacking the .rof file after making changes;
- Examining the .ZOO file that contains a Focus Point puzzle, and modifying the image embedded in it.
- Previous post: Dealing with the Proxmox Subscription Notice
- Next post: DLL Injection and Function Hooking