Crinkler packing

March 17, 2023

Demoscene programs are sometimes focused on small file size. A dedicated wizard can fit a surprising amount into not many bytes! Here's a video from a random recent demo that somehow is only 4 kilobytes, including all the code, graphics and music. (I found it on pouet.net which has a lot more like this.) In this post I investigate the pretty amazing hacks taken to shrink a file by a few more bytes.

In retrowin32, my windows emulator, I have been looking at demoscene executables. In retrospect this may have been a mistake because they often use lots of weird tricks, though on the positive side they also are fairly small executables that don't use much Windows API. One program I was looking at had some funny file headers that I spent some time admiring and which I would like to now share with you.

Windows executables use the "portable executable" (PE) format. In a normal executable, as pictured as a stack of colored boxes on that Wikipedia page, this means the file starts with a DOS header, which is followed by a PE header, which is then followed by code and data of the program. But what if all of that together is too many bytes?

Here's a picture of the beginning bytes of one tiny program I was looking at. The blue box outlines the DOS header, and the values marked in red squares are explained on the right.

The other bytes in that header all have meanings when interpreted by DOS, but as far as Windows is concerned it just wants to skip over this bit. (Fun trivia: the "MZ" bit it opens with are the initials of the programmer who made the format.)

But you'll notice that the file offset of the PE header is a very small number: it points back into the same bytes! Here are the same bytes pictured again, but this time with a blue box around the part that is interpreted as the PE header.

Again, all these bytes have meaning in the PE format. I highlighted some of the places where the particular value is required for the file to load. I found this page tried to analyze which fields exactly are required; it turns out other fields can have garbage in them and things will more or less work.

In the above picture, note that the section load address causes the file's contents to be loaded at memory offset 0x10000. Once the file is loaded, execution starts at the entry point address, 0x10002... which is to say, execution starts at second byte of this data!

Here's a third picture of the same bytes, this time interpreting x86 instructions starting at offset 2.

They sneak in a bit of code and then a forward jump, which has a bit more code and then another forward jump. The jumps are presumably to skip over bytes which had important values due to the other interpretation this thing is snaking around.

It turns out this particular program was packed with a tool called crinkler. The x86 code that is threaded in there is a decompressor that unpacks the rest of the program. They were kind enough to share some commented assembly of how they generated these headers, though note that I think the code I linked there is from a different version of crinkler than the executable pictured here.