Desencriptación de juegos Sega System 16 con CPU FD1094

1.     Introducción

Como coleccionista, hace tiempo que me interesé por el tema de las baterías suicidas en placas de juegos de recreativa. Si iba a coleccionarlas quería estar seguro de que pasado cierto tiempo no se morirían.

En esta página: http://www.arcadecollecting.com/dead/dead.html hay mucha información relativa a cómo eliminar las baterías suicidas de diversos modelos de placas. Sin embargo, en el caso de los juegos Sega System 16 (http://www.system16.com/) hay muy poca información. Existen varios sets de ROM desencriptadas (podéis verlo en el primer enlace), pero en muchas ocasiones solamente funcionan con una versión concreta del juego, y los juegos System 16 solían tener varias versiones.

Por ello decidí embarcarme en un proyecto ambicioso: intentar desencriptar yo mismo las ROM de los juegos. En realidad no parto de cero. Evidentemente el trabajo difícil está hecho ya por la gente del equipo de desarrollo de MAME. MAME es capaz de desencriptar el código al vuelo, según lo va ejecutando. Como el código fuente de MAME es abierto, se puede consultar la parte que realiza la desencriptación y reutilizarla para nuestros propósitos.

Y lo he conseguido, he desencriptado mi primer juego: Wonder Boy III – Monster Lair, lo he probado, y funciona perfectamente.

En el presente tutorial se trata de dar una visión muy general de cómo funciona el sistema de protección de los juegos Sega System 16, y de cómo podemos eliminar las baterías suicidas de dichos juegos.

 

2.     Fundamentos

No voy a explicar en detalle cómo funciona el sistema de batería suicida de los juegos System 16, principalmente porque no lo conozco a fondo. Pero sí es importante conocer su base de funcionamiento para embarcarse en la aventura que nos concierne.

Muchos juegos de este tipo sustituían el procesador principal (un 68000 de Motorola) por un procesador propio (un FD1094 de Hitachi). La diferencia entre ambos, aún siendo pequeña, era muy importante. Ambos ejecutan el mismo código, pero el FD1904 espera leer el código encriptado, con lo cual lo desencripta antes de ejecutarlo.

Eso nos lleva a la batería suicida: la clave para desencriptar el código se almacena en una memoria de 8K integrada en el FD1094. Es una memoria RAM (volátil), con lo cual necesita de una pila adicional (también integrada) para alimentar dicha memoria.  Si la batería se gasta, la RAM se borra, con lo cual el FD1094 empieza a desencriptar código con una clave errónea, y el juego deja de funcionar.

Cambiar la batería del FD1094 sin que en ningún momento deje de alimentarse la RAM es una odisea. Lo mejor es sustituir el FD1094 por un 68000 normal, y reprogramar las EPROM correspondientes con código desencriptado. Fácil, ¿verdad? Pues ahora “sólo” falta desencriptar dicho código. Para ello utilizaremos las rutinas de MAME como base.

Es extraño, pero hay muy poca información al respecto. Y eso que, como digo, el trabajo duro lo ha realizado ya el equipo de desarrollo de MAME. Existe por ahí un tipo que, con un método (supongo) similar al que aquí vamos a describir, se dedica a vender ROM desencriptadas a 40 dólares (ó 25 libras). Tiene su propia página web, de la cual no vamos a hacer publicidad. Llamadme altruista, pero estas cosas deberían estar al servicio de la comunidad arcade, y eso es lo que voy a tratar de hacer.

La única página que he encontrado en la cual se dan pistas sobre el proceso de desencriptado (y sin la cual yo no hubiera tenido éxito; gracias, Charles MacDonald) es la siguiente:  http://cgfm2.emuviews.com/fdconv.php. Os recomiendo encarecidamente su lectura.

Ojo, no desesperéis en la primera lectura. Yo la he tenido que leer muchas veces, complementando dicha lectura con otras (ensamblador del 68000, fundamentos del 68000) hasta comprenderla a fondo. Pero es la clave para el desencriptado.

Y llegados a este punto tengo que avisar que este documento por sí solo no basta para aprender a desencriptar código del FD1094. Se requieren otros conocimientos, como pueden ser:

·         Manejo y edición de ficheros binarios (necesario)

·         Fundamentos básicos de lenguaje ensamblador del 68000 (necesario)

·         Fundamentos básicos de programación en C++ (muy recomendable)

·         Fundamentos básicos de compilación de MAME (muy recomendable)

Con esto no quiero desanimar a nadie, pero no quiero que nadie se engañe. Además aprovecho para decir que no me siento obligado a responder ningún tipo de pregunta acerca de las cuestiones arriba planteadas, entre otras cosas porque yo conozco al respecto lo justo para poder llevar a cabo la desencriptación que aquí se describe. Tampoco me siento obligado a contestar preguntas relativas a ese proceso de desencriptación. Por favor, comprendedlo.

 

