Qcrack explicado

Esta es la tercera entrada de 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 década de los noventa). Estas son las entradas anteriores:
  1. Quake, TestDrive y Qcrack
  2. Qcrack y la belleza de lo innecesario
Ha pasado mucho tiempo desde la última actualización. Finalmente he conseguido generar algunos seriales válidos, pero aún estoy lejos de re-implementar completamente QCRACK.EXE.
Esto es lo que sabíamos desde las entradas anteriores:
  • Se soportan dos modos de operación. En el modo implícito sólo se requiere un “challenge string” como argumento y desde allí QCRACK intentará adivinar cual juego intentamos desbloquear. En el modo explícito se requieren dos argumentos: un “code name” y un “challenge string”.
  • Inicialmente, se valida una suma de control del “challenge string” y al mismo tiempo se calcula un “game number” que se utilizará para múltiples fines en el proceso de generación del serial. Por ejemplo, con el “game number” se puede obtener el “code name” desde el archivo SKU.17 (cuando operamos en modo implícito).
  • Se extrae desde el pseudo-sistema de archivos “FLOWLIB.*” un archivo “*.DOC” de 512 bytes, cuyo nombre corresponde al “code name”. Al contenido de este archivo lo llamamos “DOC”. Por ejemplo, este es el DOC (codificado en hex) extraído del archivo “quake2.doc”:
    c4ddf7cc49c570df66b40e09d50496f57e39eb8a97f2d701849364f89b592461
    94e273afd8c81e06636071d077441f95d7a4f863ba1eef9f3f0660a682e510fd
    2820df7b7778ab48d67349eece3c257a5b296de2b26949a34bfa15dc498416c9
    172aa4ef5aed35dd01b258c5999400ddb58025cc88cd340a7fd9387f02414443
    21de11d25f0241864d2e48be3fbc6e81baaae6ebed0d6c56d856687b68f20c73
    2f5f5ff30a5ebc8a4e3cab4aceec97b065ef5a1339b111967e6c2dc5de304ae5
    511caf29886efdbabc7bfce3fe220a3bdae018206e0bac5cbd5ef35c706141b1
    bdc80954ae68c06e79d29d092e27c07f66569af3353530c60bb61244cfaf9d74
    d5605e5af94b2a86906fda46648b175b7c714778de11f5770747c98a580e7255
    2129852c8fdbca6a33cae32b50a5d73cb99b69a36348bd9b772b3c440d3b3902
    4faf40bf3ba6950bbaa1d55148953012df83366f644cb1e747c67c8f98b8d8b0
    38c836147301e8e1aafbb05a4c43b858dc24cadf2a5764978fc27e8f4ed1991f
    dd90f931540c88ecaa2560ee025d6f10c2bc2afda76bcd6e8d122173289b3193
    646b0126a3aba3b48eb8b7beb95dbdc5ced541de735050baa5ef2a6fcaf0b9dd
    1f07ad0acc8bcf494e927085688171896540e59dcf99b44f67de4ac07e75b651
    845747fce52309440ed92c03afd1c2f51116d15ca4a02d8887a916ad389514ce
