Debugging “Dust: A Tale of the Wired West”

Dust: A Tale of the Wired West” is a game produced by Cyberflix in 1995, which was made to run on Windows 3.11 and Windows 95. After Windows 98, the game was simply not playable on modern computers, because it would immediately crash.

I managed to patch the game so that it works again on Windows 7 32-bit and on Windows XP on modern computers.

People got the game to work on modern computers using DosBox running a Windows 3.11 system.
So this blog is not about making this game playable for everyone, it’s about learning how to use debugging tools and solving and interesting puzzle.

This is the first bug I fixed, so I am by no means an expert at debugging and my patch will definitely not win any awards.
Mostly, I hope that I can show some of you, that you don’t need to be an absolute code wizard to start diving into this kind of stuff.

I’d love to hear your suggestions and feedback.


What did I use?

A Virtual Machine

In order to troubleshoot this problem, I set up a Windows 7 32bit VM using Oracle VirtualBox. There are plenty of guides online on how to configure these.
If you’re planning on debugging on several platforms, I would also advise to look into Vagrant, because it allows you to set up a VM really quickly using images that other people made for you. It saves a lot of time.

A debugger

To debug this application we will need something that works on a 32-bit (x86 architecture) system.
I decided to go with x32dbg, but I’ve heard good things about WinDbg and OllyDbg.
You will need to install this on the VM.

A decompiler

Debuggers and disassembler show the Assembler code of the application. A decompiler attempts to convert this to pseudocode (a lot like C++) which makes it a bit more readable (for me). For this purpose I chose Ghidra. An alternative to this could be IDA Pro.

Dust: A Tale of the Wired West

This game has been marked as abandonware and can be found on MyAbandonware.
The game can be installed on 32bit Windows systems and launched from the CD. The actual executable that we’ll be working with can be found in the installation folder (unless you installed to a custom directory):

C:\Program Files\Cyberflix\Dust\DF.exe

Be careful of the address values I’m using in this blog. There are several versions of this game available for download so the exact location in the executable may vary.


Finding the Error Message using x32dbg

EXCEPTION_INT_DIVIDE_BY_ZERO in x32dbg

I opened the Dust executable (DF.exe) in x32dbg and just pressed the run button (the right-arrow in the toolbar) until I got stuck. By default x32dbg stops when Exceptions are thrown. So we can see exactly at which memory address the error occurred. At the very bottom the screen we can see a “division by zero” exception.
Looking at the assembler code, we can’t immediately see what’s going on at this point, but we know that there’s a division happening at address 0x0042B459.


Decompiling using Ghidra

While x32dbg can monitor the code while it’s executing, Ghidra is great at doing analyses of static code. I copied the executable to my main machine, opened it in Ghidra, told it to analyze the code and jumped to address 0x0042B459.

Loading executable into Ghidra and analyzing the code.

This is the point where it helps a lot if you have a programming background, because you’re trying to figure out what the code is trying to do. The great thing about Ghidra is that you can freely annotate by renaming functions and variables and adding comments wherever you like. The executable remains untouched.

Below is the relevant code near 0x0042B459 before I annotated it. You can jump to it by pressing “G” and filling in the address (42b459).

  iVar8 = timeGetTime();
  sVar2 = FUN_0042cb90(&stack0xfffffdf4,uVar3,0x4b000);
  iVar4 = timeGetTime();
  if (sVar2 != 0) {
    FUN_0042d1e0(0x6b,0x1a4);
  }
  uVar6 = (uint)(0x124f8000 / (ulonglong)(uint)(iVar4 - iVar8));
  GlobalFree(uVar3);
  FUN_0042cfb0(&stack0xfffffdf4);
  if (uVar6 < 0x43800) {
    uVar3 = FUN_004299c0(0x7d,uVar6 >> 10);
    FUN_0043bfd4(auStack392,uVar3);
    uVar3 = FUN_004299c0(0x7e,auStack392);
    iVar8 = FUN_0042ac80(uVar3);
    if (iVar8 != 0) {
      return 1;
    }
  }

Analyzing the code

We can now see on line 7 that it’s trying to divide a number by (iVar4 – iVar8). These are assigned in lines 1 and 3. On line 2 it runs some function. In other words, it’s calculating how long it took to run the function and using that in a calculation on line 7.

Before I try to fix this problem, I’m curious what FUN_0042cb90 does on line 7. A nice thing that Ghidra does after it’s finished analyzing the code, is add comments when variables point to other parts of the executable. FUN_004299c0 on line 11 has one of these comments.

Ghidra code-comment showing error message.
undefined * FUN_004299c0(UINT param_1)
= u"Dust runs best on a double-speed or faster CD-ROM drive (300 KB/sec).  The CD-ROM drive in your computer has an average speed of %d KB/sec.  Dust will still run, but some performance will be lost.  Choose \"Ok\" to continue or \"Cancel\" to return to Windows."

At this point we can assume that line 2 in between the two timestamps is a function that reads a specific file. So we’re measuring the read-speed of the CDROM drive. If we were to open function FUN_0042cb90 we can see that it contains a ReadFile() function.