3.     Fichero fd1094.c y programa de desencriptación

Ese fichero es la clave de este procedimiento. Es un fichero C++ incluido en el código fuente de MAME que implementa dos funciones capaces de desencriptar el código asociado al procesador FD1094, a saber:

·         fd1094_set_state

·         fd1094_decode

Este fichero (http://mamedev.org/source/src/mame/machine/fd1094.c.html) de nuevo es de lectura obligada, ya que al principio contiene una descripción exhaustiva de cómo funciona el proceso de desencriptado. Voy a tratar de explicar muy brevemente cómo funciona dicho proceso:

·     La clave de encriptación se almacena en una RAM de 8K, con lo cual tiene 8192 bytes.

·     La desencriptación se hace a nivel de Word, que en un 68000 corresponde a 2 bytes. Es decir, se desencripta de 2 bytes en 2 bytes.

·     Para desencriptar la palabra (Word) de la posición de memoria “A” se utilizan 4 bytes de la clave (de 8K): los bytes 2º, 3º y 4º (modificados por el estado), más un byte adicional que va rotando, y que depende de la dirección de memoria “A”.

·     El estado (que modifica los bytes antes mencionados de la clave) va cambiando. El estado inicial, tras un reset, es el “0” ($0). Mediante la instrucción:

CMPI.L  #$00xxFFFF, D0

el programa cambia el estado del procesador a “xx” ($xx).

·     Cuando se produce una interrupción el procesador pasa a estado $200 (en hexadecimal), y cuando se retorna de interrupción (con la instrucción RTE) o cuando se ejecuta la instrucción:

CMPI.L  #$0300FFFF, D0

el procesador retorna al estado que tenía antes de la interrupción.

·     El FD1094 (y el 68000) distingue entre espacio de código y espacio de datos, de forma que solamente lo que es código está encriptado, y la parte de datos no se encripta. Tenemos que tener en cuenta esto para, a la hora de desencriptar, aplicar el algoritmo únicamente al código.

En principio este tostón teórico no nos debe preocupar mucho. Simplemente debemos tener en cuenta que, para desencriptar una determinada palabra de código, debemos poner el procesador en el estado correspondiente (con la función “fd1094_set_state”) y debemos desencriptar dicha palabra (con la instrucción “fd1094_decode”)

A pesar de que hacía 15 años que no tocaba el C++ (lo estudié allá por el año 95 en segundo de carrera) me ha tocado refrescarlo, y he creado una subrutina capaz de leer el código recursivamente e ir desencriptándolo utilizando las dos funciones antes mencionadas. Con unos mínimos conocimientos de C++ es tarea sencilla.

A continuación os pongo el código fuente que he desarrollado. Por favor, tened en cuenta que mis conocimientos de C++ son limitadísimos, con lo cual estoy convencido de que este código es infinitamente mejorable.

#include <iostream>

#include <fstream>

#include "fd1094.h"

using namespace std;

 

#define FD1094_STATE_RESET 0x0100

#define FD1094_STATE_IRQ   0x0200

#define FD1094_STATE_RTE   0x0300

#define BIT(x,n) (((x)>>(n))&1)

#define BITSWAP16(val,B15,B14,B13,B12,B11,B10,B9,B8,B7,B6,B5,B4,B3,B2,B1,B0) \

             ((BIT(val,B15) << 15) | \

              (BIT(val,B14) << 14) | \

              (BIT(val,B13) << 13) | \

              (BIT(val,B12) << 12) | \

              (BIT(val,B11) << 11) | \

              (BIT(val,B10) << 10) | \

              (BIT(val, B9) <<  9) | \

              (BIT(val, B8) <<  8) | \

              (BIT(val, B7) <<  7) | \

              (BIT(val, B6) <<  6) | \

              (BIT(val, B5) <<  5) | \

              (BIT(val, B4) <<  4) | \

              (BIT(val, B3) <<  3) | \

              (BIT(val, B2) <<  2) | \

              (BIT(val, B1) <<  1) | \

              (BIT(val, B0) <<  0))

#define ARRAY_LENGTH(x)           (sizeof(x) / sizeof(x[0]))

#define FLIPENDIAN_INT16(x)       (((((UINT16) (x)) >> 8) | ((x) << 8)) & 0xffff)

#define FALSE 0

#define TRUE 1

 

typedef unsigned char UINT8;

typedef unsigned short UINT16;

 

int main()

{

       char *clave;

       char *codigo1, *codigo2, *codigo;

       UINT16 *resultado;

 

       int offset, estado, estado_final;

       int long_clave, long_codigo;

 

       cout << "Offset de inicio (en bytes): ";

       std::cin>> std::hex >> offset;

       cout << "Estado actual del procesador: ";

       std::cin>> std::hex >> estado;

 

       ifstream fichero_clave("clave", ios::in|ios::binary|ios::ate);

       long_clave = (int) fichero_clave.tellg();

       fichero_clave.seekg (0, ios::beg);

       clave = new char [long_clave];

       fichero_clave.read (clave, long_clave);

       fichero_clave.close();

 

       ifstream fichero_codigo1("codigo1", ios::in|ios::binary|ios::ate);

       long_codigo = (int) fichero_codigo1.tellg();

       fichero_codigo1.seekg (0, ios::beg);

       codigo1 = new char [long_codigo];

       codigo = new char [long_codigo*2];

       resultado = new UINT16 [long_codigo];

       fichero_codigo1.read (codigo1, long_codigo);

       fichero_codigo1.close();

      

       ifstream fichero_codigo2("codigo2", ios::in|ios::binary);

       codigo2 = new char [long_codigo];

       fichero_codigo2.read (codigo2, long_codigo);

       fichero_codigo2.close();

 

       for(int i = 0; i < long_codigo; i++)

       {

             codigo[i*2] = codigo1[i];

             codigo[i*2+1] = codigo2[i];

       };

      

       UINT8* clave_final;

       clave_final = reinterpret_cast<UINT8*>(clave);

       UINT16* codigo_final;

       codigo_final = reinterpret_cast<UINT16*>(codigo);

 

       for(int i = 0; i < offset/2; i++) resultado[i] = FLIPENDIAN_INT16(codigo_final[i]);

      

       estado_final = fd1094_set_state(clave_final, estado);

 

       for(int i = offset/2; i < long_codigo; i++) resultado[i] = FLIPENDIAN_INT16(fd1094_decode(i, codigo_final[i], clave_final, i < 5));

            

       char *resultado_final;

       resultado_final = reinterpret_cast<char*>(resultado);

 

       ofstream fichero_salida1("descodificado", ios::out|ios::binary|ios::trunc);

       fichero_salida1.write (resultado_final, long_codigo*2);

       fichero_salida1.close();

 

       ofstream fichero_salida2("codigo", ios::out|ios::binary|ios::trunc);

       fichero_salida2.write (codigo, long_codigo*2);

       fichero_salida2.close();

};

 

4.     Descripción del procedimiento

En primer lugar veamos las herramientas que vamos a necesitar:

·     Un programa como el que acabamos de mencionar capaz de, en función de al menos dos parámetros básicos (el estado del procesador y la dirección de inicio de desencriptación), generar el código desencriptado. Cada cuál que lo desarrolle como más le guste. El mío por ejemplo es tan sencillo que ni siquiera pregunta por el nombre de los archivos, supone que estos tienen un nombre determinado.

·     Un editor hexadecimal.  Personalmente a mí me gusta utilizar el frhed (http://frhed.sourceforge.net/), gratuíto, y también el Hex Comparison, que permite comparar dos ficheros binarios (http://exeicon.com/hex-comparison/).

·     Un desensamblador para 68000. Yo utilizo el 68kd (http://www.trzy.org/), de Bart Trzynadlowski, basado en el que utiliza MAME en el modo debug, creado por Aaron Giles.

·     El código fuente de MAME (http://mamedev.org/release.html) y un compilador para el mismo, como puede ser mingw-mame (http://mamedev.org/tools/). Para hacer pruebas lo mejor es modificar determinados parámetros de MAME, y compilarlo a nuestro gusto.

Con esos requisitos, vamos a ponernos manos a la obra. Describiremos paso a paso los puntos que hay que tratar. Todos los valores numéricos que se den con el símbolo “$” delante se supone que son hexadecimales.

 1º)  En primer lugar, hay que determinar qué ROM de nuestro juego son las que corresponden al código del 68000. Para ello podemos acudir al driver segas16.c del fuente de MAME (http://mamedev.org/source/src/mame/drivers/segas16b.c.html). Ahora, o bien leyendo esas EPROMs con un lector o bien cogiéndolas directamente de las ROM ya volcadas para MAME, las guardamos en el PC.

                Por otro lado hay que obtener la clave para desencriptar. Para ello tenemos que identificar qué ROM de MAME corresponde a nuestra versión del juego, y extraer de dicha ROM el fichero .key de 8K.

                Hay que tener en cuenta que el código se almacena de forma entrelazada en, generalmente, una pareja de EPROM. Cada palabra (Word) de 16 bits se almacena dividida en dos bytes, un byte en una EPROM y otro byte en otra EPROM. De manera que la palabra de la dirección “A” almacena su byte más significativo en la dirección “A” de una EPROM y su byte menos significativo en la dirección “A” de la otra EPROM. Es necesario volver a entrelazar ambos archivos para obtener el código. Yo lo que hago es pasarle directamente los dos ficheros a mi programa en C++ que desencripta, y él se encarga de entrelazarlos.

 2º)  Las $400 primeras palabras del programa constituyen la tabla de vectores. Las 2 primeras palabras indican el valor del registro SP (stack pointer) y las 2 siguientes el valor del PC (program counter), donde comenzará a ejecutarse el código. A pesar de que la tabla de punteros en principio forma parte del área de datos y no está encriptada, estos dos punteros sí se consideran parte del programa, con lo que están encriptados, en el estado $0. No es necesario desencriptarlos, ya que los valores desencriptados los podemos consultar en el fichero “fd1094.c”.

 3º)  El siguiente paso es fijarse en el resto de punteros de la tabla de vectores (todos ellos de 8 bytes). Es necesario apuntar todos los que sean diferentes, ya que son punteros a las distintas excepciones e interrupciones, que al fin y al cabo son subrutinas (especiales) que se desencriptarán utilizando el estado $200, como se dijo con anterioridad. Con ello determinaremos cuántas subrutinas de tipo excepción existen.

 4º)  Con esa información comienza el trabajo duro. En primer lugar ya sabemos dónde empieza el programa, porque nos lo indica el registro PC. A partir de ese punto podemos empezar a desencriptar el código, con el estado $0 inicialmente. A continuación será necesario desensamblar el código obtenido, e inspeccionarlo manualmente. Hay que prestar especial atención a dos tipos de instrucciones:

·     Las instrucciones de salto, ya que pueden redirigir el programa a otra zona distinta a la que estamos inspeccionando, y tendremos que tener en cuenta dicha zona para desencriptarla a continuación.

·     Las instrucciones de tipo CMPI.L  #$00xxFFFF, D0. Como dijimos estas instrucciones modifican el estado, con lo cual a partir de ellas observaremos que el código desensamblado no tiene sentido: es necesario volver a desencriptar tras esa instrucción con el nuevo estado.

 5º)  Por otro lado es necesario desencriptar también las subrutinas de interrupción. Sabemos los puntos en los que empiezan gracias a la tabla de vectores. Las desencriptaremos en el estado $200, y desensamblaremos el código obtenido para inspeccionarlas. Generalmente cada rutina terminará con una instrucción RTE, momento en el cual se regresa al punto original de salto y se restaura el estado anterior.

 6º)  Con este procedimiento vamos a obtener distintos fragmentos de código desencriptado, que posteriormente será necesario ensamblar en un único fichero. Puede parecer complicado, pero no lo es demasiado. Las distintas partes del código y de las interrupciones suelen ir separadas por multitud de $FFFF, y el área de datos suele estar al final del código, sin mezclarse con éste.

 