Esto es lo nuevo que hemos aprendido:
  • Usando el “code name” y el string “Testdrive Corp.” se calcula un buffer de 256 bytes al que llamamos “SRC”. Por ejemplo, este es el SRC (codificado en hex) para el code name “quake2”:
    2a3c72f6d45f3bdb480040d50f866f7eff43f82478124b49198fc3132102ddfc
    8d2289d9a09d6dbaf090aa57842ff909fe457faf08a6a3462b34c553e7c76259
    be0732c417180d36966c5c65704dd79101ad6074880306b795f41d6898523058
    7c7ba7eb7dea1fb05be4c8fa234fc2a8665d81e0b9a56163ef515a9469cf998b
    b56e93d692d09bf2b35eeedcbc7a1087b26a39779c293de9e66480f53fa125b6
    cecd47bbccd2410a11c90c0e8a4ee3558e9e05ac9fe2fd9754bd8c2cda04f11b
    1e261a3e166771142083d144de4250b479ae82381cfbe1df33e80bc037a435bf
    d38575319a3ac176ed734cf74af3abcaa2ec56272dd8b8e5282e6bcbb1c615a9
  • Usando DOC y SRC se calcula un nuevo buffer de 512 al que llamamos “DST”. Por ejemplo, este es el DST (codificado en hex) para el code name “quake2”:
    1ddeedf22b6a2074f08e4bc5cd090e31bd330b8f853d4adfe6070c0700fbe410
    b2e32f0e0f812452a88442fc3b6fdf2ea87698e71c7835763899b67cae55006a
    d4af5c47d6d75e3aae55fbe188c87c2b078921292876fc49fc45593ef0694987
    70d1cdd7d97a625bfe629c8f3a7e0aa6c85040154b9af1e458a85318794246a0
    201b3c134286c3811cfc076f744738d8519e79ccdf3babc4e9c121456678ba32
    1e532b0dd1284c56c701bedaa9acc782d021033fc1263123ac12c095156f4924
    d6f5e70c60a9e86a307c9067fcb8d5ab7e6c2cca44a6461298ff7046b7080aca
    7303419e472dbbcd343414e0f9242fd58debc9f99c1b1c19dbf42b3a0e086714
    45451d2054500210a8a103bba0059567bae57ea79e95dd90b01aca610bbbcd65
    fd665ffb2f0e5ed8e7fe50917a7191094a1baab3e11af8b4536a401da0b2cb27
    3cd227a07a8f9b552b6b15eb81f16861493966c026c958e4a4d1c681bd047e17
    d19f5d4f9d3a39308f6443f10eb652d3ebcf89bb1175300ed5f99e9455635782
    c6c454ff7c159b8a7bdded2abe40597222b62c0dbf2a0a9b5f37effbf4583100
    5e67ea3f404182165f9dce8c8b835768b324a29d8b020bf694e627dc10ee90a3
    437f659f95671813adc233a4c56d0638f971c7197f4978f8c2b40c22b002e5f3
    056a423c9c75092ec4752d904156776ddfd52398f9cdcffebdea8b7b8793ccf1
  • QCRACK.EXE contiene (hard-coded) un buffer de 508 bytes que también necesitaremos. Lo llamamos “BUF”. Este es el contenido de BUF (codificado en hex):
    8904b81897025b061109770ded0497209200350002116f1af109600c60129c01
    9a01b40a9b1aa715c005390305118b161c00ff124110ad200e0baf21b605c108
    4a19391c5e05c312a218bc1704022114f71cb719cc1be009981ac904d521860d
    a20ef50bbd20c001d8078c042702d508aa21c5063707450e9b009f093c0cbf0c
    ed0dd404ee197016630d73217a161a20ba20b11de5216d18bd21ed0384178414
    e90fe0055a0e3a0e5011e816500ef0066b10b019cd0fe620671b0e0c181a6e01
    5f0d1a1ef401a312710a2d1620012c0ac016aa01b90b51115802e3057d10c60d
    25150813de0689127b0ec30fe00bdf0e3700f71d461871168716411a8e1e5c11
    e21709162e16c21cf3045818ee0b4f204c10431cc813f9122e00d21fae10f717
    9c002608df1da320861b2a122a122609b9082a067e02c404a4022e068a009914
    7b1b4b12f90c0721dd1da1100e1443181d079b1a3f22db06de209719e8008a13
    2a00cc090014f90b5208d41ddf1b3019cd0b20124907441f4406141c20222305
    26083616d4189b21f70a4e04d9212122af104d087f156c14451c3e2179051c12
    1001100e9c18b900b2214907ee05d31b3b1874030a09e415a51c6002a712371a
    791b570a98126e06420750125c21321d2706ff106d163c1edf006006e30a4a03
    021ec121560d000da7194c1ab21940056c0c63036f18cd0f3a019511
  • Finalmente, usando DST se calcula una transformación para BUF. Ya que DST depende del “code name”, el propósito de esta transformación es hacer que “BUF” dependa tambien del “code name”. Esta transformación sobreescribe el valor inicial de BUF (que vimos hace un momento, y que era independiente del “code name”) y sigue siendo un buffer de 508 bytes. Este es el nuevo valor de BUF (codificado en hex) para el code name “quake2”:
    6f04b4189702bf061e09530d4504d5208e0000003a11d91a25093c0cb612c201
    b201480a671afe15b005f403dc11e916d400bf120a105c20770be9219605fd08
    9519921cb705e212bc189717d5026d14271cb4190d1bd1098d1a80040321610d
    dc0ed90bf92086016f078604540294085321ea06ba078c0e4000b409320cd80c
    730d09045e19ba169e0d2c2155164420f0201b1d042195181d212603b817a314
    a00f86057c0e620eed119616810ead066510e219260f6f20b21b900c4d1a3901
    7d0d361e4b01a912850a1c167e01c60a4b16fd010a0bf311cc02c4056d10560d
    e0150e1327064e12b90ecf0f500b3a0ef300da1d071806167e168e1a331ed711
    671743169c16ed1c5b04c018400b4f204b10621c3813b0121400d81ff610a417
    cd005f08b91d19202f1bed128612e6094508ff06e602b40490023a0616008514
    c11b3512f20cca21a71d30105d1403189c07f31a9b221d065120d419f900ba13
    940095095f14160b0d081a1d541b3b19600b131236073c1fd8061d1cff220005
    9b083d16ef1844217f0a32042721bd22db107508b815d214751cae213e05a712
    b001850e7b18e90099215c077305ea1b401899034a096615301c7802a212751a
    b41b590a361295065e0757123c21da1d8f06fc101716a71ea300fb06a00a2f03
    f21e8a21140dc30df3194e1a74191405470c43032a18d00f27017811
  • Una vez calculado el nuevo valor de BUF, se determina el serial usando una permutación de 8 bits del “game number”, una serie de constantes numéricas, y un número mágico que se calcula usando un “offset” y un “depth” para seleccionar valores de BUF que son sometidos a operaciones lógicas. Como es de esperar, “offset” y “depth” se calculan en función del “challenge string” (que no habíamos usando hasta ahora). Este proceso se describirá en una próxima entrada.
