Hola a todos

A pesar de mi amplia experiencia en la reversión de juegos para Sega Mega Drive
, nunca decidí usarlo, y no se me aparecieron en Internet. Pero, el otro día había un crackie divertido que quería resolver. Comparto contigo la decisión ...
Descripción
La descripción de la tarea y el ron en sí se pueden descargar aquí .
A pesar de que la lista de recursos dice Hydra, el estándar de facto entre las herramientas para depurar y revertir juegos en Sega es Smd Ida Tools . Tiene todo lo que necesitas para resolver esta crema:
- Cargador de ron para Ida
- Depurador
- Ver y cambiar la memoria RAM / VDP
- Mostrar información casi completa sobre VDP
Colocamos la última versión en los complementos de Ide y comenzamos a ver lo que tenemos.
Solución
El lanzamiento de cualquier juego de Shogi comienza con la ejecución del vector Reset
. Se puede encontrar un puntero en el segundo DWORD desde el comienzo del ron.


Vemos un par de funciones no identificadas que comienzan en la dirección 0x27A
. Veamos que hay ahí.
sub_2EA ()

Desde mi propia experiencia, diré que esto generalmente parece ser la función de esperar a que se complete la interrupción de VBLANK
. Veamos dónde más hay llamadas a la variable byte_FF0026
:

Vemos que el bit cero se establece en la interrupción VBLANK
. Entonces llamamos a la variable vblank_ready
, y la función donde se verifica es wait_for_vblank
.
sub_60E ()
A continuación, la función sub_60E
se llama por código. Veamos que hay ahí:

Lo que el primer comando escribe en VDP_CTRL
es el comando de control VDP
. Para saber qué está haciendo, nos paramos sobre este comando y presionamos la tecla J
:

Vemos que la entrada en CRAM
(el lugar donde se almacenan las paletas) se inicializa. Esto significa que todo el código de función posterior simplemente establece algún tipo de paleta inicial. En consecuencia, la función se puede llamar init_cram
.
sub_71A ()

Vemos que algún comando se transfiere nuevamente a VDP_CTRL
, luego presione nuevamente J
y descubrimos que este comando inicializa la grabación en la memoria de video:

Además, para entender lo que se transfiere allí a la memoria de video, no tiene sentido. Por lo tanto, simplemente llamamos a la función load_vdp_data
.
sub_C60 ()
Casi lo mismo sucede aquí que en la función anterior, por lo tanto, sin entrar en detalles, simplemente llamamos a la función load_vdp_data2
.
sub_8DA ()
Ya hay más código. Y además, se llama a otra función en esta función. Miremos allí, en sub_D08
.
sub_D08 ()

Vemos que en el registro D0
VDP_CTRL
el comando para VDP_CTRL
, en D1
, el valor con el que se rellenará VRAM
, y en D2
y D3
, el ancho y la altura del relleno (porque resulta dos ciclos: interno y externo). Llame a la función fill_vram_by_addr.
sub_8DA ()
Volvemos a la función anterior. Una vez que el valor en el registro D0
se transmite como un comando para VDP_CTRL
, presione la tecla J
en el valor. Obtenemos:

Nuevamente, desde la experiencia de revertir juegos a Sega, puedo decir que este comando inicializa la grabación de mosaicos de mapeo. Las direcciones que comienzan en $Fxxx
, $Exxx
, $Dxxx
, $Cxxx
en el 90% de los casos serán direcciones de regiones con estas mismas asignaciones. ¿Qué son las asignaciones?
estos son los valores con los que puede especificar dónde mostrar este o aquel mosaico en la pantalla (un mosaico es un cuadrado de 8x8
píxeles).
Entonces la función se puede llamar como init_tile_mappings
.
sub_CDC ()

El primer comando inicializa el registro en la dirección $F000
. Una nota: entre las direcciones de la " asignación ", todavía hay una región donde se almacena la tabla de sprites (estas son sus posiciones, mosaicos a los que apuntan, etc.) Averigüe qué región es responsable de lo que se puede depurar. Pero por ahora, no necesitamos esto, así que llamemos a la función init_other_mappings
.
Además, vemos que en esta función se inicializan dos variables: word_FF000A
y word_FF000C
. Según mi propia experiencia (sí, él decide), diré que si hay dos variables cercanas en el espacio de direcciones y están asociadas con el mapeo, en la mayoría de los casos serán las coordenadas de algún objeto (por ejemplo, un sprite). Por lo tanto, sugiero llamarlos sprite_pos_x
y sprite_pos_y
. El error en x
e y
permisible ya que Además, bajo depuración será fácil de arreglar.
VBLANK
Como el bucle va más allá en el código, podemos suponer que hemos finalizado la inicialización básica. Ahora puedes ver la interrupción de VBLANK
.