5.     Ejemplo con un caso real 

Veamos un ejemplo de lo dicho hasta ahora, con el juego Wonder Boy III – Monster Lair en su versión Japonesa, que es el primer juego que he conseguido desencriptar. El código de este juego se almacena en las EPROM de las posiciones (A1, A2, A4 y A5). Son 4 en lugar de 2 (lo más usual) porque la ROM A2 es continuación de la A1, y la A5 continuación de la A4 (por lo tanto hay que unirlas). Entrelazando A1-A2 con A4-A5 vemos que el aspecto inicial (sin desencriptar) de la tabla de vectores es el siguiente:

00000  b2 f7 89 97 af 59 05 00 00 00 04 00 00 00 04 06
00010  00 00 04 0c 00 00 04 12 00 00 04 18 00 00 04 1e
00020  00 00 04 24 00 00 04 2a 00 00 04 3c 00 00 04 3c
00030  00 00 04 42 00 00 04 42 00 00 04 42 00 00 04 30
00040  00 00 04 42 00 00 04 42 00 00 04 42 00 00 04 42
00050  00 00 04 42 00 00 04 42 00 00 04 42 00 00 04 42
00060  00 00 04 36 00 00 04 3c 00 00 04 3c 00 00 04 3c
00070  00 00 05 b0 00 00 04 3c 00 00 04 3c 00 00 04 3c
00080  00 00 06 90 00 00 06 c0 00 00 06 ce 00 00 06 e2
00090  00 00 06 fa 00 00 04 3c 00 00 04 3c 00 00 04 3c
000a0  00 00 04 3c 00 00 04 3c 00 00 04 3c 00 00 04 3c
000b0  00 00 04 3c 00 00 04 3c 00 00 04 3c 00 00 04 3c
000c0  00 00 04 42 00 00 04 42 00 00 04 42 00 00 04 42
000d0  00 00 04 42 00 00 04 42 00 00 04 42 00 00 04 42
000e0  00 00 04 42 00 00 04 42 00 00 04 42 00 00 04 42
000f0  00 00 04 42 00 00 04 42 00 00 04 42 00 00 04 42
00100  00 00 04 3c 00 00 04 3c 00 00 04 3c 00 00 04 3c
00110  00 00 04 3c 00 00 04 3c 00 00 04 3c 00 00 04 3c
:

