Conversación entre Víctor Morilla y Miguel Angel Rodríguez Jódar, desvelando las incógnitas de la protección anti-hack utilizada en Thor para ZX Spectrum y descubriendo un truco anteriormente sin documentar.
Víctor Morilla
Recordáis las técnicas en los cargadores de basic que se utilizaban para evitar hacks en los juegos? Se me ocurrió mirar lo que hice en su día con Thor (mi videojuego de plataformas comercializado por Proein). 33 años después y soy incapaz de entender lo que hice! Randomize USR a una variable inexistente? Alguno tenéis esto más reciente? Se os ocurre qué es lo que pude hacer
Miguel Angel Rodríguez Jódar
v puede existir perfectamente aunque no la hayas definido en el código con un LET. Simplemente haz un PRINT v a ver qué te sale.
Víctor Morilla
PRINT v no muestra nada: da un variable not found. En otros cargadores, se jugaba con modificar el valor de la variable del sistema ERR SP de forma que al dar un error, se lanza la ejecución del código máquina. Pero no parece ser el caso…
César Hernández Bañó
Si. Como te dice Miguel Angel Rodríguez Jódar, esa variable v puede estar guardada en el programa. Los programas en basic de ZX spectrum graban las variables que haya definidas en memoria. Si cargas dicho programa y no haces ni un RUN ni un CLEAR, la variable estará en memoria
Miguel Angel Rodríguez Jódar
Vale, creo que ya sé por dónde van los tiros:
En el listado, los pokes «dicen» que van a 23624 y 23659, pero eso no es cierto. Van a 23613 y 23614: la variable ERR_SP. Los valores que se pokean tampoco son ceros, sino que son, respectivamente, 30 y 93, es decir, 23838. ERR_SP es a donde apunta SP en caso de error. O sea, que si se produce un error, el registro SP se cargará con 23838.
El valor de 16 bits que haya en esa dirección, será la dirección de retorno de error, que en este caso es 24094.
Cuando se carga y ejecuta el programa, el RANDOMIZE USE v genera un mensaje de error. SP se carga con el valor 23838 y hace un RET. Ese RET carga en el contador de programa el valor que hay en esa dirección, 24094, que apunta a un código máquina que se cargó pegadito al BASIC.
Ese código copia un trozo de memoria desde la zona del BASIC hasta casi el final de la RAM (el cargador), y luego lo ejecuta.
Miguel Angel Rodríguez Jódar
El cargador, ya el de verdad, pide cargar un bloque gigantesco, que ocupa toda la ROM, y hasta $FFF5. La pila se salva por poquito. Está claro que durante la carga, el cargador se sobreescribe y lo que se ve que hay tras el CALL puede no ser lo que se ejecute después…
Víctor Morilla
Sí! Un error tonto! Hice innecesariamente largo el cargador.
Miguel Angel Rodríguez Jódar
No lo vi como un error. Precisamente una de las protecciones anticopias que se usan es la de cargar bloques muy muy grandes, para de esa forma impedir que un copión se lo trague.
Víctor Morilla
Eso tiene más sentido. Es demasiado evidente que la pantalla tarda un buen rato en empezar a cargarse. Muchas gracias por toda la ayuda. Me parece que no es el primer cargador que miras 😅
Miguel Angel Rodríguez Jódar
No…. no es el primero que miro, no 😉
Miguel Angel Rodríguez Jódar
Efectivamente, tras la carga, el JP NC,$0000 se ha transformado en un JP C,$C7F3, que salta a lo que parece que es ya el comienzo del programa en sí.
Miguel Angel Rodríguez Jódar
En ese comienzo hay una cosa muy curiosa: nada más comenzar el programa, y antes de cualquier otra cosa, se comprueba si las teclas C y V están pulsadas, y si ambas lo están, entonces se guarda cierto valor en $A23E (el valor $18). ¿Un easter egg para dar vidas infinitas o invulnerabilidad quizás?
Miguel Angel Rodríguez Jódar
En $A23E hay esta rutina, que salta a cierto sitio si cierta variable en $C00F no es 0. Cuando se cambia el valor que tiene por el $18, ese «salto si no es 0» se convierte en un «salta siempre». Me juego lo que quieras a que esa variable es el contador de vidas o el de energía. Voy a poner un punto de ruptura a ver cuándo se ejecuta eso.
Miguel Angel Rodríguez Jódar
Y por supuesto, «trampeo» al programa para que ejecute esa parte, como si hubiera pulsado C y V.
Miguel Angel Rodríguez Jódar
El punto de ruptura anterior se ejecuta una vez en cada vuelta del bucle de juego principal. Le daré unas cuantas veces hasta que alguien me toque, a ver qué pasa…
Miguel Angel Rodríguez Jódar
Aja! La variable es el contador de vidas! Y cuando se cambia esa instrucción, nunca se decrementa. Me quitan energía, pero cuando llega a 0 se recarga de nuevo, y las vidas continúan siendo 5. ¿Está este easter egg documentado?
Víctor Morilla
Genia! Miguel Angel. Genial. El easter egg lo conocía, pero no creo que esté documentado. Lo que sigo sin entender es cómo apareciendo en el listado unos pokes se estaban utilizando otros distintos.
Miguel Angel Rodríguez Jódar
Ah! Eso es porque en el BASIC del ZX Spectrum, los valores numéricos se representan de dos formas distintas: una, mediante código ASCII, que es lo que ves cuando listas el programa (el 23624, 23659 y los dos 0s), y otra es la representación interna que está oculta en el listado, pero que en memoria, sigue a la representación ASCII. Mira este volcado de la memoria del cargador BASIC:
En él, he señalado un byte, el 244. Ese es el código de POKE. Justo tras él está el valor en ASCII 23624. Y justo después, aparece el byte 14, que es la marca de «número». Los 5 bytes siguientes definen el valor binario del número. Normalmente, la representación ASCII coincide con ese valor interno, pero no tiene por qué ser así, y el intérprete, de hecho, ignora la versión ASCII y se queda con la versión interna.
Esa representación interna de 5 bytes es muy sencilla, para números enteros entre -65535 y +65535: el primer byte (después del 14) es un 0, el segundo es el signo (0=positivo, 255=negativo), el tercero y cuarto, el valor en 16 bits, little endian, y el quinto es 0.
En este caso en concreto, los valores que nos interesan tras el 23624 son el 61 y el 92. Eso da: 61+256*92=23613, que es el valor que realmente se va a usar en el POKE.
Y así, todos los demás valores que se ven en el listado. Ni uno de ellos coincide con lo que se ejecuta más tarde.
Víctor Morilla
Muchísimas gracias por la explicación.
Miguel Angel Rodríguez Jódar
Entiendo que siendo el autor, lo conocieras, claro, pero… ¿lo pusiste tú motu proprio o te lo «sugirieron» desde Proein? ¿o era algún tema para depurar el juego más fácilmente y se te «olvidó» quitarlo? ¿No se publicó este cheat code en su día en la prensa especializada? ¿Lo hemos sacado a la luz AHORA?
Víctor Morilla
Lo más gracioso es que posiblemente, hasta este momento, nadie intentó hackear este código. Cuando lo escribí con 15 años no me esperaba que el primero que lo intentaría sería mi yo futuro… y que fracasaría de no ser por Miguel Angel.