See my continued work on cracking qcrack here.

QCRACK is a cracking program for an id Software CD that came with several games including Quake. You install the games from the CD by generating a key then call the phone number and give them that key along with payment for the game and they’ll give you the matching key that you can use to install the game.

The cracking program generates the install key for you. I’m reverse engineering this because I’ve always been interested in how it works, and how Windows executables in general work. I am not reversing this so people can install the game for free. Quake is just $10 on Steam. That’s the price of two good microbrews at a pub. You’d buy the makers of these games a couple of beers, wouldn’t you? And if you’re willing to play these older games, it’s probably for nostalgia purposes which means you’re old enough to afford it.

QCRACK is a 16-bit DOS program. I was not able to get this to run on any modern Windows machine. You’ll have to run this with something like DOSBox. But that also means you can run it on a non-Windows machine. I installed DOSBox through the Ubuntu software center.

Then you’ll need to mount a directory on your real machine to a drive in DOSBox. That’s as simple as

Z:\>MOUNT D C:\qcrack

This example is on a Windows machine. Let’s say I have a directory at C:\qcrack with the qcrack.exe file. Running that command in DOSBox, switching to the D drive, then I can run the program. What I did in Ubuntu was

Z:\>MOUNT D ~/Documents/qcrack

It understands the directory structure of the host machine you’re on.

If you run qcrack at this point, you’ll be met with the error

Load error: no DPMI 

I’m not exactly sure what that means, but I think I found it to mean it needs some files since qcrack was built with DJGPP. The files it needs are CWSDPMI.EXE, CWSDPRO.EXE, CWDSTUB.EXE, and CWSPARAM.EXE. You can find these files online. Place them in the same directory as the qcrack.exe executable file. You can then run the program in DOSBox without error.

Now that we can run it, let’s reverse it. Like the difficulty with running this archaic string of bits, I wasn’t able to load this into OllyDbg or x64_dbg. Since I’m new to this, you’ll see I’m left with doing this by hand, which I guess means you can refer to this as artisinal.

All Windows programs start with a DOS header. Like our reptilian vestiges, most modern Windows programs have a mini DOS program in them. If run by DOS, does nothing but reminds you DOS is obsolete by printing

This program cannot be run in DOS mode.

QCRACK still being a remnant, is not the newer PE file format but does have a DOS header that does more than print a sad message.

The DOS header has the following structure. As the C standard defines things such as int or long as at least two or four bytes, respectively, those sizes are different in the 16-bit context of most references and the current architecture. So I’m describing them in current 64-bit terms. I note this because references were saying some values were long meaning four bytes, but when I wrote code with type long it was using long as eight bytes. I changed to int when needed then it worked. Regardless, I’ll note their actual size in the code.

