Quake, TestDrive y Qcrack

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):
  1. Transformation of ephemeral material [US 5341429 A]
  2. 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:  Q12345678901SKU 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:  Q12345678901SKU File:DOC File Location: quake.docGame Detected:     quakeSerial 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:  Q12345678901SKU 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:  Q12345678901SKU File:DOC File Location: flowlib.dirGame Detected:     quakeSerial 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:
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
Challenge for Game #05: Q00000001300
Challenge for Game #06: Q00000001560
Challenge for Game #07: Q00000001820
Challenge for Game #08: Q00000002080
Challenge for Game #09: Q00000002340
Challenge for Game #10: Q00000002600
Challenge for Game #11: Q00000002860
Challenge for Game #12: Q00000003120
Challenge for Game #13: Q00000003380
Challenge for Game #14: Q00000003640
Challenge for Game #15: Q00000003900
Challenge for Game #16: Q00000004160
Challenge for Game #17: Q00000004420
Challenge for Game #18: Q00000004680
Challenge for Game #19: Q00000004940
Challenge for Game #20: Q00000005200
Challenge for Game #21: Q00000005460
Challenge for Game #22: Q00000005720
Challenge for Game #23: Q00000005980
Challenge for Game #24: Q00000006240
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:
  1. 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!
  2. 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:
  1       EXE.17 0x74ed0b       0    243
  2  PRODUCTS.17 0xf99d02     243    804
  3     FLOW.TOB 0x8a4f7d    1047   2096
  4     FLOW.TOK 0x88054f    3143  25920
  5     FLOW.TXT 0x097789   29063  26199
  6 FLOWWORK.TXT 0x3e89aa   55262  29214
  7  BACK_DN.BMP 0x5350c3   84476   3198
  8  BACK_GR.BMP 0x18e8a7   87674   2824
  9  BACK_HI.BMP 0x585b59   90498   3198
 10  BACK_LO.BMP 0xa7bd06   93696   2824
 11   BACKUP.BMP 0xb0068c   96520  59150
 12     BIGQ.BMP 0xc7d68b  155670 118836
 13    BK_DN.BMP 0xbe0e8b  274506  26166
 14    BK_HI.BMP 0xad05eb  300672  26166
 15    BK_LO.BMP 0x063a49  326838  25282
Y asi continua… Pero saltaremos a lo mas interesante:
220    DEATH.DOC 0xe914eb 5084367    512
221  DOOM_SE.DOC 0xcf8051 5084879    512
222    DOOM2.DOC 0x00cd81 5085391    512
223 DOOM2_95.DOC 0x5706dd 5085903    512
224    FINAL.DOC 0x0001b8 5086415    512
225 FINALDOS.DOC 0x06c72e 5086927    512
226 HERETC13.DOC 0x390e89 5087439    512
227  HERETIC.DOC 0xc72e00 5087951    512
228    HEXEN.DOC 0x9f1106 5088463    512
229  HEXEN11.DOC 0xe95b00 5088975    512
230   MASTER.DOC 0xe8ac43 5089487    512
231    QUAKE.DOC 0x3fe805 5089999    512
232  ULTDM95.DOC 0x0e802e 5090511    512
233   WOLF3D.DOC 0xc62e9e 5091023    512
234  ENCRYPT.EXE 0x223c80 5091535  14894
235   UOK_LO.BMP 0x2e1273 5106429   2686
236     WARN.BMP 0x00019e 5109115  44260
237 COPYRGHT.TXT 0x53430f 5153375     40
238      EXE.TXT 0x8a2634 5153415    235
239    WIN31.BMP 0xed320f 5153650  57852
240  WOLHOLS.BMP 0x9e3b06 5211502  37630
241   LIMITS.TXT 0x9e3f16 5249132    281
242 PRODUCTS.TXT 0x3c802e 5249413    804
243      SKU.TXT 0x9ee8ff 5250217   1706
244  WRNGDIR.BMP 0x8d0d74 5251923  57814
245   WT1_HI.BMP 0xc35d59 5309737  17698
246   WT1_LO.BMP 0x2e4a72 5327435  16230
247    WT1BK.BMP 0x2e9e4a 5343665  41366
248   YOK_DN.BMP 0x3f8326 5385031   1580
249   YOK_HI.BMP 0xebf800 5386611   1562
250   QUAKE1.CAH 0x3f3e89 5388173     30
251   QUAKE2.CAH 0x89260a 5388203     30
252   QUAKE3.CAH 0x083ce8 5388233     30
253   QUAKE4.CAH 0x458926 5388263     30
254   QUAKE5.CAH 0xb00474 5388293     30
255   QUAKE6.CAH 0xe80374 5388323     30
256   QUAKE7.CAH 0xc72e57 5388353     30
257   QUAKE8.CAH 0x2e1274 5388383     30
258   QUAKE9.CAH 0xc72e17 5388413     30
259   QUAKE1.DOC 0x8000a9 5388443    512
260   QUAKE2.DOC 0x744000 5388955    512
261   QUAKE3.DOC 0x157401 5389467    512
262   QUAKE4.DOC 0x0200a9 5389979    512
263   QUAKE5.DOC 0x742000 5390491    512
264   QUAKE6.DOC 0x9e3b3e 5391003    512
265   QUAKE7.DOC 0x753a3c 5391515    512
266   QUAKE8.DOC 0x5edfeb 5392027    512
267   QUAKE9.DOC 0x04882e 5392539    512
268   YOK_LO.BMP 0xebdf24 5393051   1562
269    SETUP.CAH 0x515017 5394613   8187
270 FLOWWORK.BAK 0x5d8b2e 5402800  27176
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:  Q12345678901SKU File:DOC File Location: quake.docGame Detected:     quakeSerial 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:
    Q: Have there been any problems with the crack?

    A: Only a minor one, to unlock Final Doom, you must use a slightly
       different set of parameters. An Example follows:

            Use: QCRACK -g finaldos Q###########  to unlock Final Doom.
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:  Q00000002860SKU File:          sku.17DOC File Location: Finaldos.docGame Detected:     FinaldosSerial 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:  Q00000002860SKU File:          sku.17DOC File Location: Finaldos.docGame Detected:     FinaldosSerial 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