Antes de continuar, quiero remarcar que es el valor de BUF el que se usa (junto con el “challenge string” y el “game number”) para generar el serial.  Los valores de DOC, SRC, y DST no se usan directamente.  Siendo así, he volcado desde memoria los valores finales de BUF para cada juego y he conseguido generar algunos seriales validos.  Pero sólo aquellos en los que el valor de depth es 1.  Tengo algún error de lógica en el cálculo de los otros casos... (Solucionado. Ya puedo generar seriales sin restricción).

Aún así, esta es la mejor evidencia que he encontrado para decir que este keygen se originó en un leak del algoritmo: ya que es posible implementar el keygen usando los valores finales de BUF (hard-coded) y si de esta forma se reduce bastante complejidad del keygen, porque alguien implementaría todo el proceso?.   Ya antes había argumentado que una posible explicación es que se pierde generalidad: si el objetivo es escribir un keygen para cualquier aplicación protegida con TestDrive, no es posible usar valores hard-coded.  Pero si ese era el objetivo, ¿porque las instrucciones de uso hablan explícitamente de “quake”?

DEAR PEOPLE FROM THE FUTURE: Here's what we've figured out so far…
Para terminar, quiero mencionar como hice el volcado de los valores desde la memoria: Aunque probé algunas opciones, no encontré un depurador (debugger) que soportara este tipo de binario (COFF usando DPMI), así que finalmente escribí mi propia herramienta para el problema :)
Cuando QCRACK.EXE se encuentra con un SIGTRAP genera un backtrace como el siguiente:
■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
■
■ agony! pray to the one you will pay!
Exiting due to signal SIGTRAP
Breakpoint at eip=00002a37
eax=0004ff54 ebx=00050074 ecx=0005007a edx=00051066 esi=00000200 edi=00056000
ebp=000500a8 esp=0004fed8 cs=01a7 ds=01af es=01af fs=017f gs=01bf ss=01af
Call frame traceback EIPs:
  0x00002a37
  0x00003333
