Quake
Quake es un FPS lanzado por id Software en 1996 (aquí tienen un speedrun, para los que no lo conocieron) y además de ser el primer FPS en un motor gráfico 3D, también fue un pionero en áreas como el uso de la aceleración por hardware y el soporte de partidas vía Internet. Quake cambio para siempre la historia de los videojuegos.
Pues bien, la historia de hoy gira en torno al CD-ROM de Quake Shareware. Esta es una historia de una época remota (hace 20 años), una época en la que no todos tenían ancho de banda suficiente para descargar de Internet un juego de 23 MB… una época en la que debíamos "conectarnos" a Internet, vía SLIP, usando extrañas maquinas que hacían ruiditos… una época en la que los videojuegos se vendían en grandes cajas, porque la gente no estaba dispuesta a pagar 50 dolares por algo tan pequeño como un CD… ¿se lo imaginan?
Bien, en esos tiempos remotos, los desarrolladores de id Software decidieron innovar en otro frente mas: aunque GT Interactive tenía los derechos para la distribución retail de Quake, id Software mantenía los derechos para la distribución de Quake shareware. Y la forma en la que decidieron distribuirlo (ademas de ponerlo en Internet) fue mediante un CD-ROM, que junto con la versión shareware, incluía también una copia cifrada del juego completo.
De esta forma, si alguien quería comprar el juego completo, podía llamar por teléfono directamente a id Software (no a GT Interactive), hacer una orden con su tarjeta de crédito y recibir un código de desbloqueo para empezar a jugar de inmediato. Toda una innovación :)
Ah… pero hay un detalle más, y lo veremos en esta cita del libro Masters of DOOM:
“Instead of just distributing Quake shareware over the Internet for free, id could sell a CD-ROM containing both the shareware and an encrypted version of the full game. Someone would buy the shareware for $9.95, then could call up id directly and pay $50.00 for the code to unlock the complete game.”
Si. ¡Es cierto! Este disco con shareware no lo regalaban, ¡lo vendían! Era una idea genial… Podían recibir ganancias incluso con la versión del juego que ya habían puesto de forma gratuita en Internet… El truco para poder vender el CD-ROM estaba en que este incluía también una banda sonora para el juego, compuesta por Trent Reznor, el de Nine Inch Nails… Una banda sonora bastante buena por cierto…
Por aquí tenemos una imagen del sistema de desbloqueo (noten el logo "Test Drive" en la esquina inferior izquierda, hablaremos de ello en un momento):
QUAKE Unlock (IDSTUFF/FLOW.EXE)
QCRACK.EXE
Todos podemos imaginar lo que pasó, pero aquí tenemos otra cita del mismo libro:
“Quake’s shareware retail experiment had proved disastrous. In theory id was going to cut out retailers by allowing gamers to buy the shareware and then call an 800 number to place an order and receive a password that would unlock the rest of the game. But gamers wasted no time hacking the shareware to unlock the full version of the game for free.”
La herramienta se llamaba QCRACK.EXE, y la publicó Agony del grupo GNOMON, el 10 de agosto de 1996. Esto es lo que hacía QCRACK (ignoren por ahora el hecho de que reporta el juego como "quake2", ya hablaremos de ello mas adelante):
Qcrack.exe calculando el codigo de desbloqueo
OK. Es un keygen como cualquier otro… Pero aun hay mas en esta historia: resulta que este CD-ROM incluye tambien versiones encriptadas de la librería completa de juegos FPS que hasta esa fecha había lanzado id Software :O
Los demás juegos incluidos en el CD-ROM
Entonces, ademas de Quake (y su banda sonara) tenemos estos juegos: Final Doom, Doom II: Hell on Earth, Ultimate Doom, Master Levels for Doom II, Hexen: Beyond Heretic, Hexen: Deathkings of the Dark Citadel, Heretic: Shadow of the Serpent Riders, y Wolfenstein 3D… ¿y todo por 10 dolares? :)
Antes de continuar, si alguien esta interesado en este CD-ROM, puede descargar una imagen desde archive.org :)
El sistema de protección TestDrive
En principio, QCRACK no es sólo un keygen para QUAKE (y los demas juegos en el CD-ROM), sino mas bien un keygen para el sistema de protección TestDrive, una suerte de protector comercial de esa época, así que podría funcionar en mas aplicaciones de esa década.
Por cierto, TestDrive ya no existe más, y lo que solía ser el sitio web de la empresa, ahora pertenece a una empresa diferente, pero hay varios snapshots del sitio en el archivo de Internet, que cubren el periodo entre diciembre de 1996 y junio de 1998. Por ejemplo, del snapshot de enero de 1997 sacamos lo siguiente:
“TestDrive Corporation is the leader in providing software and services to enable electronic marketing and distribution of software. Using TestDrive’s patented preview and encryption technology, our customers directly distribute software via high capacity CD-ROM. This technology allows a consumer to download and preview a complete, fully-functional product before making a purchase decision, and then automatically install it with complete sensitivity to system configuration and resources.”
Como nos hablan de un sistema patentado vale la pena hacer una búsqueda de patentes. Encontré un par de patentes a nombre de TestDrive Corporation (ambas ya expiradas):
- Transformation of ephemeral material [US 5341429 A]
- Virtualized installation of material [US 5642417 A]
La segunda es posterior a QUAKE y QCRACK y parece referirse a otro producto. Pero la primera si parece ser lo que buscamos. Aquí un fragmento:
“The preferred embodiment of the present invention converts and encrypts any desired software products using a ‘denaturing’ process that replaces a portion of the product information, such as a standard version of an application program's ‘start-up’ code, with a special portion, e.g. a special start-up code segment, that links the product to a usage counter and encrypts the application. The enabling system of the present invention converts the application back to its ‘natural’ state, for example, by replacing the modified start-up code segment with the application's original start-up code.”
Puede verificarse que así es como funcionan los archivos distribuidos en el CD-ROM de Quake Shareware. Las versiones completas de los juegos están en archivos *.MJ3 que una vez desbloqueados por FLOW.EXE (tras resolver el challenge) se convierten en archivos *.EXE del mismo tamaño.
Al compararlos se observa que solo una porción inicial difiere. A partir de cierto punto los archivos bloqueados y desbloqueados son iguales. Pero al principio hay un bloque modificado. Por ejemplo, para el caso de QUAKE, los binarios tienen un tamaño de 0x01763627 bytes, pero a partir de 0x00007FFF son iguales. En la sección modificada, el archivo MJ3 incluye mensajes como “This application is disabled”, “You cannot run this app”, o “The TESTDRIVE sampler shell”.
Otro trozo interesante es el siguiente:
“Security Strategy. The encrypted product appears as a special version developed by the publisher, although it is not a special version. Rather, a portion of the original material, e.g. the product's original start-up code, is hidden in another place. The denaturing process is a unique, check-summed operation using any of the many known encryption algorithms, such as the data encryption standard published by the U.S. government (‘DES’). Thus, to enable the product, a special enable program must be activated by a unique, check summed code number.”
OK. Eso es lo que hace QUAKE Unlock (FLOW.EXE): valida el numero de serie, descifra el stub inicial, y lo reemplaza en al versión desnaturalizada (i.e. el archivo *.MJ3) para recuperar el contenido original… Según esto, sería posible analizar FLOW.EXE para escribir directamente un unpacker. Sin preocuparnos por la parte de generar un serial para cada challenge string… Pero la idea no es desproteger los juegos de id Software sino entender a QCRACK.EXE, así que mejor vamos a analizarlo :)
Analizando QCRACK.EXE
Si intentamos correr QCRACK en DOSBox, encontramos este mensaje de error: "Load error: no DPMI". Esto significa que la aplicación debe ejecutarse en modo protegido (DPMI son las siglas en ingles para Interfaz de Modo Protegido para DOS) y por lo tanto se requiere un extensor de DOS. En este caso, por tratarse de un binario compilado con DJGPP (esto se puede verificar corriendo "strings") la solución mas simple es agregar en el mismo directorio el ejecutable CWSDPMI.EXE.
Al hacerlo, podemos correr QCRACK.EXE sin ningún parámetro y obtener un texto que incluye, entre otras cosas, el modo de uso:
C:\IDSTUFF>QCRACK.EXE ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= ■ ■ agony! pray to the one you will pay! ■ ■ usage: qcrack [-g <game>] <challenge> ■ eg, qcrack Q12345678901 ■ eg, qcrack -g quake Q12345678901 ■ ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= ■ ■ Greetz to Akoo and the Mentality guys ■ working hard to bring you the latest ■ and greatest! ■ ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= ■ ■ The author of this excellent program ■ takes no responsibility for its use ■ and makes no warranty either expressly ■ stated or implied. This program may ■ not be used unless proper licensing ■ has been previously obtained from the ■ copyright holder of the locked program.
Si probamos el primer ejemplo (qcrack Q12345678901), recibimos el siguiente mensaje de error:
C:\IDSTUFF>QCRACK.EXE Q12345678901 ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= ■ ■ agony! pray to the one you will pay! ■ warning: challenge string checksum error! ■ challenge string is probably invalid, but anyway... ■ error: could not open sku file () ■ ■ Challenge String: Q12345678901 ■ SKU File: ■ DOC File Location: ■ Game Detected: ■ Serial Number: ■ ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Si probamos el segundo ejemplo (qcrack -g quake Q12345678901), recibimos el un mensaje de error diferente:
C:\IDSTUFF>QCRACK.EXE -g quake Q12345678901 ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= ■ ■ agony! pray to the one you will pay! ■ warning: challenge string checksum error! ■ challenge string is probably invalid, but anyway... ■ error: could not locate quake.doc (not even in the .lib) ■ ■ Challenge String: Q12345678901 ■ SKU File: ■ DOC File Location: quake.doc ■ Game Detected: quake ■ Serial Number: ■ ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
La primera cosa interesante es que el keygen verifica un checksum para confirmar si el "challenge string" es valido, pero aun si no es valido, intentara calcular el serial. Ademas, por la diferencia entre las dos salidas, inferimos que el proposito de ese "SKU File" que no encuentra, es identificar el nombre del juego a partir del "challenge string" (cuando le damos un nombre como argumento, no necesita buscar el SKU). Tambien aprendemos que después de tener el nombre del juego se requiere encontrar un archivo *.doc para calcular el numero de serie.
Si buscamos un poco en el directorio IDSTUFF del CD-ROM, encontramos un archivo SKU.17 que resulta ser el que estamos necesitando. No encontramos ningún archivo QUAKE.DOC, pero teniendo en cuenta que nos dice "not even in the .lib", buscamos también archivos *.LIB y encontramos un FLOWLIB.LIB (junto con un FLOWLIB.DIR que también necesitaremos).
Asi, para ejecutar correctamente este keygen necesitamos los siguientes archivos en el mismo directorio:
- QCRACK.EXE
- CWSDPMI.EXE
- SKU.17
- FLOWLIB.LIB
- FLOWLIB.DIR
Al agregar los archivos necesarios, las salidas cambian. Esta es la nueva salida para el primer ejemplo (qcrack Q12345678901):
C:\IDSTUFF>QCRACK.EXE Q12345678901 ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= ■ ■ agony! pray to the one you will pay! ■ warning: challenge string checksum error! ■ challenge string is probably invalid, but anyway... ■ error: could not parse sku.17 game #90 ■ ■ Challenge String: Q12345678901 ■ SKU File: ■ DOC File Location: ■ Game Detected: ■ Serial Number: ■ ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
La nueva salida para el segundo ejemplo (qcrack -g quake Q12345678901):
C:\IDSTUFF>QCRACK.EXE -g quake Q12345678901 ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= ■ ■ agony! pray to the one you will pay! ■ warning: challenge string checksum error! ■ challenge string is probably invalid, but anyway... ■ ■ Challenge String: Q12345678901 ■ SKU File: ■ DOC File Location: flowlib.dir ■ Game Detected: quake ■ Serial Number: B197706865 ■ ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Con el primer ejemplo, aprendemos que el "challenge string" de alguna forma codifica un "game number" (en este caso es #90) y con ese numero sería posible determinar el nombre del juego (en este caso, no es posible porque no encuentra el juego #90 en el SKU). Por otra parte, con el segundo ejemplo confirmamos que dentro de FLOWLIB.DIR existe un QUAKE.DOC y que lo que encontremos ahi, lo necesitamos para calcular el serial.
Validación del checksum y detección del juego
Una cosa que me parece muy interesante es lo del checksum: ¿para que verificar el checksum si aun asi voy a intentar calcular el serial?, tal vez el checksum es muy facil de calcular, pero si resuta ser especialemtne complejo podría indicar que este keygen se derivo (por lo menos parcialmente) de un leak del algoritmo de protección (¿porque alguien iba a reversar esta funcion de checksum? y aun si por alguna razon la reversaba. ¿porque iba a tomarse el tiempo para incluirla en el crack para luego ignorarla?).
Pero no especulemos mas. Empecemos por entender ese checksum. Llegar al punto donde se valida el checksum es trivial, basta con seguir las referencias al mensaje de error. Entonces, encontramos que la validación se encuentra entre 0x1BB8 y 0x1C52 (ahh… por cierto… ¿mencione que este es un COFF?) y vemos también que este checksum incluye llamadas a un par de funciones desconocidas en 0x47AC y 0x7FE8:
0x00001bb8 6a04 push 4
0x00001bba 8b8568feffff mov eax, dword [ebp - 0x198]
0x00001bc0 40 inc eax
0x00001bc1 50 push eax
0x00001bc2 8d5df0 lea ebx, [ebp - 0x10]
0x00001bc5 53 push ebx
0x00001bc6 e8e12b0000 call 0x47ac ;[1]
0x00001bcb c645f400 mov byte [ebp - 0xc], 0
0x00001bcf 53 push ebx
0x00001bd0 e8132c0000 call 0x47e8 ;[2]
0x00001bd5 89c6 mov esi, eax
0x00001bd7 6a07 push 7
0x00001bd9 8b8568feffff mov eax, dword [ebp - 0x198]
0x00001bdf 83c005 add eax, 5
0x00001be2 50 push eax
0x00001be3 53 push ebx
0x00001be4 e8c32b0000 call 0x47ac ;[1]
0x00001be9 c645f700 mov byte [ebp - 9], 0
0x00001bed 53 push ebx
0x00001bee e8f52b0000 call 0x47e8 ;[2]
0x00001bf3 89c3 mov ebx, eax
0x00001bf5 89f7 mov edi, esi
0x00001bf7 83e77f and edi, 0x7f
0x00001bfa 89f1 mov ecx, esi
0x00001bfc c1e907 shr ecx, 7
0x00001bff 898d50feffff mov dword [ebp - 0x1b0], ecx
0x00001c05 89f0 mov eax, esi
0x00001c07 c1e006 shl eax, 6
0x00001c0a 29f0 sub eax, esi
0x00001c0c 31d8 xor eax, ebx
0x00001c0e b903010000 mov ecx, 0x103
0x00001c13 31d2 xor edx, edx
0x00001c15 f7f1 div ecx
0x00001c17 89954cfeffff mov dword [ebp - 0x1b4], edx
0x00001c1d b981000000 mov ecx, 0x81
0x00001c22 31d2 xor edx, edx
0x00001c24 f7f1 div ecx
0x00001c26 89d3 mov ebx, edx
0x00001c28 b907000000 mov ecx, 7
0x00001c2d 31d2 xor edx, edx
0x00001c2f f7f1 div ecx
0x00001c31 89d6 mov esi, edx
0x00001c33 8b9550feffff mov edx, dword [ebp - 0x1b0]
0x00001c39 01fa add edx, edi
0x00001c3b 01da add edx, ebx
0x00001c3d 01f2 add edx, esi
0x00001c3f 01d0 add eax, edx
0x00001c41 898550feffff mov dword [ebp - 0x1b0], eax
0x00001c47 83c420 add esp, 0x20
0x00001c4a 8b8d4cfeffff mov ecx, dword [ebp - 0x1b4]
0x00001c50 39c8 cmp eax, ecx
0x00001c52 741e je 0x1c72 ;[3]
El challenge string esta en la variable [ebp - 0x198]. Iniciamos apilando un 4. Luego, apuntamos al challenge string en eax e incrementamos en 1 (esto es, ignoramos el primer carácter). También apilamos la dirección de [ebp - 0x10] (que funcionara como buffer destino) y llamamos la función en 0x47AC. Mas adelante vemos el mismo patrón (en 0x1BD9), pero esta vez la referencia al challenge string se incrementa a 5 y la constante que apilamos es un 7… Si… ya vemos de que va esto…
Nuestro challenge string tiene 12 caracteres: una letra 'Q' y once números. Si ignoramos la letra nos quedan once números… si iniciamos en la posición 1 y extraemos 4 caracteres (la 'Q' era la posición 0) y luego iniciamos en la posición 5 y extraemos 7 caracteres lo que estamos haciendo es partir el serial en dos números… Es fácil adivinar que la otra funcion que se esta llamando (cal 0x47E8) se encarga de convertir el substring a numeros… las dos funciones misteriosas deben ser algo como substr() y str2num()…
UPDATE (2016/08/20): realmente estas dos funciones son: strncpy() y atol(). El strncpy() se descubre porque luego cada llamada a esta función se agrega manualmente el null-terminator en la posición correcta. El atol() se descubre porque es un wrapper para otra función en la que se fijan los valores de dos argumentos adicionales (0 y 10). Esta función interna es strtoul().
Desde allí, es fácil seguir las operaciones para reconstruir algo como esto:
def ValidateChecksum(challenge):
s4 = int(challenge[1: 1 + 4])
s7 = int(challenge[5: 5 + 7])
a, b = divmod((s7 ^ (0x3F * s4)), 0x103)
a, c = divmod(a, 0x81)
d = (s4 / 0x80) + (a % 7) + c + (s4 & 0x7F) + (a / 7)
return d == b
Descubrimos entonces que el primer carácter es inútil. Esto puede verificarse fácilmente: basta con cambiar el primer carácter de un challenge string valido y vemos que QCRACK calcula el serial valido sin decir nada sobre el checksum… Pero ademas notamos algo mas la variable que yo llamo "c" (y que corresponde al valor que almacenamos en ebx, en la linea 39) no esta siendo usada. Si imprimimos el valor de esta variable, veremos que para el challenge string Q12345678901 tenemos c = 90. Y Podemos verificar que poco después de este bloque del checksum, este valor se almacena en var_17C y que mucho mas adelante este valor se apila junto con un buffer que tenemos en 0xC69C y con la referencia al string "error: could not parse %s game #%d" para imprimir ese mensaje de error. Confirmamos pues que "c" es el "game number" y que en 0xC69C se almacena el nombre del archivo "sku" (en nuestro caso "SKU.17").
Como ya conocemos la forma en al que se calcula el checksum, podemos usar fuerza bruta para generar challenge strings validos para diferentes game numbers:
def GenerateChallenge(n):
for s4 in range(10000):
for s7 in range(10000000):
a, b = divmod((s7 ^ (0x3F * s4)), 0x103)
a, c = divmod(a, 0x81)
d = (s4 / 0x80) + (a % 7) + c + (s4 & 0x7F) + (a / 7)
if d == b and c == n:
return "Q%04i%07i" % (s4, s7)
for n in range(5):
print "Challenge for Game #%i: %s" % (n, GenerateChallenge(n))
Con lo que obtenemos las siguiente salida:
Challenge for Game #00: Q00000000000
Challenge for Game #01: Q00000000260
Challenge for Game #02: Q00000000520
Challenge for Game #03: Q00000000780
Challenge for Game #04: Q00000001040
Es evidente que tenemos una progresión :) Para ahorrar el costo de la fuerza bruta podemos usar algo tan simple como:
def GenerateChallenge(n):
return "Q%011i" % 260 * n
Y de esta forma, extendemos la lista tanto como queramos:
Podemos usar QCRACK para verificar que todos nuestros challenges son validos, y al mismo tiempo extraer la lista de los nombres de juegos validos en el SKU. Asi pues, Q00000000000 se identifica como "doom2", Q00000000260 se identifica como "ultdm95", y asi hasta Q00000005720 que corresponde al numero de juego #22 y se identifica como "quake9". Para #23 y sucesivos no se encuentra ningun juego en el SKU. Aqui tenemos la lista completa:
sku = [
"doom2", "ultdm95", "master", "hexen", "heretic", "wolf3d", "heretc13",
"death", "hexen11", "doom2_95", "doom_se", "Finaldos", "Final", "quake",
"quake1", "quake2", "quake3", "quake4", "quake5", "quake6", "quake7",
"quake8", "quake9" ]
Para detectar el juego solo necesitamos hacer una evaluación en esta lista usando como indice la variable "c" que calculamos en ValidateChecksum() y que ya vimos que almacenaba el "game number"…
NOTA: tenemos un bug aquí, pero es un bug que estamos heredando intencionalmente de QCRACK. Lo comentaremos y lo resolveremos al final del post.
Desde luego, aun no hemos visto como hace QCRACK.EXE para extraer esos nombres de SKU.17, que resulta ser un binario sin textos en claro. Le adelanto que realmente se trata de un archivo de texto que esta cifrado y QCRACK se encarga de descifrarlo y parsearlo solo para extraer esos nombres. Bastante avanzado este keygen, ¿no creen?. Pues no. Despues de reversar la rutina de descifrado, ¿porque incluirla en el keygen?, si solo necesitamos esa lista de nombres, la solucion mas simple es almacenar los nombres en el keygen. Esto me hace sospechar aun mas que este keygen se debe a un leak.
La verdad es que no necesitamos siquiera reversar la rutina de descifrado por dos razones:
- Si lo unico que necesitamos de SKU.17 es la lista de nombres de los juegos, veremos en un momento que esa lista puede leerse directamente (en texto plano) desde FLOWLIB.LIB ¡y esta justo al inicio del archivo!
- Si por alguna razon necesitamos algun otro valor de SKU.17 que no se encuentra en otros archivos, veremos en un momento que es posible extraer una copia de SKU.17 en texto plano.
Nuevamente, este keygen debe ser un leak. No tiene sentido descifrar el archivo SKU.17 para extraer una lista de palabras que ya estan descifradas en otro lado. Y aun si no lo estuvieran, y si fuera necesario reversar la rutina de descifrado, nadie implementaria esa rutina dentro del keygen. La opcion logica es almacenar las palabras en plano.
UPDATE (2016/08/20): aunque era innecesario para avanzar con la generación de seriales, reverse también la rutina de descifrado del SKU.17, y encontre un proceso de ofuscación muy simple (pero bonito), asi que no necesariamente implica un leak. Adicionalmente, ahora que entiendo mejor que QCRACK estaba pensado como un keygen para TestDrive y no para Quake, me resulta obvio que almacenar las palabras en texto plano reduce la aplicabilidad del keygen.
Desempaquetando los archivos *.DOC
Como vimos antes, el keygen necesita unos archivos *.DOC para calcular los seriales y logra identificarlos en FLOWLIB.DIR. Resulta que FLOWDIR.DIR / FLOWLIB.LIB. es simplemente un paquete de archivos. FLOWLIB.LIB tiene los datos y FLOWLIB.DIR los metadatos. Ademas, hay otros tres pares de archivos en el CD-ROM con ese patron *.DIR / *.LIB:
- PAGEMKR.*
- PHOTOS.*
- TAGS.*
Después de unos minutos analizando la estructura en HexWorkshop tenemos claro que la estructura de de los archivos *.DIR es muy simple: el primer DWORD nos dice el numero de archivos almacenados, y después de eso vienen 6 DWORDS por cada archivo describiendo: el nombre del archivo (3 DWORDS = 12 caracteres, 8 del nombre, 3 de la extensión, 1 del separador), el siguiente DWORD asumo que es un checksum, el siguiente es el offset dentro del archivo *.LIB donde empieza el archivo que estamos describiendo, y el siguiente es el tamaño del archivo que estamos describiendo.
Estas son las estructuras que defini en HexWorkshop:
typedef struct FLOW_FILE
{
char file_name[12] ;
unsigned int file_cheksum ;
unsigned int file_offset ;
unsigned int file_size ;
} FLOW_FILE ;
struct FLOW_FILESYSTEM
{
unsigned int number_of_files ;
FLOW_FILE files[number_of_files] ;
} ;
Y esta es la salida al procesar el archivo:
Interpretando el archivo FLOWLIB.DIR
Despues de verlo asi, note que todos los checksum tienen la forma 0x??????00. Tal vez debi usar 13 chars para el nombre (para contar el finalizador de un nombre de archivo que utilize los 8.3) y asi el checksum seria de solo 3 bytes, como un crc24 (aunque verifique y no es crc24, no vi que QCRACK verificara este checksum, tendria que reversar FLOW.EXE para salir de dudas)... En cualquier caso, este es mi codigo para la extracción:
with open(FLOWLIB_DIR, "rb") as f_dir:
number_of_entries = struct.unpack('L', f_dir.read(4))[0]
for entry in range(1, number_of_entries + 1):
name = f_dir.read(13).rstrip('\0')
checksum = struct.unpack('L', f_dir.read(3) + '\0')[0]
offset = struct.unpack('L', f_dir.read(4))[0]
size = struct.unpack('L', f_dir.read(4))[0]
print '%3i' % entry, '%12s' % name, '0x%06x' % checksum, '%7i' % offset, '%6i' % size
with open(FLOWLIB_LIB, "rb") as f_lib:
f_lib.seek(offset)
data = f_lib.read(size)
with open(name, "wb") as f_name:
f_name.write(data)
assert offset + size == os.path.getsize(FLOWLIB_LIB)
Que me genera esta salida mientras extrae los archivos:
Y asi continua… Pero saltaremos a lo mas interesante:
Resalto en cyan que encontramos los *.DOC :) y ademas en verde vemos ese "EXE.17", que es el que tiene la lista de nombres para los juegos, ordenados por su numero de juego asociado. Como es el primer archivo del paquete, esto explica porque podiamos sacar esta lista incluso antes de extraer los archivos, solo leyendo FLOWLIB.LIB. Tambien en verde vemos un "ENCRYPT.EXE" que explicare en un momento y ese "SKU.TXT" que es el que corresponde a la version en texto plano de "SKU.17" (puede verificarse ambos que tienen el mismo tamaño).
Ese "ENCRYPT.EXE" me parecio un hallazgo emocionante al principio, pero al ejecutarlo vi el siguiente mensaje con las instrucciones de uso:
C:\IDSTUFF>ENCRYPT.EXE Usage: encrypt <source> <destination>
El hecho de no solicitar una clave ya indicaba que no seria algo muy util. Aun asi, hice algunas pruebas: primero cifre un archivo que cree y obtuve una criptograma, luego cifre el criptograma con el mismo programa y recupere el mensaje, asi pues: es un reflexivo. Luego cifre un mensaje que tenia 10 ceros (no eran nulls, sino el char 0x30), el resultado fue este criptograma "789:;<=>?", al incrementar en uno cada caracter aprendemos que la clave depende de la posicion, y como el primer caracter fue un 0x37, asumimos que el estado inicial de la clave es 7. Rapidamente confirme que era una codificacion muy simple. Algo como:
def encrypt(m):
return ''.join([chr(ord(c) ^ (7 + i)) for i, c in enumerate(m)])
Nada interesante… Pero tal vez se use en alguna parte para ofuscar cadenas o algo.
Si verificamos los contenidos de los archivos *.DOC veremos que en cada caso se trata de binarios de 512 bytes. Si los agregamos a nuestro directorio de trabajo, podemos eliminar los archivos FLOWLIB.*. Veamoslo corriendo nuevamente el segundo ejemplo (qcrack -g quake Q12345678901):
C:\IDSTUFF>QCRACK.EXE -g quake Q12345678901 ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= ■ ■ agony! pray to the one you will pay! ■ warning: challenge string checksum error! ■ challenge string is probably invalid, but anyway... ■ ■ Challenge String: Q12345678901 ■ SKU File: ■ DOC File Location: quake.doc ■ Game Detected: quake ■ Serial Number: B197706865 ■ ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Como se genera el mismo serial que habiamos obtenido usando FLOWLIB.DIR, concluimos que la extracción de QUAKE.DOC se hizo correctamente :)
¿Serán estos bloques de 512 bytes parte de la información que TestDrive extrae, cifra y esconde durante el proceso de desnaturalización?
Eliminando la dependencia del CD-ROM
Como eventualmente intentaremos reimplementar completamente este keygen, podemos aprovechar para eliminar la dependencia de SKU.17, incluyendo la lista de nombres que ya conocemos en nuestra implementación. Y claro tambien podemos incluir una lista adicional con los contenidos de los archivos *.DOC. Sin embargo, tenemos 23 archivos de 512 bytes cada uno. No me animo a incluir 12.5KB de datos… Convenientemente, podemos optimizar un poco lo que vamos a incluir si vemos que aunque la plataforma que controla el desbloqueo se implemento para soportar 23 juegos (y segun el valor "c" en ValidateChecksum, puede soportar hasta 128 productos), en el CD no hay realmente 23 juegos diferentes, solo son 9.
Decidí hacer algunas pruebas dejando que FLOW.EXE me creara algunos challenges para cada juego y verificando el nombre del juego que identificaba QCRACK. Encontré que en lugar de usar la lista de 23 juegos puedo usar un diccionario de 9:
sku = {
0: "doom2", 2: "master", 5: "wolf3d",
6: "heretc13", 7: "death", 8: "hexen11",
10: "doom_se", 11: "Finaldos", 15: "quake2"
}
Agregando un segundo diccionario con los contenidos de los archivos *.DOC para esas mismas 9 claves, eliminamos completamente la dependencia del CD-ROM :)
Solucionando el bug en QCRACK.EXE
Cuando estaba investigando sobre QCRACK, antes de encontrar una copia del ejecutable encontré una FAQ en un viejo mensaje de USENET… Entre las preguntas tenemos esta:
Resulta que el formato "QCRACK.exe Q###########" funciona para todos los juegos menos para Final Doom. Para poder desbloquear Final Doom es necesario decirle de forma explicita al keygen que juego se va a desbloquear.
Haciendo algunas pruebas con mi generador de challenges vi que QCRACK detectaba correctamente el juego como "Finaldos". Asi que algo mas estaba pasando:
C:\IDSTUFF>QCRACK.EXE Q00000002860 ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= ■ ■ agony! pray to the one you will pay! ■ ■ Challenge String: Q00000002860 ■ SKU File: sku.17 ■ DOC File Location: Finaldos.doc ■ Game Detected: Finaldos ■ Serial Number: B198529686 ■ ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Si le damos la opcion "-g finaldos" el serial cambia:
C:\IDSTUFF>QCRACK.EXE -g finaldos Q00000002860 ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= ■ ■ agony! pray to the one you will pay! ■ ■ Challenge String: Q00000002860 ■ SKU File: sku.17 ■ DOC File Location: Finaldos.doc ■ Game Detected: Finaldos ■ Serial Number: B198544010 ■ ■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Si se verifica con un challenge generado por FLOW.EXE se confirma que, efectivamente, el serial generado cuando el juego es auto-detectado resulta ser incorrecto. Pero el serial generado cuando le decimos de forma explícita el nombre del juego si es correcto.
No se si lo notaron pero la diferencia esta en que esta detectando el juego como "Finaldos" en lugar de "finaldos". Al parecer la mayúscula inicial afecta el resultado. Podemos confirmarlo cuando le decimos de forma explícita "-g Finaldos" con lo que obtenemos el mismo serial erróneo inicial. También cambia si le decimos algo como "-g fiNalDoS". Así que la generación del serial utiliza el nombre del juego y es sensible a las mayúsculas.
Curiosamente, el bug se origina en SKU.TXT, que es la versión en plano del SKU.17 cifrado. Al analizar el texto plano vemos que tanto "final" como "finaldos" aparecen con mayúscula inicial. Pero en EXE.17 aparecen en minúsculas. Así pues, si dejamos de usar SKU.17 y usamos las definiciones de EXE.17 no tendremos este problema:
sku = {
0: "doom2", 2: "master", 5: "wolf3d",
6: "heretc13", 7: "death", 8: "hexen11",
10: "doom_se", 11: "finaldos", 15: "quake2"
}
Comentarios finales
Esta entrada ya esta muy larga y como tengo trabajo pendiente no quiero dejarla como borrador de forma indefinida… Ademas, cada vez me convenzo mas de que aquí tuvo que haber un leak. Eso hace que el proceso sea menos emocionante. Creo que lo dejaremos en "Continuará… (Tal vez)".
UPDATE (2016/08/20): ya no estoy tan seguro de que sea un leak, asi que esto se pone emocionante de nuevo. Y ya no es un "tal vez", ya sabemos que hay una continuacion en: Qcrack y la belleza de lo innecesario.
Pero antes de cerrar, quiero mencionar que encontre que ya existe un proyecto de Eric Faehnrich para reversar Qcrack. Aunque no hay mucho allí todavía, vale la pena tenerlo en cuenta…




Comentarios
Publicar un comentario