Continuing QCRACK

picking up on reversing the Quake cracking program


related posts:

I’m picking up reversing QCRACK again, the Quake cracking program. This post will be on how I started the reversing using the tool Radare, and reverse the first part of the executable which is calculating the checksum of the challenge string.

My previous work on it was mostly just an intro to executables. I’ve left it behind because I hit a roadblock with reversing it since it’s a 16-bit DOS executable, not a normal (nowadays) 32/64-bit Windows PE executable.

But then I recieved an email from Ruben Molina about his work on reversing QCRACK (Google Translate English translation here). He has done much more research, and has gotten farther. In our exchanges I learned he was able to reverse it with the tool Radare, a tool I’ve been meaning to learn for some time. My original goal wasn’t necessarily to reverse QCRACK but learn some reversing, however with this new knowledge of an effective tool I am continuing my efforts. Read his posts for what I’m sure is more detail, because at this point I’m effectively playing catch up.

As before, I use DOSBox to run the executable. With QCRACK and the DJGPP DPMI files together in the same directory (that you mount to a drive in DOSBox) you can run the program. Just running it gives the usage.

Trying the first example input

qcrack -g quake Q12345678901

tells you it couldn’t open an sku file. This sku file is on the shareware CD that this program is trying to crack. You can find a copy of this shareware CD at the Archive.

That rip of the CD comes in MDF format. Can convert to ISO with mdf2iso on Linux. Get it with:

sudo aptitude install mdf2iso

Then convert to ISO with:

mdf2iso dsc_image.mdf disc_image.iso

To mount the ISO to access the files (may need to put sudo in front of these commands.)

mkdir /mnt/iso
mount -t iso9660 -o loop /home/faehnrich/mydisc.iso /mnt/iso/
cd /mnt/iso
ls -la

May not need the -t iso9660? When done, to unmount:

umount /mnt/iso

Copying the sku file to the directory with QCRACK and running the second example

qcrack -g quake Q12345678901

it now complains about quake.doc not being there.

This sku file may be able to tell you the game based on the sku file, where if you just tell it the game it doesn’t need the sku and can get right to looking at the games .doc file, which we don’t know about yet.

The error also mentions it couldnt’ find it in the .lib file, so copying the flow.lib, and guessing it needs the flow.dir (?) to find the .doc in the .lib so copying that too.

Running qcrack with those files together with

qcrack Q12345678901

We get

error: could not parse sku.17 game #90

Challenge String: Q12345678901
SKU File:         sku.17

With this

qcrack -g quake Q12345678901

We get a checksum error, but get a serial number

Challenge String:  Q12345678901
DOC File Location: flow.dir
Game Detected:     quake
Serial Number:     B197706865

Let’s try looking at it in the reversing tool Radare.

radare2 -m 0x800 QCRACK.EXE

-m 0x800 for the mapping address. Look for that string it prints for the usage when you just run qcrack

[0x00000000]> /i agony
Searching 5 bytes from 0x00000000 to 0x0000ca00: 61 67 6f 6e 79
Searching 5 bytes in [0x0-0xca00]
hits: 1
0x00001d14 hit3_0 .=-=-=-= agony! pray to the on.

Set the address with

> s 0x1d14

It’s not the start of a string. You can view file with v and cycle through views with p until you get to a hex viewer. You may need to press p a few times to get to a hex viewer viewing individual bytes instead of words. You can also press left and right to have lines start with an address ending in 0 which I find easier to read.

0x00001d04  3d2d 3d2d 3d2d 3d0d 0a00 00fe 0d0a fe20  =-=-=-=........
0x00001d14  6167 6f6e 7921 2070 7261 7920 746f 2074  agony! pray to t
0x00001d24  6865 206f 6e65 2079 6f75 2077 696c 6c20  he one you will
0x00001d34  7061 7921 0d0a 002d 6700 fe0d 0a00 fe20  pay!...-g......

Looking back from “agony!”, we see the first address after the terminating zeroes for the previous string’s nul byte is at 0x1d0f (it’ll be an ASCII character that’s not really printable and just shows up as a block usually.)

Search for that in the code.

> /c 1d0f
0x000021e6 # 5: push 0x1d0f

So some code references that string at 0x21e6. Set the address to that. (Note the previous search was just 1d0f because it’s doing text search, here you need the 0x before the address for it to understand it’s hex.)

> s 0x21e6

View with v again and change views with p until you’re looking at disassembly.

Go a few lines above that and you’ll see

push 0xf
call 0x3d3c
push 0x1ce4
call 0x4598
push 0x1d0f
call 0x4598

This is call to set color function to white (0xf = 15), print the banner then the “agony!” line.