Si solo nos interesa observar el estado de los registros es posible entonces modificar el binario con un editor hexadecimal y agregar un 0xCC en el punto deseado (0xCC corresponde al opcode INT 3, que se encarga de generar el SIGTRAP). El breakpoint lo agregué en 0x2a36 y se muestra como 0x2a37 porque EIP ya esta apuntando a la siguiente instrucción.
Sin embargo, podemos abusar del backtrace incluyendo trozos pre-ensamblados para generar otros comportamientos deseados.  Por ejemplo, en la salida anterior EAX tiene el valor 0x4ff54 (esta es una dirección en el stack).   Si queremos saber que hay en esa dirección podemos desreferenciar este puntero.   Para esto, editamos el binario y esta vez incluimos tres valores: [0x8B, 0x00, 0xCC].  Los dos primeros codifican un “MOV EAX, [EAX]” y el ultimo es el “INT 3” que ya habiamos usado antes.
Esta es la salida que obtenemos:
■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
■
■ agony! pray to the one you will pay!
Exiting due to signal SIGTRAP
Breakpoint at eip=00002a37
eax=00050074 ebx=00050074 ecx=0005007a edx=00051066 esi=00000200 edi=00056000
ebp=000500a8 esp=0004fed8 cs=01a7 ds=01af es=01af fs=017f gs=01bf ss=01af
Call frame traceback EIPs:
  0x00002a37
  0x00003333
He resaltado que el valor de EAX cambia. Ahora es igual al de EBX.  Resulta que el puntero en el stack que teníamos en 0x4ff54 apuntaba al mismo lugar que el registro EBX.  Ese lugar era 0x50074, que a su vez es otro puntero, pero ubicado en el heap.
Si modificamos nuevamente el binario usando este parche: [0x8B, 0x00, 0x8B, 0x00, 0xCC] estaríamos inyectando:
  • MOV EAX, [EAX]    ; para desreferenciar el puntero en el stack
  • MOV EAX, [EAX]    ; para desreferenciar el puntero en el heap
  • INT 3    ; para generar el SIGTRAP
Y ahora nos encontramos con esto:
■ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
■
■ agony! pray to the one you will pay!
Exiting due to signal SIGTRAP
Breakpoint at eip=00002a37
eax=6b617571 ebx=00050074 ecx=0005007a edx=00051066 esi=00000200 edi=00056000
ebp=000500a8 esp=0004fed8 cs=01a7 ds=01af es=01af fs=017f gs=01bf ss=01af
Call frame traceback EIPs:
  0x00002a37
  0x00003333
¡Y ahora en EAX podemos leer un string! Veamos:
print struct.pack('L', 0x6b617571)
Esto nos retorna el substring “quak”, que es el inicio de nuestro “code name”:  “quake2”.
La idea general es esa: agregar trozos de ensamblador según vayamos necesitando y luego generar un SIGTRAP. Desde luego, he ido generando funciones para cada tarea que voy necesitando, pero la mas importante de todas es esta:
def get_24_bytes(addr, game, challenge, breakpoint):
    with open(TARGET_EXE, "r+b") as f:
        addr = struct.pack("L", addr)
        f.seek(breakpoint - BASE_ADDR)
        f.write("\x68" + addr)   # PUSH ADDR
        f.write("\x5F")          # POP EDI
        f.write("\x8B\x07")      # MOV EAX,DWORD PTR DS:[EDI]
        f.write("\x8B\x5F\x04")  # MOV EBX,DWORD PTR DS:[EDI+0x04]
        f.write("\x8B\x4F\x08")  # MOV ECX,DWORD PTR DS:[EDI+0x08]
        f.write("\x8B\x57\x0C")  # MOV EDX,DWORD PTR DS:[EDI+0x0C]
        f.write("\x8B\x77\x10")  # MOV ESI,DWORD PTR DS:[EDI+0x10]
        f.write("\x8B\x7F\x14")  # MOV EDI,DWORD PTR DS:[EDI+0x14]
        f.write("\xCC")          # INT3
    p = subprocess.Popen([TARGET_EXE, "-g", game, challenge],
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)
    output, errors = p.communicate()
    pattern = "eax=(.*) ebx=(.*) ecx=(.*) edx=(.*) esi=(.*) edi=(.*)\r"
    match = re.findall(pattern, errors).pop()
    result = [struct.pack("L", int(reg, 16)) for reg in match]
    return binascii.hexlify("".join(result))
Esta función permite la lectura arbitraria de 24 bytes consecutivos usando los registros EAX, EBX, ECX, EDX, ESI y EDI :) y es trivial extenderla para leer un número arbitrario de bytes asi:
def get_many_bytes(addr, count, codename, challenge, breakpoint):
    ptr, data = 0, ""
    while ptr < count:
        data += get_24_bytes(addr + ptr, codename, challenge, breakpoint)
        ptr += 24
    return data[:count*2]
¡Y eso es todo por hoy!

Comentarios