Como hemos visto en las dos entregas anteriores, todo se reduce a un concepto: cambiar la memoria. Si lo hacemos cuidadosamente, podemos colocar, o bien nuestras propias instrucciones, cambiando el comportamiento del programa, o bien nuestros propios datos, de manera que podamos cambiar el número de vidas, el número de balas, el número de bombas... etc.
En esta entrega vamos a ver un ejemplo real, centrándonos en Stop the express. Vamos a seguir el proceso de creación de un cargador que nos proporcione vidas y tiempo infinito. Lo que hoy en día se conoce como cheats.
Encontrar las posiciones de memoria adecuadas y saber modificarlas, implica tener conocimientos de ensamblador y código máquina del Z80, el microprocesador dentro del ZX Spectrum. En este pequeño artículo lo haremos paso a paso, sin asumir conocimientos previos.
Comencemos el proceso de crear un cargador con Stop the Express. Tras encender el Spectrum, y colocar la cinta en el reproductor de cassette, tecleamos merge "" y pulsamos PLAY. Bueno, esto es la idea, a partir de ahora vamos a manejarnos con un emulador de Spectrum, llamado FBZX. Precisamente, nos permitirá emular insertar una cinta, ponerla en marcha, etc.
Así, pulsamos F3 y 1 para indicar que queremos cargar una cinta, el .tap o .tzx de Stop the Express que nos hayamos descargado de World of Spectrum. Teclamos merge"" como se ve en la imagen anterior. Es importante que nos fijemos en que hemos desactivado la carga rápida y la carga turbo.
Ahora, de vuelta en la pantalla de Spectrum, pulsamos ENTER para cargar la cinta, y pulsamos F6 para ponerla en marcha (virtualmente). Una vez que aparece Ok 0:1, pulsamos F5 para parar la cinta. Volvemos a pulsar ENTER para ver el código en pantalla.
Introducimos 20 y ENTER y en la línea 50 eliminamos los colores, de manera que solo nos quede la línea 20, que es la que carga la pantalla de presentación (load "" screen$), el código máquina del juego (load "" code), y la ejecución del mismo randomize usr 48096. Este último lo vamos a modificar poniendo REM delante, lo que lo transforma en un comentario.
50 load "StopExpr2" screen$: load "StopExpr3" code: rem randomize usr 48096
Esta última instrucción es interesante. Ahora le hemos puesto rem delante, con lo que la hemos transformado en un comentario y no se va a ejecutar. La pregunta ahora es: ¿cómo es que esta instrucción ejecuta el juego? La parte importante es USR 48096. Lo que sucede es que es una función, no una instrucción. Necesita una instrucción a la cual va a ser pasada por parámetro. ¿Cuál instrucción exactamente? Realmente no importa demasiado, solo la necesitamos para que evalúe sus argumentos. Concretamente, se usa RANDOMIZE, que inicializa la semilla aleatoria que alimenta a la función RND, que devuelve un valor aleatorio entre 0 y 1. Así, una vez que RANDOMIZE evalúe su parámetro, podrá inicializar la semilla aleatoria. Lo que pasa es que nunca se producirá un retorno de la función que llama al código máquina del juego, claro...
La documentación del ZX Spectrum nos dice que cuando ejecutamos un load "" code, en realidad estamos haciendo un load "" code 32768. Estamos cargando el código del juego a partir de la dirección de memoria 32768. Si tenemos en cuenta que el código tiene un tamaño de 15394 bytes, entonces sabemos que ocupará desde la dirección en memoria 32768 hasta la dirección 48162, inclusive. El resto de la memoria, desde la dirección en memoria 48163 hasta 64k (65536), queda libre.
Tener parte de la memoria libre es importante porque necesitamos espacio para poder cargar un desensamblador, sin "pisar" el programa, es decir, sin utilizar la memoria que está siendo ocupada por el programa. El desensamblador nos va a decir las instrucciones que están en memoria, sin necesidad de que interpretemos los números en ella directamente.
Hasta ahora hemos utilizado emulador el mejor Spectrum posible, el creado por Investrónica en 1986, con características mejoradas como el chip de sonido de tres canales, o la memoria de 128k. Se trata del Sinclair Spectrum + 128k. Pero Stop the express necesita un modelo de 48k, el modelo anterior, así que por si acaso, pasaremos a ese modo con la instrucción SPECTRUM. Si ahora pulsamos la tecla K, veremos que aparece la instrucción LIST en pantalla. Pulsamos ENTER y veremos de nuevo el listado.
Esta forma de manejarse con el teclado es típica del ZX Spectrum original, el de 48k. Se concibió como una manera de ahorrar memoria, y también de ayudar a los usuarios a teclear programas en BASIC. Hay muchos detractores de esta forma de operación, así como fervientes seguidores de la misma.
Ahora pulsamos R (aparece RUNen pantalla) y pulsamos ENTER. Pulsamos F6 para volver a poner la cinta en marcha. El programa cargará la pantalla de presentación, y después el bloque de código máquina del juego en sí.
Cuando termine, veremos en la parte inferior de la pantalla Ok 50:2. Aparentemente, no ha pasado nada, pero tenemos el juego en memoria. Si pulsamos V (aparece CLS), y ENTER, y K (aparece LIST), y ENTER, veremos que seguimos teniendo el cargador en memoria.
Ahora es necesario cargar el desensamblador que mencionamos. Vamos a utilizar HiSoft MONS. El primer archivo, HiSoftDevpacV3M2Mons.tap.zip ya nos vale. Lo descomprimimos, por ejemplo en el mismo directorio de descargas, con lo que nos quedamos con MONS321.tap. Esta cinta, es decir, este archivo .tap, solo tiene un programa, que es el propio desensamblador, que cargaremos con load "" code 52000. Volvemos a FBZX, pulsamos F3 y 1 para seleccionar MONS321.tap, y ESC para volver a la emulación. Pulsamos J (aparece LOAD), pulsamos Ctrl + P dos veces para obtener dos dobles comillas, y a continuación pulsamos Ctrl + Shift, y soltamos (el cursor cambia para mostrar una E parpadeante), y entonces pulsamos la tecla I (aparece CODE en pantalla). Finalmente, tecleamos 52000, para obtener load "" code 52000. Pulsamos ENTER para pasar al modo de carga, y F6 para comenzar la reproducción de la cinta. En pantalla aparecerá BYTES: MONS3M2, y realizará la carga correspondiente, hasta mostrar Ok 0:1.
El proceso de carga del juego para su desensamblado, en realidad suele hacerse desde el mismo MONS, pero para no entrar en excesivas tecnicidades, y explorar también al propio Spectrum 48k, al final he decidido hacerlo así.
Para ejecutar el desenamblador, tecleamos T(aparece RANDOMIZE), Ctrl + Shift (el cursor cambia a una E parpadeante), y L (aparece USR). Tecleamos a continuación 52000, para obtener RANDOMIZE USR 52000. Pulsamos ENTER.
Veremos brevemente un mensaje de bienvenida de MONS3, y después una pantalla con el contenido de los registros del Z80, el contenido de una zona de la memoria en hexadecimal, y un cursor parpadeante en la zona inferior de la pantalla.
Como yo no soy un experto en ensamblador del Z80, me dirijo a los pokes disponibles, que son los siguientes para vidas infinitas y tiempo infinito. A continuación aparecen las direcciones en memoria a cambiar, el valor en hexadecimal del nuevo valor a poner en memoria, y su opcode correspondiente en el Z80.
| Dirección en memoria | Valor | Significado | Hex | Opcode |
|---|---|---|---|---|
| 34464 | 183 | Vidas infinitas | B7 | OR A, A |
| 34926 | 183 | Vidas infinitas | B7 | OR A, A |
| 35257 | 183 | Vidas infinitas | B7 | OR A, A |
| 35780 | 0 | Tiempo infinito | 0 | NOP |
| 39549 | 0 | Tiempo infinito | 0 | NOP |
En Mons3, si pulsamos la combinación de teclas Shift + 1, volvemos a BASIC. Para volver a entrar en Mons3, necesitaremos volver a teclear randomize usr 52000, es decir, T, Ctrl+ Shift, L, 52000 y ENTER.
Con Ctrl+ 3, desensamblamos el programa empezando en la dirección de memoria por defecto, que al principio es 0, es decir, estaríamos desensamblando la ROM del Speccy, la que contiene el intérprete de BASIC, etc.
Con H, y un número decimal, obtenemos su valor hexadecimal equivalente. Por ejemplo, H y 52000 nos devuelve CB20, que es la posición en memoria de Mons3. Si convertimos 34464, la dirección del primer POKE, nos devuelve 86A0.
Con 'M', y un valor en memoria hexadecimal, nos pasamos a esa dirección en memoria. En lugar de utilizar directamente 86A0, utilizaremos 869B, así, con M', 869B yCtrl`+ 3, obtendremos un listado como el siguiente:
m68k
869V 1102A7 LD DE, #A702
869E 96 SUB (HL)
869F 5F LD E, A
86A0 9D SBC A, L ; este es el valor a cambiar
86A1 93 SUB E
86A2 AA XOR D
96A3 ...
En la primera columna aparece la dirección en memoria en hexadecimal. A continuación, los valores en hexadecimal para la instrucción. Hay que tener en cuenta que hay instrucciones que ocupan varios bytes. Por ejemplo, la primera en 869V es 1102A7. El valor 11 es el de la instrucción LD, concretamente LD DE, cargar en el registro DE el valor A702, que como vemos aparece como 02A7, ya que el Z80 codifica los valores como LSB (Least Significan Byte), es decir, coloca primero el byte menos significativo de un valor en 16 bits. El registro DE es el formado por el par de registros de 8 bits D y E, para conformar un registro de 16 bits. En cambio, en la dirección 86A0 tenemos 9D, una instrucción que ocupa un solo byte y que significa que se debe restar el contenido del registro L + el bit de acarreo, del contenido del registro A. El resultado se guarda en A.
Si en esta posición ponemos en cambio el byte B7, entonces estaremos haciendo un OR del valor del registro A con el valor del registro A. La operación OR comprueba un valor en binario con otro, y pone a 1 aquellos pares de valores que contengan 0 y 1, o 1 y 1. Por ejemplo, si tomamos un valor en binario como 0110, que es 6 en binario, con 1100, que es 12, obtendremos 1110, que es 14. Sí, ¡se puede utilizar para sumar! ¿Pero, qué pasa si hacemos un OR de un valor consigo mismo? Pues nada. El valor se queda como estaba. Y eso es lo que quiere conseguir el POKE: eliminar la resta para que no se reste una vida. Pero, a la vez, deja el registro A y el acarreo como tiene que estar para poder continuar la ejecución sin que esta se vea afectada.
De acuerdo. La dirección en memoria 36780 (8FAC), contiene una instrucción que debemos poner a 0 para que el tiempo sea infinito. El opcode 0 es NOP, es decir, no hacer nada. Si nos fijamos, en esa posición hay otro SBC, otra resta teniendo en cuenta el acarreo. Si la ponemos a 0, simplemente no se hace. En este caso, no es necesario dejar el registro A y el acarreo de ninguna manera especial para que la ejecución continúe correctamente.
Y bueno, este es el proceso que seguiría un hacker de 8 bits para encontrar los POKEs del juego. Buscaríamos por ejemplo el valor 3, descartando todos los valores hasta encontrar el número de vidas inicial en una dirección de memoria concreta. Después buscar restas de esa posición en memoria, buscando por el opcode SUB o SBC hasta encontrar la instrucción que resta la vida, y encontrar el valor adecuado que consigue que se haga la resta mientras a la vez consigue que continúe la ejecución sin errores. Y cada error, supone cargar el Mons3 desde cinta, y a continuación el juego desde cinta. Tiene mérito, ¿eh?
Y ahora es necesario crear el cargador del juego. Es decir, la parte BASIC que carga el juego. Nos habíamos quedado con esto:
bbcbasic
20 load "StopExpr2" screen$: load "StopExpr3" code: rem randomize usr 48096
Vamos a pasarlo a lo siguiente. Tenemos que cambiar los valores después de cargar el juego, y antes de ejecutarlo. Podemos hacerlo como aparece a continuación, creando el archivo cargador.bas:
bbcbasic
10 load "StopExpr2" screen$
20 load "StopExpr3" code
30 cls:print "Stop the Express cracked"
40 input "Vidas infinitas (s/n):";v$
50 if v$(1)<>"n" then poke 34464, 183:poke 34926, 183:poke 35257,183
60 input "Tiempo infinito (s/n):";t$
70 if t$(1)<>"n" then poke 35780, 0: poke 39549, 0
100 randomize usr 48096
Nos podemos descargar BasinC, y ejecutarlo (en linux, necesitaremos Wine). Cargamos el archivo cargador.bas en BasinC, y seleccionamos File >> Export as tap, guardándolo como cargador.tap.
Ya solo queda volver FBZX, y pulsar F3 para seleccionar una cinta, escoger cargador.tap, pulsar ESC e introducir SPECTRUM, y después pulsar J, Ctrl + P dos veces y ENTER para ejecutar load "", pulsando entonces F6. Al terminar la carga, pulsamos R y ENTER para ejecutar el programa. Seleccionamos la cinta del juego original con F3 y 1, y al pulsar ESC y F6, el juego se cargará saltándose el BASIC original, y preguntándonos si queremos vidas y tiempo infinito. Al terminar las preguntas, el juego se ejecutará modificado.
Enhorabuena, ¡eres un hacker!



Top comments (0)