Mientras buscaba formas de activar los menús del desarrollador que quedaban en Animal Crossing, incluido el menú de selección de juegos para el emulador NES, encontré una característica interesante que existe en el juego original y estaba constantemente activa, pero Nintendo nunca la usó.
Además de los juegos NES / Famicom del juego, puedes descargar nuevos juegos NES desde una tarjeta de memoria.
También logré encontrar una manera de usar este cargador de arranque ROM para parchear mi código y mis datos en el juego, lo que te permite ejecutar código a través de una tarjeta de memoria.
Introducción - Objetos de consola NES
Los juegos normales de NES, que se pueden obtener de Animal Crossing, son muebles separados en forma de una consola de NES con un cartucho sobre él.
Habiendo localizado este objeto en tu casa e interactuando con él, puedes ejecutar este único juego. La imagen a continuación muestra Excitebike y Golf.
También hay un objeto de consola NES común en el que no hay juegos integrados. Se puede comprar en Redd, y a veces se obtiene a través de eventos aleatorios, por ejemplo, leyendo en el tablón de anuncios de la ciudad que la consola está enterrada en un punto aleatorio de la ciudad.
Este objeto se parece a una consola NES en la que no hay cartuchos.
El problema con este objeto es que se pensó que no se podía reproducir. Cada vez que interactúas con él, solo ves un mensaje que dice que no tienes software de juego.
¡Resultó que este objeto en realidad está tratando de escanear la tarjeta de memoria en busca de archivos especialmente diseñados que contienen imágenes ROM para NES! El emulador NES utilizado para ejecutar juegos integrados parece ser el emulador NES estándar completo para GameCube y es capaz de lanzar la mayoría de los juegos.
Antes de demostrar estas características, explicaré el proceso de ingeniería inversa.
Buscar ROM bootloader en la tarjeta de memoria
Estamos buscando un menú de desarrollador
Inicialmente, quería encontrar un código que active varios menús de desarrollador, como el menú de selección de mapas o el menú de selección de juegos para el emulador NES. El menú
Forest Map Select , gracias al cual puedes cargar fácilmente instantáneamente diferentes ubicaciones del juego, fue bastante simple de encontrar: solo busqué la línea FOREST MAP SELECT que aparece en la parte superior de la pantalla (se puede ver en diferentes videos y capturas de pantalla en Internet )
En "FOREST MAP SELECT" hay referencias cruzadas de datos a la función
select_print_wait
, lo que conduce a un montón de otras funciones que también tienen el prefijo
select_*
, incluida la función
select_init
. Resultó ser funciones que controlan el menú de selección del mapa.
La función
select_init
conduce a otra función interesante llamada
game_get_next_game_dlftbl
. Esta función enlaza todos los demás menús y "escenas" que puede ejecutar: una pantalla con el logotipo de Nintendo, la pantalla principal, el menú de selección de tarjetas, el menú del emulador NES (Famicom), etc. Comienza al comienzo del procedimiento principal del juego, encuentra qué función de inicialización de escena debe ejecutar y encuentra su entrada en la estructura de datos de la tabla llamada
game_dlftbls
. Esta tabla contiene enlaces a las funciones de procesamiento de varias escenas, así como algunos otros datos.
Un estudio cuidadoso del primer bloque de la función mostró que carga la función "siguiente inicio del juego", y luego comienza a compararla con una serie de funciones de inicio conocidas:
first_game_init
select_init
play_init
second_game_init
trademark_init
player_select_init
save_menu_init
famicom_emu_init
prenmi_init
Uno de los punteros de función que está buscando es
famicom_emu_init
, que es responsable de ejecutar el emulador NES / Famicom.
game_get_next_game_init
resultado de
game_get_next_game_init
a
famicom_emu_init
o
select_init
en el depurador Dolphin, pude mostrar menús especiales. El siguiente paso es determinar cómo se configuran estos punteros de la manera normal durante la ejecución del programa. Lo único que hace la función
game_get_next_game_init
es cargar el valor en el desplazamiento
0xC
primer argumento en
game_get_next_game_dlftbl
.
Hacer un seguimiento de estos valores establecidos en varias estructuras de datos fue un poco aburrido, así que iré directamente al núcleo. Lo más importante que encontré:
- Cuando el juego comienza de la manera habitual, realiza la siguiente secuencia de acciones:
first_game_init
second_game_init
trademark_init
play_init
player_select_init
establece el siguiente inicio en select_init
. Esta pantalla debería permitirle seleccionar un jugador inmediatamente después de elegir una carta, pero parece que no funciona correctamente.
También encontré una función sin nombre que define la función init del emulador, pero no encontré nada que establezca la función init en el valor init del jugador o la opción de mapa.
En este punto, me di cuenta de que tenía otro estúpido problema con la forma en que cargaba los nombres de funciones en la IDA: debido a la expresión regular utilizada para cortar líneas en el archivo de símbolos de depuración, me perdí todos los nombres de funciones que comienzan con una letra mayúscula . La función que configuró
famicom_emu_init
parecía transiciones entre escenas y, por supuesto, se llamaba
Game_play_fbdemo_wipe_proc
.
Game_play_fbdemo_wipe_proc
maneja las transiciones entre escenas, como
Game_play_fbdemo_wipe_proc
pantalla y apagones.
Bajo ciertas condiciones, la transición de la pantalla se realizó de la jugabilidad habitual a la visualización del emulador. Fue él quien configuró la función de inicio del emulador.
Manejo de objetos de la consola
En realidad, los manipuladores de objetos de muebles para consolas NES hacen que el controlador de transición de pantalla cambie al emulador. Cuando un jugador interactúa con una de las consolas, se
aMR_FamicomEmuCommonMove
.
Al llamar a la función,
r6
contiene el valor de índice correspondiente a los números en los nombres de los archivos del juego NES en
famicom.arc
:
01_nes_cluclu3.bin.szs
02_usa_balloon.nes.szs
03_nes_donkey1_3.bin.szs
04_usa_jr_math.nes.szs
05_pinball_1.nes.szs
06_nes_tennis3.bin.szs
07_usa_golf.nes.szs
08_punch_wh.nes.szs
09_usa_baseball_1.nes.szs
10_cluclu_1.qd.szs
11_usa_donkey3.nes.szs
12_donkeyjr_1.nes.szs
13_soccer.nes.szs
14_exbike.nes.szs
15_usa_wario.nes.szs
16_usa_icecl.nes.szs
17_nes_mario1_2.bin.szs
18_smario_0.nes.szs
19_usa_zelda1_1.nes.szs
(
.arc
es un formato de archivo de propiedad).
Cuando
r6
no
r6
igual a cero, se pasa en
aMR_RequestStartEmu
llamada
aMR_RequestStartEmu
. En este caso, se activa la transición al emulador.
Sin embargo, si
r6
es cero, se
aMR_RequestStartEmu_MemoryC
función
aMR_RequestStartEmu_MemoryC
. Al establecer el valor en el depurador en 0, recibí el mensaje "No tengo ningún software". No recordé de inmediato que necesitaba verificar el objeto de la consola NES para asegurarme de que restablece el valor
r6
, pero resultó que el índice cero se usa para el objeto de la consola sin un cartucho.
Aunque
aMR_RequestStartEmu
simplemente almacena el valor del índice en algún tipo de estructura de datos,
aMR_RequestStartEmu_MemoryC
realiza operaciones mucho más complejas ...
Este tercer bloque de código llama
aMR_GetCardFamicomCount
y busca un resultado distinto de cero; de lo contrario, omite la mayoría de las cosas interesantes en el lado izquierdo del gráfico de funciones.
aMR_GetCardFamicomCount
llama a
famicom_get_disksystem_titles
, que luego llama a
memcard_game_list
, y aquí todo se vuelve muy interesante.
memcard_game_list
monta la tarjeta de memoria y comienza a dar vueltas en el ciclo de escritura de archivos, verificando cada uno de los valores. Al rastrear la función en el depurador, pude entender que estaba comparando los valores con cada uno de mis archivos en la tarjeta de memoria.
La función decide si descargar o no el archivo, dependiendo de los resultados de la verificación de varias líneas. En primer lugar, comprueba la presencia de las líneas "GAFE" y "01", que son los identificadores del juego y la empresa. 01 significa Nintendo, GAFE significa Animal Crossing. Creo que significa GameCube Animal Forest English.
Luego verifica las líneas "DobutsunomoriP_F_" y "SAVE". En este caso, la primera línea debe coincidir, pero no la segunda. Resultó que "DobutsunomoriP_F_SAVE" es el nombre del archivo que almacena los datos de los juegos incrustados para NES. Por lo tanto, todos los archivos, excepto este, se cargarán con el prefijo "DobutsunomoriP_F_".
Utilizando el depurador Dolphin para omitir las comparaciones de cadenas con "SAVE" y haciendo que el juego engañe para creer que mi archivo "SAVE" se puede descargar de forma segura, obtuve este menú después de usar la consola NES:
Respondí "Sí" e intenté cargar el archivo guardado como un juego, después de lo cual vi por primera vez la pantalla de bloqueo del juego incorporada:
Genial Ahora sé que en realidad está intentando descargar juegos desde una tarjeta de memoria, y puedo comenzar a analizar el formato de los archivos guardados para ver si se puede descargar una ROM real.
Lo primero que intenté hacer fue tratar de encontrar dónde se lee el nombre del juego del archivo de la tarjeta de memoria. Al buscar la línea "FEFSC" que estaba presente en el mensaje "¿Desea jugar <nombre>?", Encontré el desplazamiento en el que se leía del archivo:
0x642
. Copié el archivo de guardar, cambié el nombre del archivo a "DobutsunomoriP_F_TEST", cambié los bytes en el desplazamiento
0x642
a "PRUEBA" e
0x642
el guardado modificado, después de lo cual el nombre que necesitaba apareció en el menú.
Después de agregar algunos archivos más en este formato, aparecieron algunas opciones más en el menú:
Descargar ROM
Si
aMR_GetCardFamicomCount
devuelve distinto de cero, la memoria se asigna en el montón,
famicom_get_disksystem_titles
se llama directamente
famicom_get_disksystem_titles
, después de lo cual se especifican un montón de compensaciones aleatorias en la estructura de datos. En lugar de descifrar dónde se leerán estos valores, comencé a estudiar la lista de funciones de
famicom
.
Resultó que necesitaba
famicom_rom_load
. Controla la carga de ROM, ya sea desde una tarjeta de memoria o desde los recursos internos del juego.
Lo más importante en este bloque de "arranque desde la tarjeta de memoria" es que llama
memcard_game_load
. Ella vuelve a montar el archivo en la tarjeta de memoria, lo lee y analiza. Aquí es donde las opciones de formato de archivo más importantes se hacen evidentes.
Valor de suma de control
Lo primero que sucede después de cargar el archivo es el cálculo de la suma de verificación. Se
calcSum
función
calcSum
, que es un algoritmo muy simple que suma los valores de todos los bytes en los datos de la tarjeta de memoria. Los ocho bits inferiores del resultado deben ser cero. Es decir, para pasar esta verificación, debe sumar los valores de todos los bytes en el archivo fuente, calcular el valor que debe agregarse para que los ocho bits inferiores se conviertan en cero y luego asignar este valor al byte de suma de verificación en el archivo.
Si la verificación falla, recibirá un mensaje sobre la imposibilidad de leer correctamente la tarjeta de memoria, y no sucede nada. Durante la depuración, todo lo que tengo que hacer es omitir esta verificación.
Copiar ROM
Cerca del final de
memcard_game_load
, sucede otra cosa interesante. Hay varios bloques de código más interesantes entre él y la suma de verificación, pero ninguno de ellos genera ramificaciones que omiten la ejecución de este comportamiento.
Si un cierto valor entero de 16 bits leído desde la tarjeta de memoria no es igual a cero, se llama a una función que verifica el encabezado de compresión en el búfer. Comprueba los formatos de compresión patentados de Nintendo al observar el comienzo del búfer Yay0 o Yaz0. Si se encuentra una de estas líneas, se llama a la función de desempaquetado. De lo contrario, se realiza una función de copia simple. En cualquier caso, después de eso,
nesinfo_data_size
una variable llamada
nesinfo_data_size
.
Otro indicio de contexto aquí es que los archivos ROM para juegos NES integrados usan la compresión Yaz0, y esta línea está presente en los encabezados de sus archivos.
Después de observar el valor que se verifica para cero, y el búfer pasó a las funciones de verificación de compresión, rápidamente descubrí de dónde se estaba leyendo el juego en el archivo de la tarjeta de memoria. La comprobación cero se realiza para parte del búfer de 32 bytes copiado del desplazamiento
0x640
en el archivo, que probablemente sea el encabezado ROM. Esta función también verifica otras partes del archivo, y es en ellas donde se encuentra el nombre del juego (comenzando con el tercer byte del encabezado).
En la ruta de ejecución del código que encontré, el búfer de ROM se encuentra inmediatamente después de este búfer de encabezado de 32 bytes.
Esta información es suficiente para intentar crear un archivo ROM que funcione. Acabo de tomar uno de los otros archivos guardados de Animal Crossing y lo
DobutsunomoriP_F_TEST
en un editor hexadecimal para reemplazar el nombre del archivo con
DobutsunomoriP_F_TEST
y borrar todas las áreas donde quería pegar los datos.
Para una ejecución de prueba, utilicé la ROM del juego Pinball, que ya está en el juego, e inserté su contenido después del encabezado de 32 bytes. En lugar de calcular el valor de la suma de verificación, establezco puntos de interrupción para simplemente omitir
calcSum
y también observar los resultados de otras verificaciones que pueden conducir a una rama que omite el proceso de arranque de ROM.
Finalmente, importé el nuevo archivo a través del administrador de tarjetas de memoria Dolphin, reinicié el juego e intenté iniciar la consola.
Funcionó! Hubo algunos pequeños errores gráficos relacionados con los parámetros de Dolphin, que afectaron el modo gráfico utilizado por el emulador NES, pero en general el juego funcionó bien. (En las versiones más recientes de Dolphin, debería funcionar de forma predeterminada).
Para asegurarme de que otros juegos también comiencen, traté de escribir otras ROM que no estaban en el juego. Battletoads comenzó, pero dejó de funcionar después del texto de la pantalla de inicio (después de otras configuraciones logré hacer que se pueda jugar). Mega Man, por otro lado, funcionó perfectamente:
Para aprender a generar nuevos archivos ROM que podrían cargarse sin la intervención de depuradores, tuve que comenzar a escribir código y comprender mejor el análisis del formato de archivo.
Formato de archivo ROM externo
La parte más importante del análisis de archivos ocurre en
memcard_game_load
. Hay seis secciones principales de bloques de análisis de código en esta función:
- Suma de comprobación
- Guardar nombre de archivo
- Encabezado de archivo ROM
- Buffer desconocido copiado sin ningún procesamiento
- Comentario de texto, icono y cargador de banner (para crear un nuevo archivo guardado)
- Cargador de arranque ROM
Suma de comprobación
Los ocho bits inferiores de la suma de todos los valores de bytes en el archivo guardado deben ser cero. Aquí hay un código simple de Python que genera el byte de suma de verificación necesario:
checksum = 0 for byte_val in new_data_tmp: checksum += byte_val checksum = checksum % (2**32)
Probablemente haya un lugar especial para almacenar el byte de suma de comprobación, pero agregarlo al espacio vacío al final del archivo de guardar funciona bastante bien.
Nombre de archivo
Nuevamente, el nombre del archivo guardado debe comenzar con "DobutsunomoriP_F_" y terminar con algo que no contenga "GUARDAR". Este nombre de archivo se copia un par de veces, y en un caso la letra "F" se reemplaza por "S". Este será el nombre de los archivos guardados para el juego NES ("DobutsunomoriP_S_NAME").
Encabezado ROM
Se carga en la memoria una copia directa del encabezado de 32 bytes. Algunos de los valores en este encabezado se usan para determinar cómo manejar secciones subsiguientes. Básicamente, estos son algunos valores de tamaño de 16 bits y bits de parámetros empaquetados.
Si traza el puntero copiado por el encabezado hasta el inicio de la función y encuentra la posición de su argumento, la firma de la función a continuación mostrará que en realidad tiene el tipo
MemcardGameHeader_t*
.
memcard_game_load(unsigned char *, int, unsigned char **, char *, char *, MemcardGameHeader_t *, unsigned char *, unsigned long, unsigned char *, unsigned long)
Buffer desconocido
Comprueba el valor de tamaño de 16 bits del encabezado. Si no es igual a cero, el número correspondiente de bytes se copia directamente desde el búfer de archivo a un nuevo bloque de memoria asignada. Esto mueve el puntero de datos en el búfer de archivos para que pueda continuar la copia desde la siguiente sección.
Banner, ícono y comentario
Se verifica otro valor de tamaño en el encabezado, y si no es igual a cero, se llama a la función de verificación de compresión de archivos. Si es necesario, se iniciará el algoritmo de desempaquetado, después de lo cual se
SetupExternCommentImage
.
Esta función hace tres cosas: "comentario", imagen de banner e icono. Para cada uno de ellos, hay un código en el encabezado de la ROM que muestra cómo manejarlos. Existen las siguientes opciones:
- Usar valor predeterminado
- Copie de la sección de banner / icono / comentario en el archivo ROM
- Copiar desde el búfer alternativo
Los valores predeterminados del código hacen que el ícono o pancarta se cargue desde el recurso en el disco, y el nombre del archivo guardado y el comentario (descripción de texto del archivo) se les asignan los valores "Animal Crossing" y "NES Cassette Save Data". Así es como se ve:
El segundo valor del código simplemente copia el nombre del juego del archivo ROM (una alternativa a "Animal Crossing"), y luego trata de encontrar la cadena "] ROM" en el comentario del archivo y reemplazarlo con "] SAVE". Aparentemente, se suponía que los archivos que Nintendo quería lanzar tenían el formato de los nombres "Game Name [NES] ROM" o algo similar.
Para el icono y el banner, el código intenta determinar el formato de la imagen, obtener un valor de tamaño fijo correspondiente a este formato y luego copiar la imagen.
En el último valor del código, el nombre y la descripción del archivo se copian sin cambios desde el búfer, y el icono y el banner también se cargan desde el búfer alternativo.
ROM
Si observa cuidadosamente la captura de pantalla de la
memcard_game_load
ROM de copia
memcard_game_load
, puede ver que el valor de 16 bits verificado para igualdad a cero se desplaza a la izquierda por 4 bits (multiplicado por 16), y luego se usa como el tamaño de la función de
memcpy
si no se detecta compresión. Este es otro valor de tamaño presente en el encabezado.
Si el tamaño no es igual a cero, se comprueba la compresión de los datos de la ROM y luego se copia.
Búfer desconocido y búsqueda de errores
Aunque descargar nuevas ROM es bastante curioso, lo más interesante de este cargador de ROM para mí fue que, de hecho, esta es la única parte del juego que recibe la entrada del usuario de tamaño variable y la copia en diferentes ubicaciones de memoria. Casi todo lo demás usa buffers de tamaño constante. Cosas como nombres y textos de letras pueden parecer diferentes en longitud, pero esencialmente el espacio vacío simplemente está lleno de espacios. Las cadenas terminadas en cero se usan con poca frecuencia, evitando errores comunes de corrupción de memoria, como el uso de
strcpy
con un búfer que es demasiado pequeño para copiar cadenas.
Estaba muy interesado en la posibilidad de encontrar un exploit del juego basado en guardar archivos, y parecía que esta era la mejor opción.
La mayoría de las operaciones de archivos ROM descritas anteriormente usan copias de tamaño constante, con la excepción de un buffer desconocido y datos ROM. Desafortunadamente, el código que procesa este búfer asigna exactamente tanto espacio como sea necesario para copiarlo, por lo que no hay desbordamiento, y establecer tamaños de archivo ROM muy grandes no fue muy útil.
Pero aún quería saber qué sucede con este búfer, que se copia sin ningún procesamiento.
Manejadores de etiquetas de información de NES
famicom_rom_load
a
famicom_rom_load
. Después de cargar ROM desde una tarjeta de memoria o disco, se llaman varias funciones:
nesinfo_tag_process1
nesinfo_tag_process2
nesinfo_tag_process3
Después de haber rastreado el lugar donde se copia el búfer desconocido, me aseguré de que estas tareas realicen esta tarea. Comienzan con una llamada a
nesinfo_next_tag
, que realiza un algoritmo simple:
- Comprueba si el puntero especificado
nesinfo_tags_end
puntero en nesinfo_tags_end
. Si es menor que nesinfo_tags_end
o nesinfo_tags_end
es cero, entonces verifica la presencia de la cadena "END" en el encabezado del puntero.
- Si se alcanza "FIN", o el puntero ha subido a
nesinfo_tags_end
, la función devuelve nulo. - De lo contrario, el byte en el desplazamiento
0x3
puntero se agrega a 4 y al puntero actual, después de lo cual se devuelve el valor.
Esto nos dice que hay algún tipo de formato de etiqueta a partir de un nombre de tres letras, un valor de tamaño de datos y los datos en sí. El resultado es un puntero a la siguiente etiqueta, porque se omite la etiqueta actual (
cur_ptr + 4
omite el nombre de tres letras y un byte, y
size_byte
omite los datos).
Si el resultado no es cero, la función de procesamiento de etiquetas realiza una serie de comparaciones de cadenas para determinar qué etiqueta debe procesarse. Algunos de los nombres de etiquetas
nesinfo_tag_process1
en
nesinfo_tag_process1
: VEQ, VNE, GID, GNO, BBR y QDS.
Si se encuentra una coincidencia de etiqueta, se ejecuta algún código de controlador. Algunos de los controladores no hacen nada más que mostrar una etiqueta en el mensaje de depuración. Otros tienen manejadores más complejos. Después de procesar la etiqueta, la función intenta obtener la siguiente etiqueta y continuar procesando.
Afortunadamente, hay muchos mensajes detallados de depuración que se muestran cuando se detectan etiquetas.
Todos están en japonés, por lo que primero deben decodificarse de Shift-JIS y traducirse. Por ejemplo, un mensaje para QDS podría leer "Cargando un área de guardado de disco" o "Dado que esta es la primera ejecución, cree un área de guardado de disco". Los mensajes para el BBR dicen "cargar una copia de seguridad de la batería" o "dado que este es el primer inicio, realizamos una limpieza".Ambos códigos también cargan algunos valores de la sección de datos de sus etiquetas y los usan para calcular el desplazamiento en los datos de ROM, después de lo cual realizan operaciones de copia. Obviamente, son responsables de determinar las partes en la memoria ROM asociadas con la preservación del estado.También hay una etiqueta "HSC" con un mensaje de depuración que dice que está procesando registros de puntos. Ella obtiene un desplazamiento en ROM de los datos de su etiqueta, así como el valor del registro de puntaje original. Estas marcas se pueden usar para indicar un lugar en la memoria del juego NES para almacenar puntuaciones altas, posiblemente para guardarlas y restaurarlas en el futuro.Estas etiquetas crean un sistema de descarga de metadatos de ROM bastante complejo. Además, muchos de ellos conducen a llamadas memcpy
basadas en los valores transmitidos en los datos de la etiqueta.Caza de insectos
La mayoría de las etiquetas que conducen a la manipulación de la memoria no son muy útiles para exploits, porque todas tienen valores máximos de desplazamiento y tamaño especificados como enteros de 16 bits. Esto es suficiente para trabajar con el espacio de direcciones NES de 16 bits, pero no es suficiente para escribir valores de destino útiles, como punteros a funciones o direcciones de retorno en la pila en el espacio de direcciones GameCube de 32 bits.Sin embargo, hay varios casos donde los valores de las compensaciones de tamaño transmitidas memcpy
pueden exceder 0xFFFF
.QDS
QDS carga un desplazamiento de 24 bits de sus datos de etiqueta, así como un valor de tamaño de 16 bits.Lo bueno aquí es que el desplazamiento se usa para calcular la dirección de destino de la operación de copia. La dirección base del desplazamiento es el comienzo de los datos descargados, el origen de la copia está en el archivo ROM de la tarjeta de memoria y el tamaño se establece mediante el valor de tamaño de 16 bits de la etiqueta.El valor de 24 bits tiene un valor máximo 0xFFFFFF
, que es mucho más de lo necesario para escribir fuera de los datos ROM cargados. Sin embargo, hay ciertos problemas ...El primero es que, aunque el valor de tamaño máximo es igual 0xFFFF
, inicialmente se usa para restablecer la partición de memoria. Si el valor del tamaño es demasiado alto (no mucho más grande 0x1000
), esto restablecerá la marca "QDS" en el código del juego.Y ahí radica el problema, porque en nesinfo_tag_process1
realidad se llama dos veces. Por primera vez, recibe información sobre el espacio que necesita para prepararse para los datos almacenados. Las etiquetas QDS y BBR no se procesan completamente en la primera ejecución. Después de la primera ejecución, se prepara un lugar para guardar los datos y se vuelve a llamar a la función. Esta vez, las etiquetas QDS y BBR se procesan completamente, pero si las cadenas de nombre de la etiqueta se borran de la memoria, ¡entonces es imposible que las etiquetas vuelvan a coincidir!Esto se puede evitar estableciendo un valor de tamaño más pequeño. Otro problema es que el valor de desplazamiento solo puede avanzar en la memoria, y los datos de ROM NES se encuentran en el montón bastante cerca del final de la memoria disponible.Después de ellos solo hay unos pocos montones, y ninguno de ellos tiene algo particularmente útil, como punteros de función obvios.En el caso normal, podría usar esto para explotar un desbordamiento de montón, pero en la implementación malloc
utilizada para este montón, se han agregado bastantes bytes de comprobaciones de estado en bloques malloc
. Podemos escribir sobre los valores del puntero en los siguientes bloques de montón. Sin controles de estado, esto podría usarse para escribir en un área de memoria arbitraria cuando se solicita free
un bloque de montón involucrado.Sin embargo, la implementación utilizada aquí malloc
busca un patrón de byte específico ( 0x7373
) al comienzo de los bloques siguientes y anteriores que manipulará cuando se llamefree
. Si no encuentra estos bytes, llama OSPanic
y el juego se congela.Incapaz de influir en la presencia de estos bytes en alguna ubicación de destino, no es posible escribir aquí. En otras palabras, es imposible grabar algo en un lugar arbitrario sin poder grabar algo cerca de este lugar. Puede haber alguna forma de hacer que el valor 0x73730000
almacenado en la pila directamente en frente de la dirección de retorno y el lugar al que se refiere el valor que queremos escribir en la dirección de destino (también se verificará como si fuera un puntero a un bloque de montón), pero esto es difícil de lograr y usar esto en un exploit.nesinfo_update_highscore
Otra función con respecto a las etiquetas QDS, BBR y HSC es esta nesinfo_update_highscore
. Los tamaños de las marcas QDS, BBR y OFS (desplazamiento) se utilizan para calcular el desplazamiento en el que grabar, y la marca HSC incluye la grabación en esa ubicación. Esta función se realiza para cada cuadro procesado por el emulador NES.El valor de desplazamiento máximo para cada etiqueta en este caso, incluso para QDS, es igual 0xFFFF
. Sin embargo, durante el ciclo de procesamiento de etiquetas, los valores de dimensión de las etiquetas BBR y QDS realmente se acumulan . Esto significa que se pueden usar varias marcas para calcular casi cualquier valor de desplazamiento. La limitación es el número de etiquetas que pueden caber en la sección de datos de las etiquetas ROM en un archivo en una tarjeta de memoria, y también tiene un tamaño máximo 0xFFFF
.La dirección base a la que se agrega el desplazamiento es el 0x800C3180
búfer de guardar datos. Esta dirección es mucho más baja que los datos ROM, lo que nos da más libertad para elegir una ubicación de grabación. Por ejemplo, será bastante simple reescribir la dirección de retorno en la pila a la dirección 0x812F95DC
.Lamentablemente, esto tampoco funcionó. Resulta que nesinfo_tag_process1
también verifica el tamaño acumulado de las compensaciones de estas etiquetas, y usa este tamaño para inicializar el espacio: bzero(nintendo_hi_0, ((offset_sum + 0xB) * 4) + 0x40)
Con el valor de compensación que estaba tratando de calcular, esto condujo al hecho de que 0x48D91EC
(76,386,796) bytes de memoria se borraron, por lo que el juego se bloqueó espectacularmente.Marca PAT
Ya había comenzado a perder la esperanza, porque todas estas etiquetas que hacían llamadas desprotegidas memcpy
habían fallado incluso antes de que pudiera usarlas. Decidí documentar el propósito de cada etiqueta y gradualmente llegué a las etiquetas nesinfo_tag_process2
.La mayoría de los manejadores de etiquetas nesinfo_tag_process2
nunca se inician, porque solo funcionan cuando el puntero nesinfo_rom_start
no es cero. Nada en el código asigna un valor distinto de cero a este puntero. Se inicializa con un valor nulo y nunca se vuelve a usar. Cuando se carga la ROM solo está configurada nesinfo_data_start
, por lo que parece un código muerto.Sin embargo, hay una etiqueta que todavía puede funcionar cuando no es cero nesinfo_rom_start
: PAT. Esta es la etiqueta más difícil en una función nesinfo_tag_process2
.También se usa como puntero nesinfo_rom_start
, pero nunca lo verifica por cero. La etiqueta PAT lee su propio búfer de datos de etiqueta, procesando códigos que calculan las compensaciones. Estos desplazamientos se agregan al puntero nesinfo_rom_start
para calcular la dirección de destino, y luego los bytes se copian desde el búfer de parches a esta ubicación. Esta copia se realiza cargando y guardando bytes, no usando instrucciones memcpy
, por lo que no lo he notado antes.Cada búfer de datos de marca PAT tiene un código de tipo de 8 bits, un tamaño de parche de 8 bits y un valor de desplazamiento de 16 bits, seguido de los datos del parche.- Si el código es 2, el valor de compensación se agrega a la suma actual de las compensaciones.
- Si el código es 9, el desplazamiento se desplaza 4 bits hacia arriba y se agrega a la suma actual de los desplazamientos.
- Si el código es 3, la suma de las compensaciones se restablece a 0.
El tamaño máximo de la etiqueta de información NES es 255, es decir, el tamaño de parche PAT más grande es 251 bytes. Sin embargo, se pueden usar múltiples marcas PAT, es decir, puede parchear más de 251 bytes, así como parchear espacios no contiguos.Mientras tengamos una serie de suelas PAT con código 2 o código 9, el desplazamiento del puntero de destino continúa acumulándose. Al copiar los datos del parche, se restablece a cero, pero si usa un tamaño de parche cero, esto se puede evitar. Está claro que esto puede usarse para calcular algún desplazamiento arbitrario con un puntero nulo nesinfo_rom_start
usando muchas marcas PAT.Sin embargo, hay dos comprobaciones más de valores de código ...- Si el código está entre
0x80
y 0xFF
, entonces se agrega a 0x7F80
, y luego se desplaza hacia arriba 16 bits. Luego se agrega al valor de desplazamiento de 16 bits y se usa como la dirección final del parche.
¡Esto nos permite asignar una dirección de destino para el parche en el rango de 0x80000000
a 0x807FFFFF
! Aquí es donde reside la mayor parte del código de Animal Crossing en la memoria. Esto significa que podemos parchear el código de Animal Crossing usando etiquetas de metadatos ROM de un archivo en una tarjeta de memoria.Con la ayuda de un pequeño cargador de parches, incluso puede descargar fácilmente parches más grandes de una tarjeta de memoria a cualquier dirección.Como comprobación rápida, creé un parche que incluía "zuru mode 2" (modo desarrollador de juegos, descrito en mi artículo anterior) cuando un usuario carga una ROM desde un mapa del juego. Resultó que el combo de trucos de las teclas solo activa el modo "zuru mode 1", que no tiene acceso a las funciones que tiene el Modo 2. Con este parche, gracias a la tarjeta de memoria, podemos obtener acceso completo al modo desarrollador en hardware real.Las marcas de parche se procesarán cuando se inicie la ROM.Después de cargar ROM, debe salir del emulador NES para ver el resultado.Funciona!
Formato de etiqueta de información de parche
Las marcas de información en el archivo de guardar que ejecuta este parche se ven así:000000 5a 5a 5a 00 50 41 54 08 a0 04 6f 9c 00 00 00 7d >ZZZ.PAT...o....}<
000010 45 4e 44 00 >END.<
ZZZ \x00
: marca de inicio ignorada. 0x00
Es el tamaño de su búfer de datos: cero.PAT \x08 \xA0 \x04 \x6F\x9C \x00\x00\x00\x7D
: Parche 0x80206F9C
en 0x0000007D
.
0x08
Es el tamaño del búfer de etiquetas.0xA0
cuando se agrega a se 0x7F80
convierte 0x8020
, es decir, los 16 bits superiores de la dirección de destino.0x04
Es el tamaño de los datos del parche ( 0x0000007D
).0x6F9C
Son los 16 bits inferiores de la dirección de destino.0x0000007D
Son los datos del parche.
END \x00
: marca de marcador final.
Si desea experimentar por su cuenta con la creación de un parche o archivos de almacenamiento ROM, entonces en https://github.com/jamchamb/ac-nesrom-save-generator publiqué un código muy simple para generar archivos. Se puede generar un parche como el que se muestra arriba con el siguiente comando:$ ./patcher.py Patcher /dev/null zuru_mode_2.gci -p 80206F9c 0000007D
Ejecución de código arbitrario
Gracias a esta etiqueta, puede lograr la ejecución de código arbitrario en Animal Crossing.Pero aquí llega el último obstáculo: el uso de parches para datos funciona bien, pero surgen problemas al parchear las instrucciones del código.Cuando se registran parches, el juego continúa siguiendo las viejas instrucciones que estaban en su lugar. Esto parece un problema de almacenamiento en caché, y en realidad lo es. La CPU GameCube tiene cachés de instrucciones, como se describe en las especificaciones .Para comprender cómo puede borrar el caché, comencé a estudiar las funciones relacionadas con el caché de la documentación del SDK de GameCube y descubrí ICInvalidateRange
. Esta función invalida los bloques de instrucciones en caché en la dirección de memoria especificada, lo que permite que la memoria de instrucciones modificada se ejecute con código actualizado.Sin embargo, sin la capacidad de ejecutar el código original, aún no podemos llamar ICInvalidateRange
. Para una ejecución exitosa del código, necesitamos un truco más.Al estudiar la implementación malloc
para la posibilidad de usar un exploit con desbordamiento de montón, aprendí que las funciones de implementación malloc
se pueden deshabilitar dinámicamente usando una estructura de datos llamada my_malloc
. my_malloc
carga un puntero a la implementación actual malloc
o free
desde un lugar estático en la memoria, y luego llama a esta función, pasando todos los argumentos pasados my_malloc
.El emulador NES usa activamentemy_malloc
para asignar y liberar memoria para datos NES relacionados con ROM, por lo que estaba seguro de que se lanzaría varias veces aproximadamente al mismo tiempo que las marcas PAT.Como my_malloc
carga un puntero desde la memoria y hace una transición a él, puedo cambiar el proceso de ejecución del programa simplemente sobrescribiendo el puntero para que apunte a la función actual malloc
o free
. El almacenamiento en caché de herramientas no evitará que esto suceda, ya que no es necesario cambiar las instrucciones my_malloc
.El desarrollador del proyecto Dōbutsu no Mori e + fan, llamado Cuyler, escribió dicho cargador en el ensamblador PowerPC y demostró su uso para inyectar nuevo código en este video: https://www.youtube.com/watch?v=BdxN7gP6WIc. (Dōbutsu no Mori e + fue la última iteración de Animal Crossing en GameCube, que tuvo la mayor cantidad de actualizaciones. Lanzado solo en Japón). El parche descarga un código que permite al jugador crear cualquier objeto ingresando su ID por letra y presionando el botón Z.Gracias a esto, puedes descargar mods, trucos y homebrew en una copia regular de AnimalCrossing en un GameCube real.