:

003d0  00 00 04 3c 00 00 04 3c 00 00 04 3c 00 00 04 3c
003e0  00 00 04 3c 00 00 04 3c 00 00 04 3c 00 00 04 3c
003f0  00 00 04 3c 00 00 04 3c 00 00 04 3c 00 00 04 3c

En verde se han resaltado el SP y el PC, que están encriptados. Del fichero fd1094.c de MAME (o desencriptando el código desde el inicio con estado $0) obtenemos que sus valores reales deberán ser $ffffff7e y $00000500 respectivamente. El resto de la tabla de vectores no se desencripta.

En amarillo se resaltan los vectores únicos que se pueden identificar, y que hacen referencia a subrutinas de interrupción, que habrá que examinar y desencriptar utilizando el estado $200. Estos son $400, $406, $40c, $412, $418, $41e, $424, $42a, $430, $436, $43c, $442, $5b0, $690, $6c0, $6cE, $6e2 y $6fa. Claramente vemos que, como el código empieza en $500 (PC), es muy probable que las subrutinas de interrupción se agrupen de forma consecutiva en dos grandes grupos: las que van desde la que empieza en $400 hasta la que empieza en $442, y las que van desde la que empieza en $5b0 hasta la que empieza en $6fa.

La rutina más importante será la que se almacena en la posición $070, que en nuestro caso es la que se almacena en $5b0. Esta rutina tiene que ver con el refresco de pantalla. Será probablemente la más larga, y a la que más atención tengamos que prestarle.

