sábado, 20 de agosto de 2016

Qcrack y la belleza de lo innecesario

Esta es la segunda entrada en lo que espero sea una serie dedicada a reversar QCRACK.EXE, un keygen de 1996 destinado a desbloquear software protegido con TestDrive (esto incluye a Quake y a muchos otros juegos desarrollados por id Software en la decada de los noventa).  La primera entrada es: "Quake, TestDrive y Qcrack".

Descifrando SKU.17

En la entrada anterior comente que no necesitamos realmente descifrar el archivo "SKU.17" (porque podemos extraerlo directamente en texto plano de los archivos FLOWLIB.*) y que, de hecho, es el uso de este archivo el que causa del bug de QCRACK que lo hace generar seriales incorrectos para "Final Doom".  Sin embargo, aclarar que tan compleja es esta rutina podría ayudar a confirmar (o descartar) la idea que tengo de que este keygen pudo ser el resultado de un leak.

Sin mas preambulos, asi funciona el proceso de descifrado para el archivo "SKU.17": Se descifran bloques de hasta 1024 bytes.  El proceso se realiza para todos los caracteres desde el segundo en adelante (el primer caracter tendra un tratamiento diferente al final del ciclo) y consiste en lo siguiente:  se calcula el XOR del caracter en la posicion i con el caracter en la posición (i - 1) y con termino en la posición i de la secuencia de Fibonacci, esto es: ci ⊕ ci - 1 ⊕ Fi.

Ciclo de descifrado
Al llegar a el bloque mostrado en la imagen anterior, el registro "esi" apunta al buffer que contiene el bloque de datos a descifrar, y el registro "edi" almacena el numero de bytes a descifrar.  El ciclo de descifrado esta entre 0x2600 (bonito numero) y 0x262a.

En las tres primeras lineas vemos como se inicializan las variables [ebp - 0x1b4] y [ebp - 0x1b0] que llevaran la cuenta de los terminos Fibonacci, asi como el registro ebx que funcionara como indice para el ciclo.  En 0x260c y 0x2610 se ve el doble XOR (el primero es el XOR entre el caracter previo y el numero de Fibonacci). La actualizacion de los terminos Fibonacci esta entre 0x2614 y 0x2626. Finalmente, la llamada en 0x263b (call 0x3a5c) es un "call memcpy" que va uniendo los bloques descifrados.

En la ultima linea, la llamada en 0x265c (call 0x173c) es una llamada a la funcion que descifra el primer caracter del bloque.  Para esta funcion,  "eax" apunta al buffer descifrado (y por tanto, al primer caracter), y tambien vemos que apila un 8 como segundo parametro.  Este proceso de descifrado para el primer caracter utiliza una "bit-reversal permutation" de ocho bits.

Descifrado del primer caracter
Entender esta rutina no fue facil.  Reconstruirla es bastante simple, pero entender que estaba haciendo me tomo algun tiempo.  Aqui esta una reimplemntación mas o menos literal:


def sub_173C(arg1, arg2):
    result = 0
    while arg2:
        result = arg1 & 1 | 2 * result
        arg1 >>= 1
        arg2 -= 1
    return result

No entiendo :(

En este punto tuve que atacar los datos en lugar de atacar el codigo:

for i in range(16):
    print "%d -> %d" % (i, sub_173C(i, 8))

Al evaluar la transformacion que realiza esta funcion sobre unos pocos bytes se encuentra esto:

0 -> 0
1 -> 128
2 -> 64
3 -> 192
4 -> 32
5 -> 160
6 -> 96
7 -> 224
8 -> 16
9 -> 144
10 -> 80
11 -> 208
12 -> 48
13 -> 176
14 -> 112
15 -> 240

Una busqueda de la serie "0, 128, 64, 192, 32, 160, …" nos lleva a identificar la secuencia A160638: Bit-reversed 8-bit binary numbers  :)  El argumento 1 se lee como binario (usando el padding a ceros que sea necesario para completar 8 bits), este valor invierte (se lee de atras hacia adelante) y el nuevo valor binario resultante se lleva nuevamente a la base original.  No lo hubiera entendido solo mirando el codigo :)

