Engenharia reversa do emulador NES no jogo para GameCube

imagem

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) # keep it 32 bit checkbyte = 256 - (checksum % 256) new_data_tmp[-1] = checkbyte 

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:

  1. Usar valor padrão
  2. Copie da seção banner / ícone / comentário no arquivo ROM
  3. 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 memcpybase 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 memcpypodem 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_process1na 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 mallocusada 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 freepor um bloco de heap envolvido.

No entanto, a implementação usada aqui mallocverifica 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 OSPanice 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 0x73730000armazenado 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 0x800C3180buffer 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_process1també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 memcpyhaviam 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_process2nunca inicia, porque eles só funcionam quando o ponteiro é nesinfo_rom_startdiferente 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_startpara 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_startusando muitas marcas PAT.

No entanto, existem mais duas verificações de valores de código ...

  • Se o código estiver entre 0x80e 0xFF, então ele será adicionado 0x7F80e 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 0x80000000a 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 0x80206F9Cno 0x0000007D.
    • 0x08 É o tamanho do buffer do rótulo.
    • 0xA0quando adicionado a 0x7F80torna - 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 mallocpara a possibilidade de usar uma exploração com estouro de heap, aprendi que as funções de implementação mallocpodem ser desabilitadas dinamicamente usando uma estrutura de dados chamada my_malloc. my_malloccarrega um ponteiro para a implementação atual mallocou freede 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_mallocpara 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_malloccarrega 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 mallocou 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 Animal
Crossing em um GameCube real.

Source: https://habr.com/ru/post/pt417117/


All Articles