struct exe {
  char m; /*M and Z are one byte*/
  char z;
  unsigned short bytes_in_last_block;  /*short is two bytes*/
  unsigned short blocks_in_file;
  unsigned short num_relocs;
  unsigned short header_paragraphs;
  unsigned short min_extra_paragraphs;
  unsigned short max_extra_paragraphs;
  unsigned short ss;
  unsigned short sp;
  unsigned short checksum;
  unsigned short ip;
  unsigned short cs;
  unsigned short reloc_table_offset;
  unsigned short overlay_number;

Here is the qcrack executable in a hex editor.

The first two bytes are “MZ”. Those serve as a kind of signature for the type of file. They’re the initials of Mark Zbikowski, designer of this header and file format.

Next, we have to consider that these executables were thought of as being in “pages” or “blocks” that were 512 bytes in size. So each block is 512 bytes, except the last one that could be any size from 1 byte to 512. The next two bytes in the DOS header gives the number of bytes of that last block. If the last block is the full 512 bytes then the value here is zero. One thing I’m curious about is what if the last block is 256 bytes or more up to 511 since there are only two bytes to describe it so can only have values 0-255.

Also keep in mind that this is little-endian, so when you see 0x04 followed 0x00, if those two bytes are part of the same short then that value is actually 0x0004, not 0x0400.

After that we have the number of blocks needed for I think the header and load module that comes after it. So it was 1024 bytes, there would be two blocks needed and this value would be 0x2 (and bytes in last block would be 0x0.) If this was 1025 bytes, it won’t fit in two blocks, so it need three and this value would be 0x3 (and bytes in last block would be 0x1.) In our example of qcrack above, it says it has 4 blocks, and 4*512 is less than the size of the whole file, we’ll come to the extra data after the header and load module later.

Next is the number of entries in the relocation pointer table. No idea what this is. Only seen this as 0x0, so not a concern right now I guess.

The term paragraph here refers to 16 (0x10) bytes. The next two bytes is the number of paragraphs for this header. Note that the header could be bigger in size than the above struct. As you can see in the hex dump of qcrack, the header is 0x20 paragraphs or 0x10*0x20=0x200 (512) bytes. That’s much larger than the 28-byte struct of the header. You can see they filled up those extra bytes with ASCII text describing some compilation info.

Next is the minimum number of paragraphs needed to load program to begin execution. After that is the maximum needed, this seems to usually the max value possible of 0xFFFF.

Initial address of stack segment relative to load segment. But this is zero in qcrack. Next is initial stack pointer value. All more stuff I don’t know what it’s for yet.

I mentioned the load module. That is what immediately follows this DOS header. I think this is the actual executable code that gets run. The end of the header is it’s size, so the load module or executable starts at 0x10*0x20=0x200 in the file.

Then checksum that seems to be ignored and usually just set to 0x0.

Then we have the initial instruction pointer value. This is the offset from the start of the load module. So to find where we actually begin execution in the file, add this value to the start of the load module in the file, so 0x200+0x54 in our case for 0x254.

Then is the CS register value, then reloaction table offset but I don’t think we have to worry since we also have table size zero. Finally overlay number which I don’t know about.

The file could be bigger than the size according to the number of blocks. After that, we have more data in the file. So the DOS header is 0x200 bytes, the load module with code starts right after that at 0x200 but the IP is 0x54 so the code really starts at 0x254.

We’ll revisit the code. For now let’s look at that extra data after the load module. Since there’s 4 blocks that are 512 bytes, the extra data after the header and load module starts at 4*512=2048=0x800. That also means the load module is 0x800-0x200=0x600 bytes large.

After the load module is the start of the extra data that is in the format of a COFF header. Again, the references had long where I now put int for correct size on current machines. Just remember in my examples short is two bytes, int is four.

struct coff_head {
  unsigned short f_magic;  /* magic number    0x14c    */
  unsigned short f_nscns;  /* number of sections       */
  unsigned int   f_timdat; /* time & date stamp        */
  unsigned int   f_symptr; /* file pointer to symtab   */
  unsigned int   f_nsyms;  /* number of symtab entries */
  unsigned short f_opthdr; /* sizeof(optional hdr)     */
  unsigned short f_flags;  /* flags                    */

The magic number is a value indicating the processor this was compiled for. You can see our magic number 0x14c. Then it tells us we have three “sections”.

Not sure about the rest of the values. Next we have an “optional” header.

struct aouthdr {
  unsigned short magic;         /* type of file                         */
  unsigned short vstamp;        /* version stamp                        */
  unsigned int  tsize;          /* text size in bytes, padded to FW bdry*/
  unsigned int  dsize;          /* initialized data    "  "             */
  unsigned int  bsize;          /* uninitialized data  "  "             */
  unsigned int  entry;          /* entry pt.                            */
  unsigned int  text_start;     /* base of text used for this file      */
  unsigned int  data_start;     /* base of data used for this file      */

This has a magic number 0x10b, and gives us size and entry of our sections. The sections are the text section, data, and bss section. I’m not sure how to read this info yet.

After the optional header is the section headers for the sections the optional header mentioned.

struct scnhdr{
  char           s_name[8];  /* section name                     */
  unsigned int   s_paddr;    /* physical address, aliased s_nlib */
  unsigned int   s_vaddr;    /* virtual address                  */
  unsigned int   s_size;     /* section size                     */
  unsigned int   s_scnptr;   /* file ptr to raw data for section */
  unsigned int   s_relptr;   /* file ptr to relocation           */
  unsigned int   s_lnnoptr;  /* file ptr to line numbers         */
  unsigned short s_nreloc;   /* number of relocation entries     */
  unsigned short s_nlnno;    /* number of line number entries    */
  unsigned int   s_flags;    /* flags                            */

Each section header has eight characters for the name. Note this is NOT null terminated, so watch if passing this string around. If the name takes up all eight characters, passing just the address of the name like to print will probably print more than just the name. This struct has more info that I don’t know about yet either. I have no idea what is in the sections either. Like, the text section is supposed to have the code, but we have code before this too?

So to the code. From the MZ header at the start, it mentioned a load module at 0x200 and IP offset 0x54 pointing in that. So at 0x254 in the file we have code we’ll execute.

We can disassemble with nasm. With windows, I used their installer and it installed in C:\Users\eric.faehnrich\AppData\Local\nasm. cd into that directory and can run their executables.

ndisasm.exe -b 16 -e 596 f:\QCRACK.EXE > qcrack.txt

We run the disassembler for 16 bit, skip to byte 596 (0x254) of our executable and spit the disassembly into a text file. This code seems about right because the instructions begin almost just like the DOS stub of PE files.

Note in the nasm disassembly output it starts the code offset address at zero. Look at the instruction at address 0x12.

00000012  7306              jnc 0x1a

It’s a jump. The raw machine code says jump ahead 6 bytes while the mneumonic says to jump to the instruction at 0x1a. We jump the number of bytes after the current instruction. Since this current instruction is two bytes, 0x12+0x2+0x6 = 0x1a. Nasm is nice and prints the calculated value in the mneumonic.

Well, I finally how to debug the running program. There is supposedly a program called DEBUG for DOS that was standard but DOSBox doesn’t come with it, or you have to compile on your own to get it or something. I couldn’t figure it out. But I found someone has a standalone DEBUG. Put that in another mounted directory so DOSBox can get to it.

Further reading and resources