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:
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”:
- 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”:
- 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”:
- 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):
- 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”:
- 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.
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!
Este comentario ha sido eliminado por el autor.
ResponderEliminar