Evert's website

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:

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.

Installing the game on a VM

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:

Installed files

If you have played the game, you can map most directories to the various puzzle types by looking at the directory name:

DirectoryPuzzle type
3DSKINOuter Layer
BGLLens Bender
FNFFind and Fill
FPNFocus Point
IMGImage Hole
JESJesse’s Strips
OVEROverlap
RTTRotascope
SLICESlices
TRIGRAMSInterlock

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:

DirectoryContents
MUSICMusic played on the map and puzzle screens
pandora.htm
pandora.mgd
SCREENSMenu sound effects
VOVoiceovers

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:

FileFunction
besttime.datPresumably contains high scores per puzzle
bkant.ttfBook Antiqua font, used in the game
clcd16.dllSafeDisc (copy protection) library
clcd32.dllSafeDisc (copy protection) library
credits.datContains the list of people who developed the game
dplayerx.dllSafeDisc (copy protection) library
drvmgt.dll?
EBUEula.dll?
EBUSetup.sem? (Empty file)
EULA.RTFEnd User License Agreement
int01.avi to int07.aviTrickster capture videos, played when a chapter is completed
langenu.dll?
loadbar.pngShown next to the splash screen when starting the game
nameenu.dll?
open.aviOpening movie, played when first starting the game
pandora.eulExact 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.exeMain game executable, version 1.0a
pandora.rofGame data (226 MB)
puzenu.dll?
puzzle.datGame data (70 kB)
r<player name>.blySave 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<player name>.plySave data, see above
Readme.rtfGame manual
screens.rofAnother data file (68 MB)
SETUPENU.DLL?
splash.pngGame splash screen
statenu.dll?
tricks.bmp? (Not a bitmap image despite the file extension)
tricks.rtr?
UNINSTAL.EXEUninstaller

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:

Hex dump – first listing

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:

Hex dump – second listing

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:

Hex dump – PNG signature

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:

Extracted asset (PNG)

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:

Hex dump – file metadata

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.