Llegados a este punto lo lógico sería empezar a desencriptar el código a partir de su punto de inicio ($500), pero eso nos dejaría un hueco entre $400 y $500. De la tabla de vectores deducimos que en ese rango hay ciertas subrutinas de interrupción, y si analizamos el código sin desencriptar vemos que entre las posiciones $448 y $4ff hay un separador (valores $FFFF). Por lo tanto desencriptamos únicamente entre $400 y $448, con estado $200 (por ser interrupciones). Obtenemos lo siguiente:

0x00000400: 0x103C 0x0002             MOVE.B   #0x2,D0

0x00000404: 0x60FA                    BRA.S    *-0x4 [0x400]

0x00000406: 0x103C 0x0003             MOVE.B   #0x3,D0

0x0000040A: 0x60FA                    BRA.S    *-0x4 [0x406]

0x0000040C: 0x103C 0x0004             MOVE.B   #0x4,D0

0x00000410: 0x60FA                    BRA.S    *-0x4 [0x40C]

0x00000412: 0x103C 0x0005             MOVE.B   #0x5,D0

0x00000416: 0x60FA                    BRA.S    *-0x4 [0x412]

0x00000418: 0x103C 0x0006             MOVE.B   #0x6,D0

0x0000041C: 0x60FA                    BRA.S    *-0x4 [0x418]

0x0000041E: 0x103C 0x0007             MOVE.B   #0x7,D0

0x00000422: 0x60FA                    BRA.S    *-0x4 [0x41E]

0x00000424: 0x103C 0x0008             MOVE.B   #0x8,D0

0x00000428: 0x60FA                    BRA.S    *-0x4 [0x424]

0x0000042A: 0x103C 0x0009             MOVE.B   #0x9,D0

0x0000042E: 0x60FA                    BRA.S    *-0x4 [0x42A]

0x00000430: 0x103C 0x000F             MOVE.B   #0xF,D0

0x00000434: 0x60FA                    BRA.S    *-0x4 [0x430]

0x00000436: 0x103C 0x0018             MOVE.B   #0x18,D0

0x0000043A: 0x60FA                    BRA.S    *-0x4 [0x436]

0x0000043C: 0x103C 0x0080             MOVE.B   #0x80,D0

0x00000440: 0x60FA                    BRA.S    *-0x4 [0x43C]

0x00000442: 0x103C 0x0090             MOVE.B   #0x90,D0

0x00000446: 0x60FA                    BRA.S    *-0x4 [0x442]

 

Como vemos hemos descodificado de un plumazo 12 subrutinas de interrupción. He resaltado la primera, pero son todas iguales. En realidad son un bucle infinito, con lo cual si durante el desarrollo del juego se produce una interrupción de este estilo, el juego se colgará.

A continuación, y tras el relleno de $FFFF, comenzamos a desencriptar el código a partir de $500 en el estado $0. Obtenemos este resultado:

0x00000500: 0x46FC 0x2700             MOVE     #0x2700,SR

0x00000504: 0x0C80 0x0077 0xFFFF      CMPI.L   #0x77FFFF,D0

0x0000050A: 0xF549                    UNRECOGNIZED

0x0000050C: 0xF752                    UNRECOGNIZED

0x0000050E: 0xBEA3                    CMP.L    -(A3),D7

0x00000510: 0xBC2D 0x802C             CMP.B    (-0x7FD4,A5),D6

0x00000514: 0xFFFF                    UNRECOGNIZED

     :

 