Let’s say we have a our challenge string, just use the one they gave as an example. When entering it we get

C:\>qcrack -g quake Q12345678901

warning: challenge string checksum error!
challenge string is probably invalid, but anyway...

So there is a checksum on the challenge strings, let’s see about that first. Let’s search for the warning.

> /i warning

Get our hit at 0x1f51, but minus two bytes for those first two characters of a block and a space. So now we search the assembly for 0x1f4f.

> /c 1f4f
0x245b push 0x1f4f
> s 0x245b
> v

then p to get to the disassembly.

Above that I’m guessing is the checksum code.

Going up enough, we come to some no-ops and guessing that’s the start of our checksum code

0x00002390      30c0           xor al, al
0x00002392      8bbd68feffff   mov edi, dword [rbp - 0x198]
0x00002398      fc             cld
0x00002399      b9ffffffff     mov ecx, 0xffffffff         ; -1 ; -1
0x0000239e      f2ae           repne scasb al, byte [rdi]
0x000023a0      83f9f2         cmp ecx, -0xe
0x000023a3      7413           je 0x23b8
0x000023a5      6a0c           push 0xc                    ; 12
0x000023a7      e890190000     call 0x3d3c
0x000023ac      682d1f0000     push 0x1f2d
0x000023b1      e927080000     jmp 0x2bdd

Zero out al, our challenge string is at rbp - 0x198, copy that location to edi. Move highest possible value to ecx (which also equals -1). This is all setup for repne scasb al, byte [rdi] to count the length of the challenge string, scanning until the character equals al (0) or ecx decrements to 0. Then compare ecx to -0xe=-14, it’s looking to compare the string length to 12 but ecx starting decrementing at -1 and it would count the null byte as well. If the string is 12 characters long, skip over to the next code. Otherwise, print the string “challenge string wrong length” that’s at 0x1f2d.

0x000023b8      6a04           push 4                      ; 4
0x000023ba      8b8568feffff   mov eax, dword [rbp - 0x198]
0x000023c0      40             inc eax
0x000023c1      50             push eax
0x000023c2      8d5df0         lea ebx, [rbp - 0x10]
0x000023c5      53             push rbx
0x000023c6      e8e12b0000     call 0x4fac
0x000023cb      c645f400       mov byte [rbp - 0xc], 0
0x000023cf      53             push rbx
0x000023d0      e8132c0000     call 0x4fe8
0x000023d5      89c6           mov esi, eax
0x000023d7      6a07           push 7                      ; 7
0x000023d9      8b8568feffff   mov eax, dword [rbp - 0x198]
0x000023df      83c005         add eax, 5
0x000023e2      50             push rax
0x000023e3      53             push rbx
0x000023e4      e8c32b0000     call 0x4fac
0x000023e9      c645f700       mov byte [rbp - 9], 0
0x000023ed      53             push rbx
0x000023ee      e8f52b0000     call 0x4fe8

push 4, the challenge string, and the address [rbp -0x10] onto the stack, call 0x4fac.

0x00004fac      55             push rbp
0x00004fad      89e5           mov ebp, esp
0x00004faf      56             push rsi
0x00004fb0      53             push rbx
0x00004fb1      8b7508         mov esi, dword [rbp + 8]    ; [0x8:4]=-1 ; 8
0x00004fb4      8b5510         mov edx, dword [rbp + 0x10] ; [0x10:4]=-1 ; 16
0x00004fb7      85d2           test edx, edx
0x00004fb9      7424           je 0x4fdf                   ;[1]
0x00004fbb      89f1           mov ecx, esi
0x00004fbd      8b5d0c         mov ebx, dword [rbp + 0xc]  ; [0xc:4]=-1 ; 12
0x00004fc0      8a03           mov al, byte [rbx]
0x00004fc2      8801           mov byte [rcx], al
0x00004fc4      434184c0       test al, r8b
0x00004fc8      7512           jne 0x4fdc                  ;[2]
0x00004fca      4a7412         je 0x4fdf                   ;[1]
0x00004fcd      90             nop
0x00004fce      90             nop
0x00004fcf      90             nop
0x00004fd0      c60100         mov byte [rcx], 0
0x00004fd3      414a75f9       jne 0x4fd0                  ;[3]
0x00004fd7      eb06           jmp 0x4fdf                  ;[1]
0x00004fd9      90             nop
0x00004fda      90             nop
0x00004fdb      90             nop
0x00004fdc      4a75e1         jne 0x4fc0                  ;[4]
0x00004fdf      89f0           mov eax, esi
0x00004fe1      8d65f8         lea esp, [rbp - 8]          ;[5]
0x00004fe4      5b             pop rbx
0x00004fe5      5e             pop rsi
0x00004fe6      c9             leave
0x00004fe7      c3             ret
0x00004fe8      55             push rbp
0x00004fe9      89e5           mov ebp, esp
0x00004feb      6a0a           push 0xa                    ; 10
0x00004fed      6a00           push 0
0x00004fef      ff7508         push qword [rbp + 8]
0x00004ff2      e835320000     call 0x822c                 ;[6]
0x00004ff7      c9             leave
0x00004ff8      c3             ret