A continuación, la versión completa de la rutina de descifrado (incluyendo una versión de la funcion anterior mucho mas al estilo de python):


def bit_reversal(n, k):  http://stackoverflow.com/q/12681945/
    return int('{:0{width}b}'.format(n, width=k)[::-1], 2)


def fibonacci(n):  
    a, b = 11
    for i in range(n - 1):
        a, b = b, a + b
    return a


def decode_piece(data):
    data = bytearray(data)
    for in range(1, len(data)):
        data[i] ^= data[i - 1] ^ (fibonacci(i + 1) & 0xff)
    data[0] = bit_reversal(data[0], 8)
    return data


def decode_sku(sku):
    data = ''
    with open(sku, 'rb') as f:
        while True:
            piece = f.read(1024)  # block-size must be 1024 bytes
            if not piece:
                break
            data += decode_piece(piece)
    return data


# demo
print decode_sku('/dosprogs/QCRACK/SKU.17')


En la función bit_reversal() decidí usar una implementación en la que el ancho puede pasarse como un parámetro, en lugar de usar alguna de las versiones mas eficientes (que solo funcionan para ocho bits), porque en otras partes de QCRACK se usa este mismo truco con un numero de bits diferente.

Adicionalmente, la mascara "& 0xff" en mi decode_sku(), sería mas eficiente tenerla directamente en la funcion fibonacci(), algo como: "a, b = b, (a + b) & 0xff", sin embargo, ya no seria correcto llamar "fibonacci" a la funcion resultante. Como nunca calcularemos valores mas grandes que fibonacci(1024), no creo que haga falta… Por esta misma razón tampoco incluí el decorador de memoización que suelo usar en este tipo de funciones.

A decir verdad, es mucho mas eficiente usar algo mas cercano a la versión desensamblada (actualizando los terminos de la serie al mismo tiempo que se va descifrando cada caracter), aunque de esta forma se pierde legibilidad:

def decode_sku(sku):
    decoded = ''
    with open(sku, 'rb'as f:
        while True:
            piece = bytearray(f.read(1024))
            if not piece:
                break
            i, j, k = 111
            while i < len(piece):
                piece[i] ^= piece[i - 1] ^ k
                i, j, k = i + 1, k, (k + j) & 0xff
            piece[0] = int('{:08b}'.format(piece[0])[::-1], 2)
            decoded += piece
    return decoded

Comentarios

Esta rutina no habla muy bien del componente criptografico de TestDrive (¿y recuerdan el ENCRYPT.EXE que vimos antes?).  De haber encontrado una funcion criptografica compleja, funcionaría como evidencia de un leak, ahora ya debo reconsiderar mi postura sobre ese aspecto de esta historia…

En la entrada anterior me preguntaba ¿Porque alguien incluiría esta rutina innecesaria? Despues de identificar que este proceso usa dos secuencias numericas bien conocidas, la respuesta bien puede ser: "porque aunque innecesaria esta rutina no deja de ser bonita" :)

domingo, 14 de agosto de 2016

Quake, TestDrive y Qcrack

Quake

Quake es un FPS lanzado por id Software en 1996 (aquí tienen un speedrun 100%, para los que no lo conocieron) y ademas de ser el primer FPS en un motor grafico 3D, tambien 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 epoca remota (hace 20 años), una epoca en que no todos tenían ancho de banda para descargar de Internet un juego de 23 MB… una epoca en la que necesitabamos "conectarnos" a Internet via SLIP, con extrañas maquinas que hacian ruiditos… una epoca 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 una copia encriptada del juego completo. De esta forma, si alguien quería comprar el juego completo, podía llamar por telefono directamente a id Software (no a GT Interactive), hacer una orden con su tarjeta de credito y recibir un codigo de desbloqueo para empezar a jugar de inmediato.  Toda una innovación :)

Ah… pero hay un detalle más, como 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…  Iban a recibir ganancias hasta 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 tambien una banda sonora para el juego, compuesta por Trent Reznor, el de Nine Inch Nails… Una banda sonora bastante buena por cierto…
Por aqui tenemos la interface del sistema de desbloqueo (noten el logo "Test Drive" en la esquina inferior izquierda, hablaremos de ello en un momento):