Como vemos hay un par de instrucciones con sentido, y luego a partir de la dirección $50A comienza código irreconocible. Es lógico: la instrucción CMPI.L  #$0077FFFF, D0 cambia el estado a $77, con lo cual a partir de ahí ya no se utiliza el estado $0, sino el $77 para desencriptar. Por tanto desencriptamos a partir de $50A con ese estado, y obtenemos:

0x0000050A: 0x41F9 0x00DF 0x0021      LEA      0xDF0021,A0

0x00000510: 0x0C80 0x0010 0xFFFF      CMPI.L   #0x10FFFF,D0

0x00000516: 0xF78A                    UNRECOGNIZED

     :

 

Una instrucción más, y un nuevo cambio de estado, al estado $10. Por tanto debemos seguir desencriptando a partir de la dirección $516 con el estado $10. El resultado es:

0x00000516: 0x10FC 0x0001             MOVE.B   #0x1,(A0)+

0x0000051A: 0x5248                    ADDQ.W   #1,A0

0x0000051C: 0x10FC 0x0000             MOVE.B   #0x0,(A0)+

0x00000520: 0x5248                    ADDQ.W   #1,A0

0x00000522: 0x10FC 0x0001             MOVE.B   #0x1,(A0)+

0x00000526: 0x5248                    ADDQ.W   #1,A0

0x00000528: 0x10FC 0x0002             MOVE.B   #0x2,(A0)+

0x0000052C: 0x5248                    ADDQ.W   #1,A0

0x0000052E: 0x10FC 0x0001             MOVE.B   #0x1,(A0)+

0x00000532: 0x5248                    ADDQ.W   #1,A0

0x00000534: 0x10FC 0x0004             MOVE.B   #0x4,(A0)+

0x00000538: 0x5248                    ADDQ.W   #1,A0

0x0000053A: 0x10FC 0x0000             MOVE.B   #0x0,(A0)+

0x0000053E: 0x5248                    ADDQ.W   #1,A0

0x00000540: 0x10FC 0x00FF             MOVE.B   #0xFF,(A0)+

0x00000544: 0x5248                    ADDQ.W   #1,A0

0x00000546: 0x10FC 0x0004             MOVE.B   #0x4,(A0)+

0x0000054A: 0x5248                    ADDQ.W   #1,A0

0x0000054C: 0x10FC 0x0044             MOVE.B   #0x44,(A0)+

0x00000550: 0x5248                    ADDQ.W   #1,A0

0x00000552: 0x10FC 0x000D             MOVE.B   #0xD,(A0)+

0x00000556: 0x5248                    ADDQ.W   #1,A0

0x00000558: 0x10FC 0x0040             MOVE.B   #0x40,(A0)+

0x0000055C: 0x5248                    ADDQ.W   #1,A0

0x0000055E: 0x10FC 0x0000             MOVE.B   #0x0,(A0)+

0x00000562: 0x5248                    ADDQ.W   #1,A0

0x00000564: 0x10FC 0x0084             MOVE.B   #0x84,(A0)+

0x00000568: 0x5248                    ADDQ.W   #1,A0

0x0000056A: 0x10FC 0x0000             MOVE.B   #0x0,(A0)+

0x0000056E: 0x5248                    ADDQ.W   #1,A0

0x00000570: 0x10FC 0x00C4             MOVE.B   #0xC4,(A0)+

0x00000574: 0x0C80 0x0013 0xFFFF      CMPI.L   #0x13FFFF,D0

     :

 

Un nuevo conjunto de instrucciones, y un nuevo cambio de estado al valor $13. Este será el estado definitivo en el que se descodifique el resto del juego. Es típico que el procesador pase por una serie de estados iniciales, y luego ya permanezca en un mismo estado hasta el final (salvo para las interrupciones). Descodificamos a partir de $57a en el estado $13:

0x0000057A: 0xC0C0                        MULU.W   D0,D0

0x0000057C: 0x13FC 0x0000 0x00C4 0x0001   MOVE.B   #0x0,0xC40001

0x00000584: 0xC0C0                        MULU.W   D0,D0

0x00000586: 0x13FC 0x00FF 0x00DF 0x0007   MOVE.B   #0xFF,0xDF0007

0x0000058E: 0x303C 0x0FFF                 MOVE.W   #0xFFF,D0

0x00000592: 0x41F8 0xC000                 LEA      0xC000,A0

0x00000596: 0x4298                        CLR.L    (A0)+

0x00000598: 0x51C8 0xFFFC                 DBF      D0,*-0x2 [0x596]

0x0000059C: 0x3E3C 0x0000                 MOVE.W   #0x0,D7

0x000005A0: 0x4DF9 0x0000 0x1800          LEA      0x1800,A6

0x000005A6: 0x4E40                        TRAP     #0x0

0x000005A8: 0x46FC 0x2000                 MOVE     #0x2000,SR

0x000005AC: 0x6000 0x0176                 BRA      *+0x178 [0x724]

 

