Enquanto procurava maneiras de ativar os menus do desenvolvedor deixados em Animal Crossing, incluindo o menu de seleção de jogos para o emulador NES, encontrei um recurso interessante que existe no jogo original e estava constantemente ativo, mas nunca foi usado pela Nintendo.
Além dos jogos NES / Famicom no jogo, você pode baixar novos jogos NES a partir de um cartão de memória.
Também consegui encontrar uma maneira de usar esse carregador de inicialização da ROM para corrigir meu código e dados no jogo, o que permite executar o código através de um cartão de memória.
Introdução - Objetos do Console NES
Jogos comuns de NES, que podem ser obtidos na Animal Crossing, são móveis separados na forma de um console da NES com um cartucho sobre ele.
Tendo localizado este objeto em sua casa e interagindo com ele, você pode executar este único jogo. A imagem abaixo mostra Excitebike e Golf.
Há também um objeto comum do NES Console no qual não há jogos internos. Ele pode ser comprado na Redd e, às vezes, obtido através de eventos aleatórios, por exemplo, lendo no quadro de avisos da cidade que o console está enterrado em um ponto aleatório da cidade.
Esse objeto se parece com um console NES no qual não há cartuchos.
O problema com esse objeto é que ele era considerado impossível de jogar. Toda vez que você interage com ele, você vê uma mensagem dizendo que não possui um software de jogo.
Acontece que este objeto está realmente tentando digitalizar o cartão de memória em busca de arquivos especialmente projetados contendo imagens de ROM para o NES! O emulador NES usado para executar jogos incorporados parece ser o emulador NES padrão completo para o GameCube e é capaz de iniciar a maioria dos jogos.
Antes de demonstrar esses recursos, explicarei o processo de engenharia reversa deles.
Pesquisar ROM bootloader no cartão de memória
Estamos à procura de um menu de desenvolvedor
Inicialmente, eu queria encontrar um código que ative vários menus do desenvolvedor, como o menu de seleção de mapa ou o menu de seleção de jogos do emulador NES. O menu
Forest Map Select , graças ao qual você pode facilmente carregar instantaneamente diferentes locais do jogo, foi bastante simples de encontrar - procurei a linha FOREST MAP SELECT que aparece na parte superior da tela (pode ser vista em diferentes vídeos e capturas de tela na Internet )
No "FOREST MAP SELECT", há referências cruzadas de dados para a função
select_print_wait
, o que leva a várias outras funções que também têm o prefixo
select_*
, incluindo a função
select_init
. Acabaram sendo funções que controlam o menu de seleção de mapa.
A função
select_init
leva a outra função interessante chamada
game_get_next_game_dlftbl
. Esta função une todos os outros menus e “cenas” que você pode executar: uma tela com o logotipo da Nintendo, a tela principal, o menu de seleção de cartões, o menu do emulador NES (Famicom) e assim por diante. Inicia no início do procedimento principal do jogo, localiza qual função de inicialização de cena deve ser executada e encontra sua entrada na estrutura de dados da tabela chamada
game_dlftbls
. Esta tabela contém links para as funções de processamento de várias cenas, bem como alguns outros dados.
Um estudo cuidadoso do primeiro bloco da função mostrou que ela carrega a função "next game init" e começa a compará-la com uma série de funções init bem conhecidas:
first_game_init
select_init
play_init
second_game_init
trademark_init
player_select_init
save_menu_init
famicom_emu_init
prenmi_init
Um dos indicadores de função que ele está procurando é
famicom_emu_init
, responsável pela execução do emulador NES / Famicom.
game_get_next_game_init
resultado
game_get_next_game_init
a
famicom_emu_init
ou
select_init
no depurador Dolphin, consegui exibir menus especiais. O próximo passo é determinar como esses ponteiros são configurados da maneira normal durante a execução do programa. A única coisa que a função
game_get_next_game_init
é carregar o valor no deslocamento
0xC
primeiro argumento no
game_get_next_game_dlftbl
.
Manter o controle desses valores definidos em várias estruturas de dados foi um pouco chato, então irei direto ao núcleo. A coisa mais importante que encontrei:
- Quando o jogo começa da maneira usual, ele executa a seguinte sequência de ações:
first_game_init
second_game_init
trademark_init
play_init
player_select_init
define o próximo init como select_init
. Essa tela deve permitir que você selecione um jogador imediatamente após escolher uma carta, mas parece que ela não funciona corretamente.
Também encontrei uma função sem nome que define a função init do emulador, mas não encontrei nada que defina a função init com o valor init do jogador ou da escolha do mapa.
Nesse ponto, percebi que tinha outro problema estúpido na maneira como carregava nomes de funções no IDA: devido à expressão regular usada para cortar linhas no arquivo de símbolo de depuração, perdi todos os nomes de funções começando com uma letra maiúscula . A função que o
famicom_emu_init
configurou parecia transições entre cenas e, é claro, foi chamada de
Game_play_fbdemo_wipe_proc
.
Game_play_fbdemo_wipe_proc
lida com transições entre cenas, como apagamentos de tela e apagões.
Sob certas condições, a transição da tela foi realizada da jogabilidade usual para a exibição do emulador. Foi ele quem definiu a função init do emulador.
Manipulando objetos do console
Na verdade, manipuladores de objetos de móveis para consoles NES fazem com que o manipulador de transição de tela mude para o emulador. Quando um jogador interage com um dos consoles,
aMR_FamicomEmuCommonMove
é
aMR_FamicomEmuCommonMove
.
Ao chamar a função,
r6
contém o valor do índice correspondente aos números nos nomes dos arquivos do jogo NES em
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
é um formato de arquivo proprietário).
Quando
r6
não
r6
igual a zero, é passado na chamada
aMR_RequestStartEmu
. Nesse caso, a transição para o emulador é acionada.
No entanto, se
r6
for zero, a função
aMR_RequestStartEmu_MemoryC
será chamada. Definindo o valor no depurador como 0, recebi a mensagem "Não tenho nenhum software". Não lembrei imediatamente que precisava verificar o objeto Console do NES para garantir que ele redefinisse o valor
r6
, mas o índice zero é usado para o objeto do console sem cartucho.
Embora
aMR_RequestStartEmu
simplesmente armazene o valor do índice em algum tipo de estrutura de dados,
aMR_RequestStartEmu_MemoryC
executa operações muito mais complexas ...
Esse terceiro bloco de código chama
aMR_GetCardFamicomCount
e verifica um resultado diferente de zero, caso contrário, ignora a maioria das coisas interessantes no lado esquerdo do gráfico de funções.
aMR_GetCardFamicomCount
chama
famicom_get_disksystem_titles
, que então chama
memcard_game_list
, e aqui tudo se torna muito interessante.
memcard_game_list
monta o cartão de memória e começa a circular no ciclo de gravação do arquivo, verificando cada um de alguns valores. Ao rastrear a função no depurador, eu pude entender que ele estava comparando os valores com cada um dos meus arquivos no cartão de memória.
A função decide se deve ou não fazer o download do arquivo, dependendo dos resultados da verificação de várias linhas. Primeiro, verifica a presença das linhas "GAFE" e "01", que são os identificadores do jogo e da empresa. 01 significa Nintendo, GAFE significa Animal Crossing. Eu acho que significa GameCube Animal Forest English.
Ela então verifica as linhas "DobutsunomoriP_F_" e "SAVE". Nesse caso, a primeira linha deve corresponder, mas não a segunda. Descobriu-se que "DobutsunomoriP_F_SAVE" é o nome do arquivo que armazena os dados dos jogos incorporados para o NES. Portanto, todos os arquivos, exceto este, serão carregados com o prefixo “DobutsunomoriP_F_”.
Usando o depurador Dolphin para ignorar as comparações de seqüências de caracteres com “SAVE” e fazendo o truque do jogo para acreditar que meu arquivo “SAVE” pode ser baixado com segurança, cheguei a este menu depois de usar o console NES:
Respondi "Sim" e tentei carregar o arquivo salvo como um jogo, depois do qual vi pela primeira vez a tela de travamento do jogo:
Ótimo! Agora eu sei que ela está realmente tentando baixar jogos de um cartão de memória e posso começar a analisar o formato dos arquivos salvos para ver se uma ROM real pode ser baixada.
A primeira coisa que tentei fazer foi tentar descobrir onde o nome do jogo é lido no arquivo do cartão de memória. Pesquisando a linha "FEFSC" que estava presente na mensagem "Deseja reproduzir <name>?", Encontrei o deslocamento em que foi lido no arquivo:
0x642
. Copiei o arquivo salvo, alterei o nome do arquivo para “DobutsunomoriP_F_TEST”, alterei os bytes no deslocamento
0x642
para “TESTING” e importei o salvamento alterado, após o qual o nome que eu precisava aparecia no menu.
Depois de adicionar mais alguns arquivos neste formato, mais algumas opções apareceram no menu:
Download da ROM
Se
aMR_GetCardFamicomCount
retornado diferente de zero, a memória é alocada no heap,
famicom_get_disksystem_titles
é chamado diretamente
famicom_get_disksystem_titles
, após o qual um monte de compensações aleatórias são especificadas na estrutura de dados. Em vez de decifrar onde esses valores serão lidos, comecei a estudar a lista de funções
famicom
.
Acabou que eu precisava de
famicom_rom_load
. Ele controla o carregamento da ROM, a partir de um cartão de memória ou de recursos internos do jogo.
A coisa mais importante neste bloco de "inicialização a partir do cartão de memória" é que ele chama
memcard_game_load
. Ela monta o arquivo no cartão de memória novamente, lê e analisa. É aqui que as opções mais importantes de formato de arquivo se tornam aparentes.
Valor de soma de verificação
A primeira coisa que acontece após o upload do arquivo é o cálculo da soma de verificação. A função
calcSum
é
calcSum
, que é um algoritmo muito simples que soma os valores de todos os bytes nos dados do cartão de memória. Os oito bits inferiores do resultado devem ser zero. Ou seja, para passar nessa verificação, você precisa somar os valores de todos os bytes no arquivo de origem, calcular o valor que precisa ser adicionado para que os oito bits inferiores se tornem zero e atribuir esse valor ao byte da soma de verificação no arquivo.
Se a verificação falhar, você receberá uma mensagem sobre a impossibilidade de ler corretamente o cartão de memória e nada acontece. Durante a depuração, tudo o que preciso fazer é pular essa verificação.
Copiar ROM
Perto do final de
memcard_game_load
, outra coisa interessante acontece. Existem vários blocos de código mais interessantes entre ele e a soma de verificação, mas nenhum deles leva a ramificações que ignoram a execução desse comportamento.
Se um determinado valor inteiro de 16 bits lido no cartão de memória não for igual a zero, é chamada uma função que verifica o cabeçalho de compactação no buffer. Ele verifica os formatos proprietários de compactação da Nintendo, observando o início do buffer Yay0 ou Yaz0. Se uma dessas linhas for encontrada, a função desempacotar é chamada. Caso contrário, é executada uma função de cópia simples. De qualquer forma, depois disso, uma variável chamada
nesinfo_data_size
.
Outra dica de contexto aqui é que os arquivos ROM para jogos NES incorporados usam a compactação Yaz0, e essa linha está presente nos cabeçalhos de seus arquivos.
Depois de observar o valor verificado como zero e o buffer passado para as funções de verificação de compactação, descobri rapidamente de onde o jogo estava sendo lido no arquivo do cartão de memória. A verificação zero é realizada para parte do buffer de 32 bytes copiado do deslocamento
0x640
no arquivo, que provavelmente é o cabeçalho da ROM. Esta função também verifica outras partes do arquivo, e é nelas que o nome do jogo está localizado (começando com o terceiro byte do cabeçalho).
No caminho de execução de código que encontrei, o buffer da ROM está localizado imediatamente após esse buffer de cabeçalho de 32 bytes.
Esta informação é suficiente para tentar criar um arquivo ROM funcional. Eu apenas peguei um dos outros arquivos salvos do Animal Crossing e editei-o em um editor hexadecimal para substituir o nome do arquivo por
DobutsunomoriP_F_TEST
e limpar todas as áreas onde eu queria colar os dados.
Para uma execução de teste, usei a ROM do jogo de pinball, que já está no jogo, e inseri seu conteúdo após o cabeçalho de 32 bytes. Em vez de calcular o valor da soma de verificação, defino pontos de interrupção para simplesmente ignorar o
calcSum
e também observar os resultados de outras verificações que podem levar a uma ramificação que ignora o processo de inicialização da ROM.
Por fim, importei o novo arquivo através do gerenciador de cartões de memória Dolphin, reiniciei o jogo e tentei iniciar o console.
Funcionou! Havia alguns pequenos bugs gráficos relacionados aos parâmetros do Dolphin, que afetavam o modo gráfico usado pelo emulador NES, mas, em geral, o jogo teve um bom desempenho. (Nas versões mais recentes do Dolphin, ele deve funcionar por padrão.)
Para garantir que outros jogos também iniciem, tentei escrever várias outras ROMs que não estavam no jogo. Os battletoads começaram, mas pararam de funcionar após o texto da tela inicial (após outras configurações, consegui torná-lo jogável). Mega Man, por outro lado, funcionou perfeitamente:
Para aprender a gerar novos arquivos ROM que poderiam ser carregados sem a intervenção de depuradores, tive que começar a escrever código e entender melhor a análise de formato de arquivo.
Formato de arquivo ROM externo
A parte mais importante da análise de arquivos acontece em
memcard_game_load
. Existem seis seções principais de blocos de análise de código nesta função:
- Soma de verificação
- Salvar nome do arquivo
- Cabeçalho do arquivo ROM
- Buffer desconhecido copiado sem qualquer processamento
- Comentário de texto, ícone e carregador de banner (para criar um novo arquivo salvo)
- Carregador de inicialização ROM
Soma de verificação
Os oito bits inferiores da soma de todos os valores de bytes no arquivo salvo devem ser zero. Aqui está um código Python simples que gera o byte de soma de verificação necessário:
checksum = 0 for byte_val in new_data_tmp: checksum += byte_val checksum = checksum % (2**32)
Provavelmente existe um local especial para armazenar o byte da soma de verificação, mas adicioná-lo ao espaço vazio no final do arquivo salvo funciona muito bem.
Nome do arquivo
Novamente, o nome do arquivo salvo deve começar com "DobutsunomoriP_F_" e terminar com algo que não contenha "SAVE". Este nome de arquivo é copiado algumas vezes e, em um caso, a letra "F" é substituída por "S". Este será o nome dos arquivos salvos para o jogo NES ("DobutsunomoriP_S_NAME").
Cabeçalho ROM
Uma cópia direta do cabeçalho de 32 bytes é carregada na memória. Alguns dos valores neste cabeçalho são usados para determinar como lidar com as seções subseqüentes. Basicamente, esses são alguns valores de tamanho de 16 bits e bits de parâmetro compactados.
Se você rastrear o ponteiro copiado pelo cabeçalho até o início da função e encontrar a posição de seu argumento, a assinatura da função abaixo mostrará que ele realmente possui o tipo
MemcardGameHeader_t*
.
memcard_game_load(unsigned char *, int, unsigned char **, char *, char *, MemcardGameHeader_t *, unsigned char *, unsigned long, unsigned char *, unsigned long)
Buffer desconhecido
Verifica o valor do tamanho de 16 bits no cabeçalho. Se não for igual a zero, o número correspondente de bytes será copiado diretamente do buffer do arquivo para um novo bloco de memória alocada. Isso move o ponteiro de dados no buffer do arquivo para que outras cópias possam continuar na próxima seção.
Banner, ícone e comentário
Outro valor de tamanho é verificado no cabeçalho e, se não for igual a zero, a função de verificação de compactação de arquivos é chamada. Se necessário, o algoritmo de descompactação será iniciado, após o qual
SetupExternCommentImage
será
SetupExternCommentImage
.
Esta função faz três coisas: “comentário”, imagem e ícone do banner. Para cada um deles, há um código no cabeçalho da ROM mostrando como lidar com eles. Existem as seguintes opções:
- Usar valor padrão
- Copie da seção banner / ícone / comentário no arquivo ROM
- Copiar do buffer alternativo
Os valores padrão do código fazem com que o ícone ou banner seja carregado a partir do recurso em disco, e o nome do arquivo e do comentário salvos (descrição em texto do arquivo) recebe os valores "Animal Crossing" e "NES Cassette Save Data". Aqui está o que parece:
O segundo valor do código simplesmente copia o nome do jogo do arquivo ROM (uma alternativa para "Animal Crossing") e, em seguida, tenta encontrar a string "] ROM" no comentário do arquivo e substitui-a por "] SAVE". Aparentemente, os arquivos que a Nintendo queria lançar deveriam estar no formato dos nomes "Game Name [NES] ROM" ou algo semelhante.
Para o ícone e o banner, o código tenta determinar o formato da imagem, obter um valor de tamanho fixo correspondente a esse formato e copiar a imagem.
No último valor de código, o nome e a descrição do arquivo são copiados sem alterações do buffer, e o ícone e o banner também são carregados do buffer alternativo.
ROM
Se você observar atentamente a captura de tela da
memcard_game_load
copiando ROM, poderá ver que o valor de 16 bits verificado quanto à igualdade a zero é deslocado para a esquerda em 4 bits (multiplicado por 16) e, em seguida, é usado como o tamanho da função
memcpy
se a compactação não for detectada. Este é outro valor de tamanho presente no cabeçalho.
Se o tamanho não for igual a zero, os dados da ROM serão verificados quanto à compactação e copiados.
Pesquisa desconhecida de buffer e bug
Embora o download de novas ROMs seja bastante curioso, a coisa mais interessante sobre esse carregador de ROM para mim foi que, de fato, essa é a única parte do jogo que recebe entrada do usuário de tamanho variável e a copia para diferentes locais de memória. Quase todo o resto usa buffers de tamanho constante. Coisas como nomes e textos de letras podem parecer diferentes em tamanho, mas essencialmente o espaço vazio é simplesmente preenchido com espaços. As seqüências terminadas em zero são usadas com pouca frequência, evitando erros comuns de corrupção de memória, como usar
strcpy
com um buffer muito pequeno para copiar as seqüências.
Eu estava muito interessado na possibilidade de encontrar uma exploração do jogo com base em arquivos salvos, e parecia que essa era a melhor opção.
A maioria das operações de arquivo ROM descritas acima usam cópias de tamanho constante, com exceção de um buffer desconhecido e dados da ROM. Infelizmente, o código que processa esse buffer aloca exatamente o espaço necessário para copiá-lo, para que não haja excesso e a configuração de tamanhos de arquivo ROM muito grandes não foi muito útil.
Mas eu ainda queria saber o que acontece com esse buffer, que é copiado sem nenhum processamento.
Manipuladores de informações da NES
Voltei para
famicom_rom_load
. Após carregar a ROM de um cartão ou disco de memória, várias funções são chamadas:
nesinfo_tag_process1
nesinfo_tag_process2
nesinfo_tag_process3
Depois de rastrear o local em que o buffer desconhecido é copiado, certifiquei-me de que esta tarefa seja executada por essas funções. Eles começam com uma chamada para
nesinfo_next_tag
, que executa um algoritmo simples:
- Verifica se o ponteiro especificado
nesinfo_tags_end
ponteiro em nesinfo_tags_end
. Se for menor que nesinfo_tags_end
ou nesinfo_tags_end
for zero, nesinfo_tags_end
a presença da string "END" no cabeçalho do ponteiro.
- Se "END" for atingido ou o ponteiro tiver subido para ou acima de
nesinfo_tags_end
, a função retornará nulo. - Caso contrário, o byte no deslocamento
0x3
ponteiro é adicionado a 4 e ao ponteiro atual, após o qual o valor é retornado.
Isso nos diz que existe algum tipo de formato de etiqueta de um nome de três letras, valor do tamanho dos dados e dos próprios dados. O resultado é um ponteiro para o próximo rótulo, porque o rótulo atual é ignorado (
cur_ptr + 4
ignora o nome de três letras e um byte e
size_byte
ignora os dados).
Se o resultado não for zero, a função de processamento de rótulo realiza uma série de comparações de strings para descobrir qual rótulo precisa ser processado. Alguns dos nomes de rótulos marcados em
nesinfo_tag_process1
: VEQ, VNE, GID, GNO, BBR e QDS.
Se uma correspondência de etiqueta for encontrada, algum código do manipulador será executado. Alguns manipuladores não fazem nada além de exibir um rótulo na mensagem de depuração. Outros têm manipuladores mais complexos. Após o processamento do rótulo, a função tenta obter o próximo rótulo e continuar o processamento.
Felizmente, existem muitas mensagens detalhadas de depuração que são exibidas quando as tags são detectadas.
Eles são todos em japonês, portanto devem ser decodificados a partir do Shift-JIS e traduzidos. Por exemplo, uma mensagem para o QDS pode ler “Carregando uma área de economia de disco” ou “Como esta é a primeira execução, crie uma área de economia de disco”. As mensagens para o BBR dizem "carregando um backup da bateria" ou "como este é o primeiro início, realizamos uma limpeza".Esses dois códigos também carregam alguns valores da seção de dados de seus rótulos e os utilizam para calcular o deslocamento nos dados da ROM, após o qual eles executam operações de cópia. Obviamente, eles são responsáveis por determinar as partes na memória ROM associadas à preservação do estado.Há também uma marca "HSC" com uma mensagem de depuração dizendo que está processando registros de pontos. Ela obtém um deslocamento na ROM de seus dados de tag, bem como o valor do registro de pontuação original. Essas marcas podem ser usadas para indicar um lugar na memória do jogo NES para armazenar recordes, possivelmente para salvá-los e restaurá-los no futuro.Essas tags criam um sistema de download de metadados de ROM bastante complexo. Além disso, muitos deles levam a chamadas com memcpy
base nos valores transmitidos nos dados da etiqueta.Caça de insetos
A maioria das tags que levam à manipulação de memória não é muito útil para explorações, porque todas elas têm valores máximos de deslocamento e tamanho especificados como números inteiros de 16 bits. Isso é suficiente para trabalhar com o espaço de endereço do NES de 16 bits, mas não o suficiente para gravar valores de destino úteis, como ponteiros para funções ou endereços de retorno na pilha no espaço de endereço do GameCube de 32 bits.No entanto, existem vários casos em que os valores dos desvios de tamanho transmitidos memcpy
podem exceder 0xFFFF
.QDS
O QDS carrega um deslocamento de 24 bits de seus dados de tag, bem como um valor de tamanho de 16 bits.O bom aqui é que o deslocamento é usado para calcular o endereço de destino da operação de cópia. O endereço base do deslocamento é o início dos dados baixados, a origem da cópia está no arquivo ROM do cartão de memória e o tamanho é definido pelo valor do tamanho de 16 bits da etiqueta.O valor de 24 bits possui um valor máximo 0xFFFFFF
, que é muito mais do que o necessário para gravar fora dos dados ROM carregados. No entanto, existem alguns problemas ...O primeiro é que, embora o valor máximo do tamanho seja igual 0xFFFF
, ele é usado inicialmente para redefinir a partição de memória. Se o valor do tamanho for muito alto (não muito maior 0x1000
), isso redefinirá a marca “QDS” no código do jogo.E aí está o problema, porque nesinfo_tag_process1
na verdade é chamado duas vezes. Pela primeira vez, ela recebe algumas informações sobre o espaço necessário para se preparar para os dados armazenados. As tags QDS e BBR não são totalmente processadas na primeira execução. Após a primeira execução, um local é preparado para salvar os dados e a função é chamada novamente. Desta vez, as tags QDS e BBR são totalmente processadas, mas se as cadeias de nomes de tags forem apagadas da memória, será impossível corresponder as tags novamente!Isso pode ser evitado definindo um valor de tamanho menor. Outro problema é que o valor de deslocamento só pode avançar na memória e os dados ROM NES estão localizados no heap bem perto do final da memória disponível.Depois deles, existem apenas alguns montes, e nenhum deles tem algo particularmente útil, como indicadores óbvios de funções.No caso normal, você pode usar isso para explorar um estouro de heap, mas na implementação malloc
usada para esse heap, foram adicionados alguns bytes de verificações de saúde em blocos malloc
. Podemos escrever sobre valores de ponteiro em blocos de heap subsequentes. Sem verificações de integridade, isso poderia ser usado para gravar em uma área de memória arbitrária quando chamado free
por um bloco de heap envolvido.No entanto, a implementação usada aqui malloc
verifica um padrão de bytes específico ( 0x7373
) no início dos blocos seguintes e anteriores que ele manipulará quando chamadofree
. Se ela não encontrar esses bytes, ela liga OSPanic
e o jogo congela.Não é possível influenciar a presença desses bytes em algum local de destino, não é possível escrever aqui. Em outras palavras, é impossível gravar algo em um local arbitrário sem poder gravar algo próximo a esse local. Pode haver alguma maneira de tornar o valor 0x73730000
armazenado na pilha diretamente em frente ao endereço de retorno e o local ao qual o valor se refere, ao qual queremos gravar no endereço de destino (ele também será verificado como se fosse um ponteiro para um bloco de pilha), mas isso é difícil conseguir e usar isso em uma exploração.nesinfo_update_highscore
Outra função referente às tags QDS, BBR e HSC é essa nesinfo_update_highscore
. Os tamanhos das marcas QDS, BBR e OFS (deslocamento) são usados para calcular o deslocamento no qual gravar, e a marca HSC inclui a gravação nesse local. Essa função é executada para cada quadro processado pelo emulador NES.O valor máximo de deslocamento para cada etiqueta nesse caso, mesmo para QDS, é igual 0xFFFF
. No entanto, durante o ciclo de processamento de etiquetas, os valores de dimensão das etiquetas BBR e QDS realmente se acumulam . Isso significa que várias marcas podem ser usadas para calcular quase qualquer valor de deslocamento. A limitação é o número de etiquetas que podem caber na seção de dados das etiquetas da ROM em um arquivo no cartão de memória e também possui um tamanho máximo 0xFFFF
.O endereço base ao qual o deslocamento é adicionado é o 0x800C3180
buffer de dados salvos. Esse endereço é muito menor que os dados da ROM, o que nos dá mais liberdade na escolha de um local de gravação. Por exemplo, será bastante simples reescrever o endereço de retorno na pilha no endereço 0x812F95DC
.Infelizmente, isso também não funcionou. Acontece que ele nesinfo_tag_process1
também verifica o tamanho acumulado das compensações desses rótulos e usa esse tamanho para inicializar o espaço: bzero(nintendo_hi_0, ((offset_sum + 0xB) * 4) + 0x40)
Com o valor de deslocamento que eu estava tentando calcular, isso levou ao fato de que 0x48D91EC
(76.386.796) bytes de memória foram limpos , e é por isso que o jogo travou espetacularmente.Marca PAT
Eu já tinha começado a perder a esperança, porque todas essas tags que faziam chamadas desprotegidas memcpy
haviam falhado antes mesmo de eu usá-las. Decidi apenas documentar o objetivo de cada tag e, gradualmente, cheguei às tags nesinfo_tag_process2
.A maioria dos manipuladores de etiquetas nesinfo_tag_process2
nunca inicia, porque eles só funcionam quando o ponteiro é nesinfo_rom_start
diferente de zero. Nada no código atribui um valor diferente de zero a esse ponteiro. É inicializado com um valor nulo e nunca é usado novamente. Quando o carregamento da ROM é definido apenas nesinfo_data_start
, parece um código morto.No entanto, há um rótulo que ainda pode funcionar quando diferente de zero nesinfo_rom_start
: PAT. Este é o rótulo mais difícil de uma função nesinfo_tag_process2
.Ele também usa como ponteiro nesinfo_rom_start
, mas nunca verifica como zero. A tag PAT lê seu próprio buffer de dados, processando códigos que calculam as compensações. Esses deslocamentos são adicionados ao ponteiro nesinfo_rom_start
para calcular o endereço de destino e os bytes são copiados do buffer de correção para esse local. Essa cópia é feita carregando e salvando bytes, sem usar instruções memcpy
, por isso não notei isso antes.Cada buffer de dados da marca PAT possui um código de tipo de 8 bits, um tamanho de patch de 8 bits e um valor de deslocamento de 16 bits, seguido pelos dados do patch.- Se o código for 2, o valor do deslocamento será adicionado à soma atual das compensações.
- Se o código for 9, o deslocamento será aumentado em 4 bits e adicionado à soma atual das compensações.
- Se o código for 3, a soma dos deslocamentos será redefinida para 0.
O tamanho máximo do rótulo de informações do NES é 255, ou seja, o maior tamanho do patch PAT é 251 bytes. No entanto, várias marcas PAT podem ser usadas, ou seja, você pode corrigir mais de 251 bytes, bem como corrigir espaços não contíguos.Desde que tenhamos uma série de solas PAT com o código 2 ou 9, o deslocamento do ponteiro de destino continua a se acumular. Ao copiar dados de patch, ele é redefinido para zero, mas se você usar um tamanho de patch zero, isso poderá ser evitado. É claro que isso pode ser usado para calcular algum deslocamento arbitrário com um ponteiro nulo nesinfo_rom_start
usando muitas marcas PAT.No entanto, existem mais duas verificações de valores de código ...- Se o código estiver entre
0x80
e 0xFF
, então ele será adicionado 0x7F80
e aumentado para 16 bits. Em seguida, é adicionado ao valor de deslocamento de 16 bits e usado como o endereço final do patch.
Isso nos permite atribuir um endereço de destino para o patch no intervalo de 0x80000000
a 0x807FFFFF
! É aqui que a maior parte do código Animal Crossing reside na memória. Isso significa que podemos corrigir o próprio código do Animal Crossing usando rótulos de metadados da ROM de um arquivo em um cartão de memória.Com a ajuda de um pequeno carregador de patches, você pode facilmente baixar patches maiores de um cartão de memória para qualquer endereço.Como verificação rápida, criei um patch que incluía o "zuru mode 2" (modo de desenvolvedor de jogos, descrito no meu artigo anterior) quando um usuário carrega uma ROM de um mapa do jogo. Verificou-se que o truque das teclas apenas ativa o modo "zuru mode 1", que não tem acesso às funções do Modo 2. Com esse patch, graças ao cartão de memória, podemos obter acesso completo ao modo de desenvolvedor em hardware real.As marcas de correção serão processadas quando a ROM for inicializada.Após carregar a ROM, você precisa sair do emulador NES para ver o resultado.Isso funciona!
Formato da etiqueta de informações do patch
As marcas de informação no arquivo de salvamento que executam esse patch são assim: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 início ignorada. 0x00
É o tamanho do seu buffer de dados: zero.PAT \x08 \xA0 \x04 \x6F\x9C \x00\x00\x00\x7D
: Correcção 0x80206F9C
no 0x0000007D
.
0x08
É o tamanho do buffer do rótulo.0xA0
quando adicionado a 0x7F80
torna - se 0x8020
, ou seja, os 16 bits superiores do endereço de destino.0x04
É o tamanho dos dados do patch ( 0x0000007D
).0x6F9C
São os 16 bits inferiores do endereço de destino.0x0000007D
São os dados do patch.
END \x00
: marca final.
Se você quiser experimentar sozinho a criação de um patcher ou de arquivos salvos em ROM, em https://github.com/jamchamb/ac-nesrom-save-generator , publiquei um código muito simples para gerar arquivos. Um patch como o mostrado acima pode ser gerado com o seguinte comando:$ ./patcher.py Patcher /dev/null zuru_mode_2.gci -p 80206F9c 0000007D
Execução de código arbitrário
Graças a essa tag, você pode obter a execução arbitrária de código no Animal Crossing.Mas aqui vem o último obstáculo: o uso de patches para dados funciona bem, mas surgem problemas ao aplicar instruções de código.Quando os patches são registrados, o jogo continua seguindo as instruções antigas que estavam no seu lugar. Parece um problema de armazenamento em cache, e na verdade é. A CPU do GameCube possui caches de instruções, conforme descrito nas especificações .Para entender como você pode limpar o cache, comecei a estudar as funções relacionadas ao cache na documentação do GameCube SDK e descobri ICInvalidateRange
. Esta função invalida os blocos de instruções em cache no endereço de memória especificado, o que permite que a memória de instruções modificada seja executada com código atualizado.No entanto, sem a capacidade de executar o código original, ainda não podemos ligar ICInvalidateRange
. Para uma execução bem-sucedida do código, precisamos de mais um truque.Estudando a implementação malloc
para a possibilidade de usar uma exploração com estouro de heap, aprendi que as funções de implementação malloc
podem ser desabilitadas dinamicamente usando uma estrutura de dados chamada my_malloc
. my_malloc
carrega um ponteiro para a implementação atual malloc
ou free
de um local estático na memória e chama essa função, passando todos os argumentos passados para my_malloc
.O emulador NES usa ativamentemy_malloc
para alocar e liberar memória para dados NES relacionados à ROM, então eu tinha certeza de que seria lançado várias vezes mais ou menos ao mesmo tempo que as marcas PAT.Como ele my_malloc
carrega um ponteiro da memória e faz uma transição para ele, eu posso alterar o processo de execução do programa simplesmente substituindo o ponteiro para que aponte para a função atual malloc
ou free
. O armazenamento em cache da ferramenta não impedirá que isso aconteça, porque nenhuma instrução precisa ser alterada my_malloc
.O desenvolvedor do projeto de fã Dōbutsu no Mori e +, chamado Cuyler, escreveu um carregador no PowerPC assembler e demonstrou seu uso para injetar novo código neste vídeo: https://www.youtube.com/watch?v=BdxN7gP6WIc. (Dōbutsu no Mori e + foi a última iteração de Animal Crossing no GameCube, que teve mais atualizações. Lançada apenas no Japão.) O patch faz o download de um código que permite ao jogador criar qualquer objeto digitando seu ID por letra e pressionando o botão Z.Graças a isso, você pode baixar mods, cheats e homebrew em uma cópia regular do AnimalCrossing em um GameCube real.