QUAKE Unlock: el sistema de desbloqueo para obtener la versión completa de Quake
(Esta pantalla corresponde al binario IDSTUFF/FLOW.EXE)

QCRACK.EXE

Todos podemos imaginar lo que pasó, pero aqui 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.  No he encontrado mucho sobre ellos. 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 para Quake


QUAKE Unlock aceptando el codigo generado por qcrack.exe

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


Entonces, ademas de Quake (y su banda sonara) tenemos estos juegos: Final DoomDoom II: Hell on Earth, Ultimate DoomMaster 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 solo un crack para QUAKE, sino mas bien un keygen para el sistema de protección TestDrive, una suerte de protector comercial de esa epoca, asi que podría funcionar en mas aplicaciones de esa decada.

Por cierto, parece que TestDrive ya no existe más, y lo que solía ser el sitio web de la empresa, ya 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. The company’s blue-chip clients, such as Hewlett-Packard, id Software, Intel, Intuit and Novell, use TestDrive’s technology such as its patented preview engine, virtual install, and encryption techniques as well as its graphical user interface (GUI), production, packaging, transaction and fulfillment services. In 1997 TestDrive will bring its unique try-before-you-buy techologies to the Internet allowing it's customers to set up electronic software "stores". To see an example of a leading product wrapped in TestDrive's encryption technology, go to the id Software Shopping Maul and download Quake.
Como nos hablan de un sistema patentado vale la pena hacer una busqueda de patentes. Encontre 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 (file image), for example, by replacing the modified start-up code segment with the application's original start-up code.
Puede verificarse que asi es como funcionan los archivos distribuidos en el CD-ROM de Quake Shareware.  Las versiones completas de los juegos estan 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 porcion 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:  valida el numero de serie, descifra el stub inicial, y lo reemplaza en al version desnaturalizada (i.e. el archivo *.MJ3) para recuperar el contenido original.  Es un crypter, sin packer, ni loader :)  

Segun esto sería posible escribir directamente un unpacker.  Sin preocuparnos por la parte de generar un serial number para cada challenge string… Pero la idea no es desproteger los juegos de id Software sino entender a QCRACK asi que mejor vamos a analizarlo :)