Llegados a la dirección $5ac nos damos cuenta de que tenemos que parar por un doble motivo: por un lado hemos llegado a una instrucción de salto, que nos redirige a la dirección $724, y por otro lado llegamos a la posición $5b0 donde, si recordamos, empezaba la subrutina asociada a la interrupción especial. Por eso seguiremos desencriptando a partir de dicha dirección, pero utilizando ahora el estado $200:

0x000005B0: 0x46FC 0x2700             MOVE     #0x2700,SR

0x000005B4: 0x0C80 0x0300 0xFFFF      CMPI.L   #0x300FFFF,D0

     :

 

En este caso vemos que se ejecuta una instrucción, y de nuevo un cambio de estado. El estado $300 en realidad tiene el mismo efecto que una instrucción de retorno de excepción: restaura el estado que había previamente a llamar a la interrupción. En nuestro caso el estado $13. Por tanto seguimos desencriptando, pero ahora con estado $13:

0x000005BA: 0x48E7 0xFFFE             MOVEM.L  D0-D7/A0-A6,-(A7)

0x000005BE: 0x4238 0xC90E             CLR.B    0xC90E

0x000005C2: 0x0838 0x0000 0xC063      BTST     #0x0,0xC063

0x000005C8: 0x6608                    BNE.S    *+0xA [0x5D2]

0x000005CA: 0x6100 0x020E             BSR      *+0x210 [0x7DA]

0x000005CE: 0x6100 0x03A8             BSR      *+0x3AA [0x978]

0x000005D2: 0x41F8 0xFF80             LEA      0xFF80,A0

     :

     :

0x00000B58: 0x0138 0xC000             BTST     D0,0xC000

0x00000B5C: 0x67F8                    BEQ.S    *-0x6 [0xB56]

0x00000B5E: 0x10BC 0x0001             MOVE.B   #0x1,(A0)

0x00000B62: 0x4E75                    RTS

 

Tras esta instrucción de retorno de subrutina observamos que empieza a desencriptarse código no reconocible. Como no parece haber ningún cambio de estado, lo mejor es echar un vistazo al código original en esas posiciones. Si lo hacemos veremos que en realidad en la posición de memoria siguiente ($b63) comienza un área de relleno ($FFFF) que ocupa hasta la posición $17ff. Esa área nunca se va a utilizar, con lo cual podemos desencriptarla o dejarla como está.

A partir de la posición $1800 comienza de nuevo lo que parece código. No estamos seguros porque en ningún punto de lo que llevamos de programa vemos un salto a esa posición, pero existen formas en programación de provocar un salto indirectamente, como por ejemplo meter en la pila una dirección y ejecutar un RTS (el programa buscará la dirección de retorno de subrutina en la pila). Por eso suponemos que a partir de la dirección $1800 debemos seguir desencriptando en el estado $13. Y acertamos, porque obtenemos código legible:

0x00001800: 0x41F8 0xEC00             LEA      0xEC00,A0

0x00001804: 0x303C 0x009F             MOVE.W   #0x9F,D0

0x00001808: 0x4298                    CLR.L    (A0)+

0x0000180A: 0x51C8 0xFFFC             DBF      D0,*-0x2 [0x1808]

0x0000180E: 0x41F9 0x0044 0x0000      LEA      0x440000,A0

    :

0x00001856: 0x4EB9 0x0001 0x5A32      JSR      0x15A32

    :

0x0001610A: 0x3140 0x007E             MOVE.W   D0,(0x7E,A0)

0x0001610E: 0x51CB 0xFFBE             DBF      D3,*-0x40 [0x160CE]

0x00016112: 0xD0FC 0x0080             ADDA.W   #0x80,A0

0x00016116: 0x51CA 0xFFB2             DBF      D2,*-0x4C [0x160CA]

0x0001611A: 0x4E75                    RTS

 

En este bloque de código debemos prestar atención a las instrucciones de salto, para hacernos una idea del tamaño del mismo. He marcado una instrucción que provoca un salto a la dirección $15a32, lo que nos da una pista de que en ese punto sigue habiendo código. En realidad, por simple inspección, vemos que tras la instrucción RTS de la dirección $1611a hay algunos NOP, y un bloque de relleno de $FFFF.

En $165a0 termina el relleno, y lo que hay a continuación parecen datos, con lo cual no deben desencriptarse. Para asegurarse lo mejor es intentar desencriptar con el estado principal ($13), y comprobar que se obtienen un código ilegible. Además si abrimos el fichero con un editor hexadecimal vemos que en esas posiciones hay textos del juego, lo que indica que efectivamente son datos (y no están cifrados).

Ya solo queda unir los diferentes fragmentos que hemos ido creando (código desencriptado y datos sin modificar), generando un único archivo. Ese archivo se dividirá entre las distintas ROM de forma inversa a como los archivos de las ROM originales se unieron para obtener el código encriptado, y con un programador grabaremos esas ROM en sus correspondientes EPROM.

