En la primera parte vimos algunas cosas básicas del debugger de DOSBox, y llegamos hasta el comienzo del programa principal ya desencriptado. Para retomar nuestro análisis, no hace falta volver sobre los mismos pasos. (Excepto abrir el DOSBox con debugger y activarlo escribiendo debug humans, claro). Habíamos hablado de que la sección Code Overview funciona como una ventana que podemos desplazar con las flechas del cursor. Pero también podemos apuntarla a una dirección determinada escribiendo C [segmento]:[desplazamiento]. Escribamos C CS:0132, que es donde sabemos termina la rutina de arranque, y coloquemos un freno con F9.
Aquí hace falta hacer un par de aclaraciones. En primer lugar, que CS no es otra cosa que un registro que indica el segmento de código actual. (Momento para mirar en Register Overview y ver que no miento, al menos sobre esta cuestión.) Venimos usándolo en lugar de un valor fijo porque éste puede cambiar, no sólo durante la ejecución, sino también de una máquina a otra, dependiendo de la configuración, controladores y programas residentes. En segundo lugar, sepamos que sólo apuntamos la ventana, pero el registro IP (instruction pointer) sigue indicando la dirección original (0000) y es desde ahí que el procesador va a continuar. Pulsemos F5 para ejecutar toda la rutina de arranque (desde CS:0000 hasta llegar a CS:0132), demos un paso más con F10, y estaremos nuevamente ante el comienzo del programa principal:
0000: call 0000816E
0003: call 000056BC
0006: call 00000677
0009: call 0000B43C ;esta es la que nos interesa
000C: mov [00EF4],0000
A diferencia de la vez anterior, ahora vamos a ejecutar sólo las tres primeras subrutinas (¿hace falta que aclare que tenemos que pulsar F10 tres veces? – bárbaro, gracias). Sabemos que en la cuarta está el código a desmenuzar así que, en vez de F10, pulsemos F11 (step into). ¿Cuál es la diferencia? Como su nombre lo sugiere, en vez de ejecutar la llamada call 0000B43C
completa, dimos un paso “hacia adentro” de la subrutina. Una vez dentro, seguiremos con el habitual F10 (step over). Tomemos nota de que estamos en CS:B43C, que es nuestro nuevo punto de partida.
Empecemos a caminar esta nueva etapa y veremos aparecer la pantalla de carga (esa que dice «The Humans – Spanish Version»). Luego, en CS:B4C2 y CS:B4D1, aparecen dos saltos condicionales hacia atrás. Ya sabemos cómo lidiar con ellos (sí, se pueden saltear los dos juntos). Al llegar a CS:B505 pongamos un freno, pero antes borremos todos los anteriores (BPDEL *) para poder retomar directamente desde aquí cuantas veces haga falta:
B505: mov di,4894
B508: mov bp,0028 ;mostrar pantalla
B50B: mov bx,0000 ;solicitando la
B50E: mov cx,0100 ;página correspondiente
B511: call 00008C08 ;al dibujo
B514: call 00008BE7
B517: jmps 0000B574 ;ingreso y verificación (¿no vuelve?)
B519: mov di,4594
B51C: mov bp,0023 ;borrar pantalla
B51F: mov bx,0000 ;tras el ingreso correcto
B522: mov cx,0100 ;(nótese la simetría
B525: call 00008C08 ;con el bloque anterior)
B528: call 00008BE7
Acá está la papa. En estas líneas se muestra la pantalla con la señorita, se piden las claves y se vuelve a borrar la pantalla. Sin embargo, hay algo que no cierra. El salto en CS:B517 debería, en principio, ser una llamada, ya que cuando damos una respuesta correcta el flujo del programa continúa en CS:B519 (para comprobarlo sólo hace falta poner un freno ahí). Si nuestra respuesta no es satisfactoria, en cambio, iremos a parar a la consola del DOS con el mensaje “Program protection failure, Please re-run”, sin haber pasado por CS:B519.
Lo que esto significa es que a partir de CS:B574 está la lectura del teclado y el control de autenticidad. Si algo falla, el programa no vuelve a su curso normal. Esto nos lleva a realizar un experimento casi cantado. Si el fragmento citado contiene todo el proceso, por qué no saltearlo y ver qué pasa. Salgamos al DOS pulsando Alt-X, volvamos a activar el debugger escribiendo debug humans, y pulsemos F5 para dejar correr el programa. Cuando la ejecución se detenga en CS:B505 escribamos SR IP B52B. Esta orden cambia el valor del registro IP que mencionábamos antes. Es decir que cuando dejemos correr nuevamente el programa, en vez de continuar en CS:B505 lo hará desde CS:B52B, justo después del bloque.
Pulsemos F5. Suena la melodía y vemos el logo de Mirage. ¡¿Lo logramos?! No cantemos victoria: ni bien tratemos de continuar nos encontraremos de regreso en la consola del DOS. ¡A no desalentarnos! No habremos logrado quebrar la protección, pero es evidente que estamos bien encaminados. Y además descubrimos que más adelante existen otros puntos en los que se controla el dato ingresado. En lugar de buscarlos uno por uno (sin saber cuántos son en total), nos conviene identificar y reproducir el efecto de ingresar el número de página correcto. Vamos a tener que zambullirnos en el código localizado en CS:B574, que es ahora nuestro nuevo punto de referencia.
Un maestro, una causa, un efecto
Volvamos a empezar, y al llegar a CS:B505 pongamos un freno en CS:B574, para ver qué pasa al ingresar el número de página. Las rutinas de lectura del teclado suelen consistir en un gran bucle que compara los códigos suministrados por una función del DOS o de la BIOS para determinar qué tecla ha sido pulsada, llevar la cuenta de los caracteres ingresados, y salir cuando el ingreso esté completo. Recorriendo el código veremos que en CS:B5AB hay un salto al comienzo, que cierra el bucle del que hablábamos. Pero también vemos la salida:
B5A2: mov al,[457B]
B5A5: or al,al
B5A7: jne 0000B5AD
B5A9: loop 0000B59F
B5AB: jmps 0000B574
B5AD: mov b,[457B],00
B5B2: cmp al,0D
B5B4: je 0000B5F2
B5B6: cmp al,0E
B5B8: je 0000B5DC
B5BA: cmp al,30
B5BC: jb 0000B574
B5BE: cmp al,39
B5C0: ja 0000B574
A partir de CS:B5B2 comienza una serie de comparaciones sobre el caracter ingresado, que entre otras cosas descarta cualquier letra o símbolo que no sea un número del 0 al 9 (ver CS:B5BA y CS:B5BE, valores en ASCII). Pero la primera comparación de todas es contra el número hexadecimal 0Dh (¡bendito número 13 de la suerte!) que corresponde a la tecla Intro. Podemos estar seguros que esa es la salida que buscamos, así que pongamos un freno en CS:B5F2, dejemos correr el programa e ingresemos el número de página correcto.
Tal como lo previmos, el debugger retomó el control en CS:B5F2. Avancemos por el código para ver adónde nos lleva, y tras unos pocos pasos nos encontraremos saltando entre CS:B63A y CS:B637. Hemos venido a parar a un callejón sin salida, cuyo propósito es precisamente que le perdamos el rastro a la ejecución. La buena noticia es que no hace falta seguirlo.
B633: mov [3ECD],bp
B637: mov bx,B637
B63A: jmp bx
Justo antes de estancarse, el código almacena el contenido del registro BP, resultado de unos cálculos hechos líneas antes, en la dirección DS:3ECD. Apuntemos la ventana Data Overview unas posiciones antes, para tener un contexto, escribiendo D DS:3EC0 y ver si podemos sacar alguna conclusión. El valor de 16 bits (word) almacenado en DS:3ECD (señalado con B en la captura) es idéntico al de la posición DS:3ECB (señalado con A). Y si repetimos el experimento, respondiendo correctamente a un dibujo diferente, veremos que el valor A cambia, y el correspondiente valor calculado (B) coincide con él. Es hora de repetir el mismo experimento de antes, pero con una variante: una vez detenidos en CS:B505 copiaremos el contenido de la memoria de A a B, antes de modificar el IP para saltear toda la rutina, siguiendo estos pasos:
- D DS:3ECB (apuntar la ventana de datos)
- SM DS:3ECD <byte> <byte> (copiar los dos primeros bytes de Data Overview)
- SR IP B52B (modificar el IP para saltear la rutina de claves)
Dejemos correr el programa y esta vez pasamos sin problemas a la segunda presentación, al menú y podemos disfrutar del juego, nivel tras nivel. Esta vez sí hemos quebrado la protección.
¿Y ahora qué pasa, eh?
El método funciona pero está claro que todavía tenemos pendiente elaborar un parche que lo implemente. Dicho parche tendrá que hacer su trabajo una vez que el juego esté desencriptado en memoria. Y algún lector sagaz se habrá percatado de una complicación adicional: el valor correspondiente al dibujo (aquel que señalamos con A) se calcula aleatoriamente cada vez que el juego se pone en funcionamiento. Una posible solución es localizar la rutina que calcula un nuevo valor y anularla.
Dado que esta segunda parte también se ha extendido bastante, prefiero dejar la implementación del parche para la próxima (y última) entrega. Mientras tanto, recomiendo a quien haya leído esta guía practicar localizando la rutina que hay que anular para que el valor original no cambie. También puede experimentar con la edición de GameTek de este juego, donde la protección fue modificada levemente, agregándole una complicación adicional.
PS: Existe también una versión en CD-ROM que contiene el juego original combinado con los niveles del disco de expansión. Si bien en este caso la protección es distinta, al tratarse de un cd-check, la rutina de arranque es idéntica y podemos llegar al inicio del programa con los metodos aquí explicados.