Continuing QCRACK
picking up on reversing the Quake cracking program
2016-12-28
related posts:
- the original QCRACK post where I try to learn about the original DOS executable
- QCracker - a JavaScript implementation of QCRACK on a webpage
- flowlib in QCRACK
- The Super Advanced QCRACK ENCRYPT.EXE
- QCRACK SKU Encryption
- Finished with QCRACK - my last post on it
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.