Por último sustituimos el procesador FD1094 original por un 68000 y las EPROM originales por las desencriptadas, y si todo ha ido bien el juego debería funcionar sin problemas.

 

6.     Método de pruebas

Como el proceso de desencriptación es en gran parte manual, está muy expuesto a posibles errores humanos, con lo cual es muy probable que la primera versión de código desencriptado que obtengamos no funcione. Estaréis de acuerdo en que es un engorro andar borrando EPROM, reprogramándolas y probando en la recreativa por cada versión que vayamos generando. Os voy a explicar el método que yo utilizo para probar con MAME.

En primer lugar necesitamos el código fuente de MAME y un compilador. A continuación debemos anular la rutina que desencripta el código del FD1094, para simular lo que estamos queriendo hacer: utilizar un 68000 normal. Para ello basta con editar el fichero fd1094.c y hacer que la función “fd1094_decode” no devuelva el valor descodificado, sino el valor original (el mismo que se le pasa como parámetro):

int fd1094_decode(int address,int val,UINT8 *key,int vector_fetch) return val;

 

Acto seguido sustituimos los ficheros de la ROM de MAME originales por los que acabamos de desencriptar, generando así una nueva ROM (desencriptada). Mantenemos el fichero *.key porque, aunque ya no se utilice, MAME esperará encontrarlo.

Por último debemos calcular los códigos de comprobación CRC32 y SHA1 de nuestras ROM desencriptadas. MAME comprueba esos códigos al cargar un juego. Como nuestras ROM son distintas a las originales de MAME, esos códigos (que se calculan sobre la marcha al ejecutar el juego correspondiente) son distintos a los que MAME espera, con lo cual no ejecutaría dicho juego. La aplicación HashMyFiles, de Nir Sorfer, puede hacer esos cálculos:

http://www.nirsoft.net/utils/hash_my_files.html.

El siguiente paso es ir al archivo segas16.c, segas16a.c, o el relativo al dirver que corresponda del fuente del MAME, editarlo y buscar al final del mismo el código dónde se cargan las ROM de nuestro juego. En el caso de la versión del Wonder Boy III del que estamos hablando, son estas líneas:

ROM_START( wb32 )

ROM_REGION( 0x40000, "maincpu", 0 ) /* 68000 code */

       ROM_LOAD16_BYTE( "epr-12098.a1", 0x000001, 0x10000, CRC(d998e5e5) SHA1(a7398b7338a33451a650c2f1d382dad2e5130528) )

ROM_LOAD16_BYTE( "epr-12100.a6", 0x000000, 0x10000, CRC(f5ca4abc) SHA1(1331db10cf99905fb13c1606a3f4d8bbf3d681b6) )

ROM_LOAD16_BYTE( "epr-12099.a2", 0x020001, 0x10000, CRC(3e243b45) SHA1(2a079553d1b61aaf18025847570003b79c8d6edf) )

ROM_LOAD16_BYTE( "epr-12101.a5", 0x020000, 0x10000, CRC(6146492b) SHA1(93515578a6ccf770944fea86d2f3200fa08f5075) )

 

Debemos modificarlas con los nuevos valores de CRC y SHA1 que hemos calculado, y ya estaríamos listos para compilar el código fuente de MAME.

Si el juego funciona correctamente utilizando nuestro MAME modificado y la ROM desencriptada, hemos tenido éxito y podemos proceder a quemar las EPROM involucradas y a hacer las modificaciones correspondientes en la placa original. Si tenemos algún problema, podemos utilizar el debugger de MAME. Con él podemos ejecutar el código instrucción por instrucción,  y tratar de identificar dónde está el problema.

En el caso del Wonder Boy III aún haciéndolo todo correctamente hay problemas. El juego arranca bien, se muestra el disclaimer, pero ahí el juego se cuelga. Ejecutándolo paso a paso con el debugger, se observa que en $18e2 se realiza una comprobación que, en la ROM original salta a una subrutina, pero en la desencriptada salta a una zona de datos, lo que acaba provocando una excepción y el juego se cuelga. Supongo que estará relacionado con una comprobación del CRC de las ROMs, con lo cual lo mejor es anular ese salto que provoca la excepción, sustituyéndolo por un salto a la subrutina correcta.

 

7.     Epílogo

Y nada más. Tras unos cuantos días de dormir poco (cuando se tienen niños pequeños no hay mucho tiempo durante el día para dedicarle a los hobbys) he conseguido algo que ya me propuse hace mucho tiempo, y que hasta ahora no me había atrevido a abordar.

Espero que con estas indicaciones seáis capaces de desencriptar vuestros propios juegos, y que los guardéis en vuestra colección sin miedo a que tengan “fecha de caducidad”.

Yo comparto esta información con todos vosotros con agrado. Solamente espero que nadie la utilice con fines lucrativos, aprovechándose del esfuerzo tanto mío como de las personas cuyo trabajo me ha servido de referencia.

Saludos.

 

Marcos75