Please Destroy My Face: Reverse Engineering Scorched Earth’s MTN File Format

And you thought I was joking.

Humble Beginnings

Once upon a time, in my youth, I was enamored with a game called Scorched Earth.

In addition to more standard rocky mountains, glaciers were also included with the game. Cool!

Some Time Later

I rediscovered the game entirely by chance, by seeing a screenshot of it while working on an unrelated project. At this point, I had learned a thing or two about computers, and thought that maybe it was time to make this dream into a reality.

That’s all folks, thanks for reading!

Methodology

In a perfect world, I would have started with disassembly. However this is complicated by the fact that this is a quite an old DOS program, and it’s not possible to simply pop it into IDA. There are some DOS plugins and extensions for IDA, but I could not get some of them running, and those I could didn’t seem to be able to handle the programs I was working with.

Construct’s Binary Schema Structure

Construct is a python library for doing binary parsing of files. It makes use of python operators in some fairly unique ways, so I’ll give you a crash course on the syntax here, so that future schemas are a bit more comprehensible. If you’re interested in a more in-depth guide, the full documentation can be found here.

"<label>" / <datatype> * "<optional explanation>"
  1. Size in bits (8, 16, 32)
  2. Signed or unsigned
  3. Big or little Endian storage format

Digging in: Information Gathering

Quite literally
  1. It was written using Borland C++ and Turbo Assembler.
  2. Scorched earth was developed in the early/mid 90s, while the author was studying at Caltech. It was not a commercial program, and was released as a nearly full-featured shareware product.
  3. File format likely resembles bitmap (as previously discerned)
  4. File sizes for mountain files are significantly smaller than the original files
  1. As with bitmap files, I expect integers will make up most of the header. Early versions of Borland C++ and Turbo assembler use 16 bits for ints, rather than the traditional 32. Therefore, my default datatype assumption will be a 16 bit unsigned int.
  2. Some sort of compression technique is likely at play, as the MTN files are smaller than the source images.
  3. There is little-to-no intentional obfuscation of the file format. Scorched Earth was not done with explicit commercial intent, it was developed because it was fun and interesting for the author. Beyond basic shareware restrictions, there was no DRM/copy protection, so there’s no reason to believe the executable or MTN file format is obfuscated.

Bitmapping out a plan

The first thing I did was build a toolkit to generate 4 bit/16 color bitmap images.

Whoops
Now we’re in business!

File Analysis: Signature Detection and Palette Analysis

Finding the file signature is a good way to start reverse engineering. Often, it tells you something about the design of the parser. Here, finding it is easy — Every mountain file begins with MT 0xBEEF 256 . This is very similar to how bitmap files start with BM , which implies my previous assumption about the parser being similar to bitmap files is looking more likely to be correct.

0: (0, 0, 0)
1: (255, 255, 255)
0 1 0 1 0 1 0 1 1 1 1 1 1

Header Analysis: Part One

The header is the next bit of low-hanging fruit. It’s only 18 bytes long, and I expect most of the critical fields will be lifted directly from the bitmap header or derived from the MTN file itself.

Pixel Data: Round One

To start, I get naive — as naive as possible.

Don’t do drugs, kids.

Intermission: Building a Mountain

To figure this out, we’re going to have to understand a bit about how terrain works in Scorched Earth.

Do you see it?
Do you see it now?

Pixel Data: Round Two, in Which I Stare into the Nibbles until they stare back at me

We will start by looking at the pixel data for a simple example image.

Glorious
0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0
0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
0, 9, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0,
0, 9, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
0, 9, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
0, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
0, 9, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 9, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0
[0, 0, 0, 0, 0, 0, 0, 0], 
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0]
Yes I’m reusing this, do I look like I’m made of GIFs? Don’t judge me.
"pixels" / Array(
this.width,
FocusedSeq(
"items",
"count" / Rebuild(Int8ul, len_(this.items)),
"padding" / Padding(1)
"items" / Bitwise(
Aligned(
8,
Array(
this.count,
Nibble
)
)
),
)
)

BMP Extraction

  1. Mirroring, less clearly.
Picturesque!
Oops.
All better!

Header Analysis: Round Two — Giving Up

Now we reach the limitations of this sort of analysis.

Addendum: Asking the Author

After I completed the analysis, I sent an email to the original author, Wendell Hicken, for him to confirm my assumptions about the remaining unknowns.

Tying it all together: Time to Destroy my face

Once I had a successful routine to extract BMPs from MTNs, it was a simple matter to take a BMP, reverse the transformations, and begin generating MTN files.

Conclusion

In the end, there’s not much more to say. The goal was terrifically dumb, but the investigation and resulting tool were quite interesting.