next call is this, passing through the params to another function along with 10 and 0?

0x00004fe8      55             push rbp
0x00004fe9      89e5           mov ebp, esp
0x00004feb      6a0a           push 0xa                    ; 10
0x00004fed      6a00           push 0
0x00004fef      ff7508         push qword [rbp + 8]
0x00004ff2      e835320000     call 0x822c                 ;[1]
0x00004ff7      c9             leave
0x00004ff8      c3             ret

Then load 0 into byte [rbp - 0xc]. So passed 4, and [rbp-0x10], then loading 0 into [rbp-0xc], where 0x10-0xc=4. It’s almost like we’re putting a null terminator there. Similar for next call, but 7. I’m guessing this is strncpy which copies the first 4 number characters into a string, then the last 7 number characters into another string (ignoring that first Q character, meaning you can type whatever you want there.)

That other function these new strings are passed to gets that string, also passes in 10 and 0. This is likely the strtol or strtoul functions; they get a string, end pointer (null in our case saying don’t use), and the base (10 in our case.) I’ll use strtoul for unsigned.

The rest of the code

0x000023f3      89c3           mov ebx, eax
0x000023f5      89f7           mov edi, esi
0x000023f7      83e77f         and edi, 0x7f
0x000023fa      89f1           mov ecx, esi
0x000023fc      c1e907         shr ecx, 7
0x000023ff      898d50feffff   mov dword [rbp - 0x1b0], ecx
0x00002405      89f0           mov eax, esi
0x00002407      c1e006         shl eax, 6
0x0000240a      29f0           sub eax, esi
0x0000240c      31d8           xor eax, ebx
0x0000240e      b903010000     mov ecx, 0x103              ; 259
0x00002413      31d2           xor edx, edx
0x00002415      f7f1           div ecx
0x00002417      89954cfeffff   mov dword [rbp - 0x1b4], edx
0x0000241d      b981000000     mov ecx, 0x81               ; rflags
0x00002422      31d2           xor edx, edx
0x00002424      f7f1           div ecx
0x00002426      89d3           mov ebx, edx
0x00002428      b907000000     mov ecx, 7
0x0000242d      31d2           xor edx, edx
0x0000242f      f7f1           div ecx
0x00002431      89d6           mov esi, edx
0x00002433      8b9550feffff   mov edx, dword [rbp - 0x1b0]
0x00002439      01fa           add edx, edi
0x0000243b      01da           add edx, ebx
0x0000243d      01f2           add edx, esi
0x0000243f      01d0           add eax, edx
0x00002441      898550feffff   mov dword [rbp - 0x1b0], eax
0x00002447      83c420         add esp, 0x20
0x0000244a      8b8d4cfeffff   mov ecx, dword [rbp - 0x1b4]
0x00002450      39c8           cmp eax, ecx
0x00002452      741e           je 0x2472

is to perform the checksum like this function I wrote in C:

#include <string.h>
#include <stdlib.h>

/* returns zero if checksum isn't valid,
   positive non-zero if it is valid,
   negative non-zero if error (length not 12, challenge string not numbers)
int qcrack_checksum(char *challenge) {
    char str1[5];
    char str2[8];
    unsigned long a, x, y, z, breg, sreg;

    if(strlen(challenge) != 12) {
        return -1;

    strncpy(str1, challenge+1, 4);
    str1[4] = '\0';

    strncpy(str2, challenge+5, 7);
    str2[7] = '\0';

    sreg = strtoul(str1, NULL, 10);
    breg = strtoul(str2, NULL, 10);

    x = (sreg*0x3f)^breg;
    y = x/0x103;
    z = y/0x81;
    a = (z/7) + (sreg>>7) + (sreg & 0x7F) + (y%0x81) + (z%7);

    return a == x%0x103;

The program tells you the sku file if you don’t specify a game, and if you put in an invalid checksum it says it can’t find some game number. I’m going to guess it from the challenge string it can calculate a game number, and uses that to look something up in the sku.17 file. When you specify a game, it also says where the DOC file is, in flowlib.dir.

From here I’m going to continue reversing, looking to see how it calculates the game number and see if I can tell what it looks for in those files.