Vemos que dos variables se están incrementando (lo cual es extraño, en la lista de enlaces a cada una de ellas está absolutamente vacío). Pero, dado que se actualizan una vez por fotograma, puede llamarlos timer1
y timer2
.
A continuación, se sub_2FE
función sub_2FE
. Veamos que hay ahí:
sub_2FE ()

Y allí: trabaje con el puerto IO_CT1_DATA
(responsable del primer joystick). La dirección del puerto se carga en el registro A0
y se pasa a la función sub_310
. Vamos alli:
sub_310 ()

Mi experiencia me ayuda de nuevo. Si ve el código que funciona con el joystick y dos variables en la memoria, una almacena las pressed keys
y la segunda contiene las held keys
, es decir. solo presioné y sostuve las teclas. Entonces llamemos a estas variables : pressed_keys
y held_keys
. Y luego la función se puede llamar como update_joypad_state
.
sub_2FE ()
Llame a la función como read_joypad
.
Lazo del controlador
Ahora todo se ve mucho más claro:

Entonces, este ciclo responde a las teclas presionadas y realiza las acciones correspondientes. Veamos cada una de las funciones llamadas en el bucle.
sub_4D4 ()

Hay mucho código Comencemos con la primera función llamada: sub_60C
.
sub_60C ()
Ella no hace nada, puede parecerlo al principio. Acabo de regresar de la función actual es rts
. Pero porque solo se producen saltos ( bsr
) en él, lo que significa que rts
nos devolverá al ciclo del controlador. retn_to_loop
esta función como retn_to_loop
.
sub_4D4 ()
A continuación, vemos la llamada a la variable word_FF000E
. No se usa en ninguna parte excepto por la función actual y, al principio, el propósito de la misma no estaba claro para mí. Pero, si observa detenidamente, podemos suponer que esta variable es necesaria solo por un pequeño retraso entre el procesamiento de las pulsaciones de teclas. ( Ya está mal implementado en este ron, pero creo que sin esta variable sería mucho peor ).

A continuación, tenemos una gran cantidad de código que de alguna manera procesa las sprite_pos_y
sprite_pos_x
y sprite_pos_y
, que solo pueden hablar de una cosa: esto es necesario para mostrar el sprite de selección alrededor del carácter seleccionado en el alfabeto.
Así que ahora puede nombrar la función de forma update_selection
como update_selection
. Sigamos adelante.

El código verifica si los bits de algunas teclas presionadas están configurados y llama a ciertas funciones. Miremos a ellos.
sub_D28 ()

Algún tipo de magia chamánica. Primero, la WORD
se toma de la variable word_FF0018
, luego se ejecuta una instrucción interesante:
bsr.w *+4
Este comando simplemente salta a las instrucciones que lo siguen.
Lo siguiente es otra magia:
move.l d0,(sp) rts
El valor en el registro D0
se coloca en la parte superior de la pila. Vale la pena señalar que, para Shogi, así como para algunos x86
, la dirección de retorno de la función cuando se llama se coloca en la parte superior de la pila. En consecuencia, la primera instrucción coloca alguna dirección en la parte superior, y la segunda la levanta de la pila y hace una transición a lo largo de ella. Buen truco
Ahora necesita comprender cuál es este valor en la variable, que luego pasa. Pero primero, llamemos a esta variable jmp_addr
.
Y las funciones se llamarán así:
sub_D38
: goto_to_d0
sub_D28
: jump_to_var_addr
jmp_addr
Averigüe dónde se rellena esta variable. Nos fijamos en la lista de referencias:

Solo hay un lugar para escribir en esta variable. Miremoslo.
sub_3A4 ()

Aquí, dependiendo de la coordenada del sprite (recuerde que esta es probablemente la dirección del carácter seleccionado), se ingresa este o aquel valor. Vemos la siguiente sección de código:

El valor existente se desplaza a la derecha 4 bits, se coloca un nuevo valor en el byte bajo y el resultado se ingresa nuevamente en la variable. En teoría, nuestra variable jmp_addr
almacena los caracteres que podemos ingresar en la pantalla de ingreso de teclas. Tenga en cuenta también que el tamaño de la variable es WORD
.
De hecho, la función sub_3A4
puede llamarse update_jmp_addr
.
sub_414 ()
Ahora solo nos queda una función en el bucle, que no se reconoce. Y se llama sub_414
.

Su código se parece al código de la función update_jmp_addr
, solo que al final tenemos una sub_45E
función sub_45E
. Miremos allí.
sub_45E ()

Vemos que el número #$4B1E2003
ingresa en el registro D0
, que luego se envía a VDP_CTRL
, lo que significa que estamos tratando con otro comando de control VDP
. Presionamos J
, recibimos un comando de registro en la región con el mapeo $Cxxx
.
A continuación, el código funciona con la variable byte_FF0014
, que no se usa en ninguna parte excepto la función actual. Si observa detenidamente cómo se usa, notará que el número máximo que se puede instalar es 4
. Supongo que esta es la longitud actual de la clave ingresada. Vamos a verlo
Ejecute el depurador
Usaré el depurador de Smd Ida Tools
, pero, de hecho, algunas Gens KMod o Gens ReRecording serán suficientes. Lo principal es que hay una función con la visualización de direcciones en la memoria.

Mi teoría ha sido confirmada. Entonces la variable byte_FF0014
ahora se puede key_length
.
Hay otra variable: dword_FF0010
, que también se usa solo en la función actual, y su contenido, después de agregarlo al comando inicial en D0
(recuerdo que este era el número #$4B1E2003
), se envía a VDP_CTRL
. Sin pensarlo add_to_vdp_cmd
, llamé a la variable add_to_vdp_cmd
.
Entonces, ¿qué hace esta función? Tengo la suposición de que ella dibuja el personaje introducido. Comprobar esto es simple: iniciando el depurador y comparando el estado antes de llamar a la función sub_45E
y después:
Para:

Después:

Tenía razón: esta función dibuja el carácter introducido. Lo llamamos do_draw_input_char
, y la función que lo llama ( sub_414
) es draw_input_char
.
Que ahora
Veamos por ahora que la variable que llamamos jmp_addr
realmente almacena la clave ingresada. Utilizaremos el mismo Memory Watch
:

Como puede ver, la conjetura era cierta. ¿Qué nos da esto? Podemos saltar a cualquier dirección. Pero cual? En la lista de funciones, todas se ordenan después de todo:

Luego comencé a desplazarme por el código hasta que encontré esto:

El ojo entrenado vio la secuencia de $4E, $75
al final de los bytes no asignados. Este es el código de operación de la instrucción rts
, es decir volver de la función. Entonces estos bytes no asignados pueden ser el código de alguna función. Intentemos designarlos como un código, presione C
:

Obviamente, este es un código de función. También puede presionar P
para que el código sea una función. Recuerda este nombre: sub_D3C
.
Entonces surge el pensamiento: ¿qué sub_D3C
si saltas en sub_D3C
? Suena bien, aunque un solo salto aquí obviamente no será suficiente, porque no había más enlaces a la variable word_FF0020
.
Entonces se me ocurrió otra idea: ¿qué pasaría si buscáramos otro código sin asignar? Abra el cuadro de diálogo Binary search
(Alt + B), ingrese la secuencia 4E 75
en él, marque la casilla Find all occurrences
:

Haga
en
para comenzar la búsqueda, obtenemos los siguientes resultados.

Al menos dos lugares más en el ron pueden contener un código de función, debe verificarlos. Hacemos clic en la primera de las opciones, nos desplazamos un poco hacia arriba y nuevamente vemos una secuencia de bytes indefinidos. ¿Denotarlos como una función? Si! Presione P
donde comienzan los bytes:

Genial! Ahora tenemos la función sub_34C
. Intentamos repetir lo mismo con la última de las opciones encontradas, y ... tenemos un fastidio. Hay tantos bytes antes de 4E 75
que no está claro dónde comienza la función. Y, obviamente, no todos estos bytes anteriores son código, porque muchos bytes duplicados
Determinar el comienzo de la función.
Será más fácil para nosotros encontrar el comienzo de la función si encontramos dónde terminan los datos. Como hacerlo En realidad no es nada complicado:
- Giramos antes del comienzo de los datos (habrá un enlace a ellos desde el código)
- Seguimos el enlace y buscamos un ciclo en el que debería aparecer el tamaño de estos datos
- Marcar la matriz
Entonces, realizamos el primer párrafo ...:

... e inmediatamente vemos que en un ciclo de nuestra matriz se copian 4 bytes de datos a la vez (porque move.l
) a VDP_DATA
. A continuación vemos el número 2047
. Al principio, puede parecer que el tamaño final de la matriz es 2047 * 4
, pero el bucle basado en dbf
ejecuta una iteración +1
más, porque El último valor comparado no es 0
, sino -1
.
Total: el tamaño de la matriz es 2048 * 4 = 8192
. Denota bytes como una matriz. Para hacer esto, haga clic en *
y especifique el tamaño:

Giramos hasta el final de la matriz, y vemos allí bytes, que son exactamente los bytes del código:


Ahora tenemos la función sub_2D86
, ¡y tenemos todo para resolver esta grieta! Veamos qué hace la función recién creada.
sub_2D86 ()
Y simplemente coloca el valor #$4147
en el registro D1
y llama a la función sub_34C
. Mírala a ella.
sub_34C ()

Vemos que aquí se word_FF0020
el valor de la variable word_FF0020
. Si observa los enlaces, veremos otro lugar donde se está llevando a cabo el registro en esta variable, y este será exactamente el lugar donde quería saltar a través de la variable jmp_addr
. Esto confirma el presentimiento de que definitivamente necesitas saltar a sub_D3C
.
Pero lo que sucedió después fue demasiado flojo para que lo entendiera, así que arrojé el ron a GHIDRA , encontré esta función y miré el código descompilado:
void FUN_0000034c(void) { ushort in_D1w; short sVar1; ushort *puVar2; if (((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,in_D1w ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; }
Vemos que in_D1w
la variable con el nombre extraño in_D1w
, y también la variable DAT_00ff0020
, que con su dirección se parece a la word_FF0020
mencionada word_FF0020
.
in_D1w
nos dice que este valor se toma del registro D1
, o más bien de su mitad WORD más joven, y establece el registro D1
función que lo pasa. ¿Recuerdas #$4147
? Por lo tanto, debe designar este registro como argumento de entrada para la función.
Para hacer esto, en la ventana con el código descompilado, haga clic con el botón derecho en el nombre de la función y seleccione el elemento de menú Edit Function Signature
:

Para indicar que la función lleva un argumento a través de un registro específico, es decir, no mediante el método estándar para la convención de llamada actual, debe marcar la Use Custom Storage
y hacer clic en el icono con un signo más :

Aparece una posición para el nuevo argumento de entrada. Hacemos doble clic en él y obtenemos un cuadro de diálogo que indica el tipo y el medio del argumento:

En el código descompilado, vemos que in_D1w
es del tipo ushort
, lo que significa que lo especificaremos en el campo de tipo. Luego haga clic en el botón Add
:

Aparecerá una posición para indicar el medio del argumento, debemos especificar el registro D1w
en Location
y hacer OK
en OK
:

El código descompilado tomará la forma:
void FUN_0000034c(ushort param_1) { short sVar1; ushort *puVar2; if (((ushort)(param_1 ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(param_1 ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,param_1 ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; }
param_1
que nuestro valor param_1
es constante, pasado por la función de llamada y es igual a #$4147
. Entonces, ¿cuál debería ser el valor de DAT_00ff0020
? Consideramos:
0x4147 ^ DAT_00ff0020 ^ 0x5e4e = 0x5a5a 0x4147 ^ DAT_00ff0020 ^ 0x4a44 = 0x4e50
Porque xor
: la operación es reversible, todos los números constantes pueden pelearse entre sí y obtener el valor deseado de la variable DAT_00ff0020
.
DAT_00ff0020 = 0x4147 ^ 0x5e4e ^ 0x5a5a = 0x4553 DAT_00ff0020 = 0x4147 ^ 0x4a44 ^ 0x4e50 = 0x4553
Resulta que el valor de la variable debe ser 0x4553
. Parece que ya vi un lugar donde se establece ese valor ...

Conclusiones y decisión.
Llegamos a los siguientes resultados:
- Primero debe saltar a la dirección
0x0D3C
, para esto debe ingresar el código 0D3C
- Salte a la función en
0x2D86
, que establece el valor de D1
para registrar #$4147
, para esto debe ingresar el código 2D86
Experimentalmente, descubrimos la tecla que debe presionarse para verificar la tecla ingresada: B
Intentamos:

Gracias