Como nota final para la sección, tambien puede hacerse una busqueda de marcas registradas para conocer un poco mas de la empresa.


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 ningun parámetro y obtener un texto que incluye, entre otras cosas, el modo de uso:
usage: qcrack [-g <game>] <challenge>
eg, qcrack Q12345678901
eg, qcrack -g quake Q12345678901
Si probamos el primer ejemplo (qcrack Q12345678901), recibimos el siguiente mensaje de error:
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 siguiente mensaje de error:
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 despues 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 estamso necesitando.  No encontramos ningun archivo QUAKE.DOC, pero teniendo en cuenta que nos dice "not even in the .lib", buscamos tambien archivos *.LIB y encontramos un FLOWLIB.LIB (junto con un FLOWLIB.DIR que tambien 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):
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):
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 validacion se encuentra entre 0x1BB8 y 0x1C52 (ahh… por cierto… ¿mencione que este es un COFF de 16bits?) y vemos tambien que este checksum incluye llamadas a un par de funciones desconocidas en 0x47AC y 0x7FE8:

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 caracter).  Tambien apilamos la direccion de  [ebp - 0x10] (que funcionara como buffer destino) y llamamos la funcion en 0x47AC.  Mas adelante vemos el mismo patron (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 numeros.  Si ignoramos la letra nos quedan once numeros… si iniciamos en la posicion 1 y extraemos 4 caracteres (la 'Q' era la posicion 0) y luego iniciamos en la posicion 5 y extraemos 7 caracteres lo que estamos haciendo es partir el serial en dos numeros… Es facil 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 llamadas a esta funcion se agrega manualmente el null-terminator en la posicion correcta.  El "atol" se descubre porque es un wrapper para otra funcion en la que se fijan los valores de dos argumentos adicionales (0 y 10).  Esta funcion interna es "strtoul".
Desde allí, es facil seguir las operaciones para reconstruir algo como esto:

def ValidateChecksum(challenge):
    s4 = int(challenge[11 + 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 caracter es inutil.  Esto puede verificarse facilmente: basta con cambiar el primer caracter 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 imprimos el valor de esta variable, veremos que para el challenge string Q12345678901 tenemos c = 90.  Y Podemos verificar que poco despues 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 in range(5):
    print "Challenge for Game #%i: %s" % (n, GenerateChallenge(n))

Con lo que obtenemos las siguiente salida:

Challenge for Game #0: Q00000000000
Challenge for Game #1: Q00000000260
Challenge for Game #2: Q00000000520
Challenge for Game #3: Q00000000780
Challenge for Game #4: 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 evaluacion 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 neceario 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 generacion de seriales, reverse tambien 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.*
Despues 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 despues de eso vienen 6 DWORDS por cada archivo describiendo: el nombre del archivo (3 DWORDS = 12 caracteres, 8 del nombre, 3 de la extension, 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 tagFLOW_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 parsear el archivo:

Parseando 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:

flowlib_dir = 'C:/dosprogs/QCRACK/FLOWLIB.DIR'
flowlib_lib = 'C:/dosprogs/QCRACK/FLOWLIB.LIB'

import struct
import os

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
 16   BK1_DN.BMP 0x74003d  352120  20002
 17   BK1_HI.BMP 0xf600a8  372122  20002
 18   BK1_LO.BMP 0x8b00b9  392124  20042
 19   BK2_DN.BMP 0xe50000  412166  19256
 20   BK2_HI.BMP 0x8a2651  431422  19256
 21   BK2_LO.BMP 0x8020ca  450678  19218
 22   BK3_DN.BMP 0x03c683  469896  20896
 23   BK3_HI.BMP 0x8349a4  490792  20896
 24   BK3_LO.BMP 0xbeaa01  511688  20890
 25   BOUGHT.BMP 0xe82fb0  532578  45794
 26    BQ_HI.BMP 0xe3e89c  578372  16602
 27    BQ_LO.BMP 0x74c00a  594974  16434
 28   BT1_HI.BMP 0x36891f  611408  14314
 29   BT1_LO.BMP 0x0174ff  625722  12406
 30    BT1BK.BMP 0x74db0a  638128  32642
 31   BT2_HI.BMP 0xadc5be  670770  14080
 32   BT2_LO.BMP 0x4c725a  684850  12794
 33    BT2BK.BMP 0x3a0045  697644  18402
 34    BTEXT.BMP 0x0a7408  716046  15444
 35   BTEXT2.BMP 0xa2a771  731490  17444
 36      C16.BMP 0x00b856  748934  32862
 37     CALL.BMP 0xa70875  781796  13238
 38    CALL2.BMP 0x8b13eb  795034  13090
 39    CALL3.BMP 0x7401a7  808124  13770
 40    CALL4.BMP 0x06c69c  821894  13272
 41    CALL5.BMP 0xcf2ba7  835166  13514
 42  CONGRAT.BMP 0x04eb83  848680  38922
 43  DEAHOLS.BMP 0xaaaa3f  887602  40898
 44   DMHOLS.BMP 0x9c5556  928500  44504
 45 DMSEHOLS.BMP 0x330544  973004  38326
 46    ERROR.BMP 0x8bfc8a 1011330  34176
 47  EXIT_DN.BMP 0xd62ba7 1045506   2156
 48  EXIT_GR.BMP 0xc35b59 1047662   2016
 49  EXIT_HI.BMP 0x1ba8e8 1049678   2156
 50  EXIT_LO.BMP 0x1ce8f0 1051834   2016
 51  EXITBOX.BMP 0x21cd00 1053850  19792
 52     FAKE.BMP 0xc70675 1073642   1112
 53   FINDOS.BMP 0xcd005c 1074754  44514
 54  FINHOLS.BMP 0xa524ba 1119268  44316
 55   FSK_DN.BMP 0xb1ba1b 1163584  30100
 56   FSK_HI.BMP 0x8ce821 1193684  30100
 57   FSK_LO.BMP 0x75c0fe 1223784  30406
 58    GREAT.BMP 0xb4ff61 1254190  12278
 59  HERHOLS.BMP 0x3de62d 1266468  37022
 60  HEXHOLS.BMP 0x75a2eb 1303490  39034
 61   HOW_DN.BMP 0x0082ba 1342524   4186
 62   HOW_HI.BMP 0x22eb98 1346710   4186
 63   HOW_LO.BMP 0x7500f9 1350896   3900
 64    HOWTO.BMP 0xadda8b 1354796  12266
 65  IDLOGO1.BMP 0x01b800 1367062  26162
 66  IDSF_HI.BMP 0xd88a21 1393224   4908
 67  IDSF_LO.BMP 0xdabf4e 1398132   5900
 68  IDSTPOP.BMP 0x3e8027 1404032  87530
 69 INTER_DN.BMP 0xace8d2 1491562   3692
 70 INTER_HI.BMP 0x13e807 1495254   3692
 71 INTER_LO.BMP 0xb4a587 1498946   3286
 72  IORDERS.BMP 0x5f1a70 1502232  62398
 73    ITEXT.BMP 0x33c321 1564630 136332
 74    LEGAL.BMP 0x26a3a7 1700962  12116
 75  MASHOLS.BMP 0x87a2a6 1713078  43074
 76   MGREAT.BMP 0x50a2a6 1756152   9360
 77   MGTEXT.BMP 0x01a2a3 1765512  52124
 78    MSHOP.BMP 0x85a3a6 1817636  10152
 79   MSTEXT.BMP 0xe1a3a5 1827788  38502
 80  NEWBACK.BMP 0xb30081 1866290 279802
 81 NEWBACK2.BMP 0xfe9c0b 2146092 286246
 82  NOSPACE.BMP 0x10c5f7 2432338  44532
 83     NULL.BMP 0x0806f7 2476870   1120
 84    OK_DN.BMP 0xc5f700 2477990   3240
 85    OK_HI.BMP 0x462004 2481230   3240
 86    OK_LO.BMP 0xa75c06 2484470   2846
 87   P1S_DN.BMP 0x36e9e8 2487316  12884
 88   P1S_HI.BMP 0xbea761 2500200  12884
 89   P1S_LO.BMP 0xc7a628 2513084  12826
 90   P2S_DN.BMP 0x800d75 2525910  12878
 91   P2S_HI.BMP 0x3fa2e0 2538788  12878
 92   P2S_LO.BMP 0xba1176 2551666  12830
 93   P3S_DN.BMP 0xa02075 2564496  12938
 94   P3S_HI.BMP 0x020446 2577434  12938
 95   P3S_LO.BMP 0x044e80 2590372  12888
 96   P4S_DN.BMP 0x3ab098 2603260  12400
 97   P4S_HI.BMP 0x147400 2615660  12400
 98   P4S_LO.BMP 0x81beed 2628060  12408
 99   P5S_DN.BMP 0x0c7500 2640468  12926
100   P5S_HI.BMP 0x0345e8 2653394  12926
101   P5S_LO.BMP 0x437500 2666320  12896
102   P6S_DN.BMP 0x970d06 2679216  12816
103   P6S_HI.BMP 0x0ba2a3 2692032  12816
104   P6S_LO.BMP 0x00a746 2704848  12722
105   P7S_DN.BMP 0x7480c7 2717570  12414
106   P7S_HI.BMP 0xc682eb 2729984  12414
107   P7S_LO.BMP 0x743afc 2742398  12358
108   P8S_DN.BMP 0x50e903 2754756  12718
109   P8S_HI.BMP 0xc032a4 2767474  12718
110   P8S_LO.BMP 0xbe068d 2780192  12528
111   PL1_HI.BMP 0x98b6ba 2792720   2512
112   PL1_LO.BMP 0x107203 2795232   2222
113   PL2_HI.BMP 0x800775 2797454   2832
114   PL2_LO.BMP 0x00a75e 2800286   2396
115   PL3_HI.BMP 0xa68b06 2802682   2668
116   PL3_LO.BMP 0xc4e80a 2805350   2330
117   PL4_HI.BMP 0x74ffa6 2807680   2624
118   PL4_LO.BMP 0x8b0d74 2810304   2260
119  PLAY_DN.BMP 0xe8a6e6 2812564   3284
120  PLAY_GR.BMP 0x2900b8 2815848   2920
121  PLAY_HI.BMP 0x06c605 2818768   3284
122  PLAY_LO.BMP 0x8ba30b 2822052   3014
123  PLAYPOP.BMP 0xa68b06 2825066  89362
124 PLAYQ_HI.BMP 0xbec3c0 2914428   5466
125 PLAYQ_LO.BMP 0x01bac9 2919894   6078
126  PRNT_DN.BMP 0xa74816 2925972   3544
127  PRNT_GR.BMP 0x831451 2929516   3290
128  PRNT_HI.BMP 0x1e8bfd 2932806   3544
129  PRNT_LO.BMP 0xba0674 2936350   3290
130   QBACK1.BMP 0x02f4e8 2939640 275738
131   QKBACK.BMP 0x8b8f72 3215378 265940
132  QKBACK2.BMP 0xb0a310 3481318 275958
133    QUAKE.BMP 0xa3100e 3757276  70172
134  QUNLOCK.BMP 0x3e808f 3827448  61734
135 QUNLOCK2.BMP 0x0b3e80 3889182  11994
136  RTFWHIT.BMP 0x33c3f9 3901176   8892
137  SIGN_HI.BMP 0x03a742 3910068   4812
138  SIGN_LO.BMP 0x8ae903 3914880   6062
139  SIGNPOP.BMP 0x21cd2c 3920942  87336
140    SK_DN.BMP 0x8607bc 4008278  22332
141    SK_HI.BMP 0x00fb83 4030610  22332
142    SK_LO.BMP 0x4400b8 4052942  23066
143   SMLOGO.BMP 0xc2f70b 4076008  10282
144   SOF_DN.BMP 0xe8a680 4086290   2784
145   SOF_GR.BMP 0x61a156 4089074   2784
146   SOF_HI.BMP 0x8b3e88 4091858   3030
147   SOF_LO.BMP 0xdddce8 4094888   2784
148   SON_DN.BMP 0xba4300 4097672   3010
149   SON_GR.BMP 0x0244c7 4100682   2754
150   SON_HI.BMP 0x0a44c6 4103436   3010
151   SON_LO.BMP 0xb8db33 4106446   2754
152   SP1_HI.BMP 0xe1163a 4109200  16542
153   SP1_LO.BMP 0x40b473 4125742  14700
154   SP2_HI.BMP 0x80b8eb 4140442  20688
155   SP2_LO.BMP 0xd232ff 4161130  19460
156   SP3_HI.BMP 0xf800eb 4180590  16152
157   SP3_LO.BMP 0xb99e27 4196742  14692
158   SP4_HI.BMP 0x06c605 4211434  19740
159   SP4_LO.BMP 0x74003d 4231174  18430
160  SRQUAKE.BMP 0x74003d 4249604  40566
161    STAND.BMP 0xa0c3f9 4290170  38650
162    STEXT.BMP 0x037400 4328820  15430
163   STITLE.BMP 0xba00a3 4344250  12836
164   TDLOGO.BMP 0xbac933 4357086   8606
165  TDLOGO2.BMP 0x29a60b 4365692   5694
166   TOK_DN.BMP 0x21cd44 4371386   1660
167   TOK_GR.BMP 0x74a741 4373046   1674
168   TOK_HI.BMP 0xe90deb 4374720   1674
169   TOK_LO.BMP 0x01a581 4376394   1674
170    TS_DN.BMP 0xa31006 4378068  29754
171    TS_HI.BMP 0x80a742 4407822  29754
172    TS_LO.BMP 0xe90373 4437576  27818
173   TS1_DN.BMP 0xa73f3e 4465394  20546
174   TS1_HI.BMP 0x89a74c 4485940  20546
175   TS1_LO.BMP 0xcfe8c3 4506486  20454
176     TS1B.BMP 0x4e0e8b 4526940  43970
177   TS2_DN.BMP 0x3e8021 4570910  24102
178   TS2_HI.BMP 0x21cd3e 4595012  24102
179   TS2_LO.BMP 0x245010 4619114  24122
180     TS2B.BMP 0xa6243e 4643236  38948
181   TS3_DN.BMP 0x75c00a 4682184  24418
182   TS3_HI.BMP 0x9725ba 4706602  24418
183   TS3_LO.BMP 0xa4e1a2 4731020  24334
184     TS3B.BMP 0x7400a7 4755354  48044
185   TS4_DN.BMP 0xe0d0a3 4803398  20806
186   TS4_HI.BMP 0x1974c0 4824204  20806
187   TS4_LO.BMP 0x103e89 4845010  20624
188     TS4B.BMP 0x0008b9 4865634  34854
189  UCAN_DN.BMP 0xb01274 4900488   1808
190  UCAN_GR.BMP 0x0446f6 4902296   1830
191  UCAN_HI.BMP 0x21cd01 4904126   1828
192  UCAN_LO.BMP 0xd6e5e9 4905954   1828
193 UEXIT_DN.BMP 0x568b4d 4907782   1694
194 UEXIT_GR.BMP 0x0673e2 4909476   1708
195 UEXIT_HI.BMP 0x768b18 4911184   1708
196 UEXIT_LO.BMP 0xff8000 4912892   1708
197  UNLK_HI.BMP 0x49c88b 4914600   5294
198  UNLK_LO.BMP 0xa2f0a0 4919894   6094
199  UNLKPOP.BMP 0x74e806 4925988  88078
200   UNLOCK.BMP 0xc7f604 5014066  57182
201  UNLOCK1.BMP 0x383ab3 5071248   6270
202   UOK_DN.BMP 0xce8b51 5077518   3060
203   UOK_HI.BMP 0x3a5e59 5080578   3060
204    DEATH.CAH 0x027e89 5083638     28
205  DOOM_SE.CAH 0xaac032 5083666     32
206    DOOM2.CAH 0x89c033 5083698     28
207 DOOM2_95.CAH 0x75e179 5083726     34
208    FINAL.CAH 0x06c605 5083760     28
209 FINALDOS.CAH 0x063a01 5083788     34
210 HERETC13.CAH 0xe8df29 5083822     34
211  HERETIC.CAH 0x06c6a7 5083856     32
212    HEXEN.CAH 0x012ee8 5083888     28
213  HEXEN11.CAH 0xac0117 5083916     32
214   MASTER.CAH 0xa76306 5083948     30
215    QUAKE.CAH 0x24eb04 5083978     28
216  ULTDM95.CAH 0xe3e186 5084006     32
217   WOLF3D.CAH 0xa79d3e 5084038     30
218 COPYRGHT.DAT 0x6406c6 5084068     38
219  T-DRIVE.DAT 0x74c33a 5084106    261
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:

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 incremntar 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):
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 :)

¿Seran estos bloques de 512 bytes parte de la informacion que TestDrive extrae, cifra y esconde durante el proceso de desnaturalizacion?

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.  

Decidi hacer algunas pruebas dejando que FLOW.EXE me creara algunos challenges para cada juego y verificando el nombre del juego que identificaba QCRACK.  Encontre que en lugar de usar la lsita 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

Caundo estaba investigando sobre QCRACK, antes de encontrar una copia del ejecutable encontre una FAQ sobre QCRACK 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:
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:
Challenge String:  Q00000002860
SKU File:
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 autodetectado es incorrecto.  Mientras que el serial generado cuando le decimos explicitamente el 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 mayuscula inicial afecta el resultado.  Podemos confirmarlo dandole explicitamente la opcion "-g Finaldos" con lo que obtenemos el mismo serial erroneo inicial.   Tambien cambia si le decimos algo como "-g fiNalDoS". Asi que la generacion del serial utiliza el nombre del juego y es sensible a las mayusculas.

Curiosamente el bug se origina en SKU.TXT que es la version en plano del SKU.17 cifrado.  Al analizar el texto plano vemos que tanto "final" como "finaldos" aparecen con mayuscula inicial.  Pero en EXE.17 aparecen en minusculas.  Asi 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…