So I labeled the variables and functions by pressing “L” and I assigned data-types and converted the numbers by right-clicking on them in the Listing window of Ghidra.

  timeBefore = timeGetTime();
  errorStatus = ReadFile(&local_204,hMem,0x4b000);
  timeAfter = timeGetTime();
  if ((short)errorStatus != 0) {
    FUN_0042d1e0(107,420);
  }
  GlobalFree(hMem);
  FUN_0042cfb0(&local_204);
  if ((uint)(307200000 / (ulonglong)(timeAfter - timeBefore)) < 276480) {
    pbVar4 = GetErrorMessage(125);
    FUN_0043bfd4((byte *)local_180,pbVar4);
    puVar9 = local_180;
    pCVar3 = GetErrorMessage(0x7e);
    uVar6 = CreateMessagePrompt(pCVar3,(char *)puVar9);
    if (uVar6 != 0) {
      return 1;
    }
  }

The first patch

Now that we have an idea what this code does, we can try to fix it!
A nice feature of x32dbg is that it allows you to make changes to the assembler code in the form of patches and apply them to the executable.

My understanding is that when you try to patch code on assembler level, you’re trying to leave as small a footprint as possible. So one way to make sure that this code never divides by zero, is by swapping the division for a multiplication.

If we look at address 0x0042B459 again, we can see the assembler instruction:

div esi

The esi we see in this command above is a general purpose register for the CPU that can hold values. In this case it holds which values should be divided. The div is the assembler command for division.
Some resources for learning more about assembler instructions is Guide to x86 Assembly and the x86 instruction listings wiki.

By pressing the spacebar in x32dbg we can open the “assemble” prompt and change it to a multiplication.

mul esi
First patch in x32dbg

When you press OK, it will jump to the next address and you can press Cancel. Next we can apply the patch from the file menu and save it as a separate executable. (e.g. DF_PATCHED_V1.exe)
I would not advise overwriting the original executable.

Running it from the installation folder yields the following result:

Game running with first patch.

It works!!!


Second patch

We’ve got a working game again. It’s definitely a sufficient fix, however we would ideally like to get rid of that prompt window.

The issue is that we fixed the divide-by-zero exception, but now we will always end up inside the if- statement branch of the code. There are several ways of tackling this problem, but the one I used targets the comparison of the if-statement.

if ((307200000 * (timeAfter - timeBefore)) < 276480) { ... }
becomes
if ((307200000 * (timeAfter - timeBefore)) > 276480) { ... }

By changing the smaller-than to a larger-than operator, we will never end up inside the if-statement.

I looked up the address of the if-statement (0x0042B476) in Ghidra and jumped to that line in x32dbg by pressing Ctrl+G.

jae df.42B4D6

The jae stands for “Jump if above or equal” (>=) and is followed by the address that it jumps to if the comparison is true. Notice that this is the opposite of an if-statement in C++ code (and other high-level programming languages) where an if-statement uses “Enter if lower” (<).
We’re going to change it so that it always jumps to address 0x0042B4D6:

jbe df.42B4D6

We can apply the patch again and save it as a new executable (e.g. DF_PATCHED_V2.exe) and run it:

Game running with second patch.

Now it really works!!!

I could also have fixed the if statement by simply jumping past all of the code inside if-statement with a jmp instruction. However, I was having some issues with the colorscheme of the game, so I kept the jbe instruction.


Why was this bug in here?

Back in the 1995 when this game came out, CD-ROM players were under heavy development. Throughout their lifetime they went from single speed to 32x speed. So there were still a lot of computers out there that had single speed CD-ROM drives.

Now that we’re trying to run these games on virtualized systems where there is no physical CD-ROM drive, but everything happens in super fast RAM memory. Those two timestamps are quite often the exact same number. This also explains why I could get the game to run sometimes when my computer was busy calculating. For example when Windows was still booting and loading services.

A simple way of preventing this bug from ever occuring could have been to simply add 1.

(timeAfter - timeBefore + 1)

I considered using this as a fix, but that would have meant adding an extra instruction to the executable and I wasn’t sure what that would do to all the memory pointers since all the addresses after that would be shifted by 1.
It turns out there’s a lot of smart tricks to circumvent this issue, by jumping to an unused part of the program memory, adding your instructions there and jumping back. I was made aware of this by several Reddit users. See the Thank You’s and Further Reading sections below.


Final thoughts

So, I never ever thought that I would be technical enough to tackle these kinds of problems. I’ve learned that a lot of this is just a matter of getting an understanding of how these tools work.

I didn’t manage to get the hang of reverse engineering in a single attempt. I had a few false starts because I was simply overwhelmed by the complexity of the tools. I’d been working with Ghidra for a while, but the moment I started using x32dbg some of the puzzle-pieces fell into place. From that point on I spent about 4 hours figuring out how to fix the problem.

I’d advise anyone to try fixing these kinds of problems, or at least just open up a debugger when you get an error message for curiosity sake.


Thank You’s

I’d like to show my appreciation to the Reddit communities /ReverseEngineering and /REGames for giving me lots of constructive feedback and helping me learn.
Especially to users:

  • s-ro_mojosa for helping me add a little more context to the Final Thoughts sections.
  • Prasselpikachu for explaining the nuances between a disassembler, a decompiler and a debugger.
  • rolfr and lickedwindows for explaining how you can add instructions without having to shift any further instructions. (jumping to a free part of the program and back)
  • mttd for this great list of instructional resources in the Further Reading section below.

The original Reddit posts can be found here:


Further Reading

On the topic of binary patching in general, “Binary Rewriting without Control Flow Recovery” has lots of nifty techniques:


The 2017 work it’s based on is also pretty good and worth a read:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s