Olá pessoal

Apesar da minha grande experiência em reverter jogos para o Sega Mega Drive
, nunca decidi decifrar e eles não me apareceram na Internet. Mas, outro dia, houve um crackie engraçado que queria resolver. Partilho com você a decisão ...
Descrição do produto
A descrição da tarefa e o rum em si podem ser baixados aqui .
Apesar do fato de a lista de recursos indicar Hydra, o padrão de fato entre as ferramentas para depuração e reversão de jogos na Sega é a Smd Ida Tools . Tem tudo o que você precisa para resolver este creme:
- Carregador de rum para Ida
- Depurador
- Ver e alterar a memória RAM / VDP
- Exibir informações quase completas no VDP
Colocamos a versão mais recente nos plugins do Ide e começamos a ver o que temos.
Solução
O lançamento de qualquer jogo Shogi começa com a execução do vetor Reset
. Um ponteiro para ele pode ser encontrado no segundo DWORD desde o início do rum.


Vemos algumas funções não identificadas começando no endereço 0x27A
. Vamos ver o que há lá.
sub_2EA ()

VBLANK
minha própria experiência, direi que isso geralmente parece a função de esperar pela interrupção do VBLANK
. Vamos ver onde mais existem chamadas para a variável byte_FF0026
:

Vemos que o bit zero é apenas definido na interrupção VBLANK
. Então, chamamos a variável vblank_ready
, e a função em que está marcada é wait_for_vblank
.
sub_60E ()
Em seguida, a função sub_60E
é chamada pelo código. Vamos ver o que há lá:

O que o primeiro comando grava no VDP_CTRL
é o comando de controle VDP
. Para descobrir o que ela está fazendo, mantemos esse comando e pressionamos a tecla J
:

Vemos que a entrada no CRAM
(o local onde as paletas estão armazenadas) é inicializada. Isso significa que todo o código de função subsequente simplesmente define algum tipo de paleta inicial. Consequentemente, a função pode ser chamada de init_cram
.
sub_71A ()

Vemos que algum comando é novamente transferido para VDP_CTRL
, depois pressione J
e descobrimos que esse comando inicializa a gravação na memória de vídeo:

Além disso, para entender o que é transferido para a memória de vídeo, não faz sentido. Portanto, simplesmente chamamos a função load_vdp_data
.
sub_C60 ()
Quase o mesmo acontece aqui como na função anterior, portanto, sem entrar em detalhes, simplesmente chamamos a função load_vdp_data2
.
sub_8DA ()
Já existe mais código. Além disso, outra função é chamada nessa função. Vamos dar uma olhada - em sub_D08
.
sub_D08 ()

Vemos que, no registro D0
o comando para VDP_CTRL
, em D1
- o valor com o qual VRAM
será preenchido, e em D2
e D3
- a largura e a altura do preenchimento (porque ocorre dois ciclos: interno e externo). Chame a função fill_vram_by_addr.
sub_8DA ()
Voltamos à função anterior. Depois que o valor no registro D0
for transmitido como um comando para VDP_CTRL
, pressione a tecla J
no valor. Temos:

Novamente, com a experiência de reverter jogos para a Sega, posso dizer que esse comando inicializa a gravação de blocos de mapeamento. Os endereços que começam em $Fxxx
, $Exxx
, $Dxxx
, $Cxxx
em 90% dos casos serão endereços de regiões com esses mesmos mapeamentos. O que são mapeamentos:
esses são os valores com os quais você pode especificar onde exibir esse ou aquele bloco na tela (um bloco é um quadrado de 8x8
pixels).
Portanto, a função pode ser chamada como init_tile_mappings
.
sub_CDC ()

O primeiro comando inicializa o registro no endereço $F000
. Uma observação: entre os endereços do " mapeamento ", ainda existe uma região onde a tabela de sprites está armazenada (essas são suas posições, blocos para os quais apontam etc.) Descubra qual região é responsável pelo que pode ser depurado. Mas, por enquanto, não precisamos disso, então vamos chamar a função init_other_mappings
.
Além disso, vemos que nessa função duas variáveis são inicializadas: word_FF000A
e word_FF000C
. Pela minha própria experiência (sim, ele decide), direi que se duas variáveis estiverem próximas no espaço de endereço e estiverem associadas ao mapeamento, na maioria dos casos elas serão as coordenadas de algum objeto (por exemplo, um sprite). Portanto, sugiro chamá-los de sprite_pos_x
e sprite_pos_y
. O erro em x
e y
permitido, pois ainda mais na depuração, será fácil de corrigir.
VBLANK
Como o loop vai além no código, podemos assumir que concluímos a inicialização básica. Agora você pode ver a interrupção do VBLANK
.

Vemos que duas variáveis estão aumentando (o que é estranho, na lista de links para cada uma delas está absolutamente vazia). Mas, como são atualizados uma vez por quadro, você pode chamá-los de timer1
e timer2
.
Em seguida, a função sub_2FE
é sub_2FE
. Vamos ver o que há lá:
sub_2FE ()

E lá - trabalhe com a porta IO_CT1_DATA
(responsável pelo primeiro joystick). O endereço da porta é carregado no registro A0
e passado para a função sub_310
. Nós vamos lá:
sub_310 ()

Minha experiência me ajuda novamente. Se você vir o código que funciona com o joystick e duas variáveis na memória, uma armazena as pressed keys
e a segunda mantém as held keys
, ou seja, apenas pressione e segure as teclas. Então, vamos chamar essas variáveis: pressed_keys
e held_keys
. E então a função pode ser chamada como update_joypad_state
.
sub_2FE ()
Chame a função como read_joypad
.
Loop do manipulador
Agora tudo parece muito mais claro:

Portanto, esse ciclo responde às teclas pressionadas e executa as ações correspondentes. Vamos analisar cada uma das funções chamadas no loop.
sub_4D4 ()

Há muito código. Vamos começar com a primeira função chamada: sub_60C
.
sub_60C ()
Ela não faz nada - pode parecer tão a princípio. O retorno da função atual é rts
. Mas porque somente saltos ( bsr
) ocorrem nele, o que significa que os rts
nos retornam de volta ao loop do manipulador. Eu chamaria essa função como retn_to_loop
.
sub_4D4 ()
Em seguida, vemos a chamada para a variável word_FF000E
. Ele não é usado em nenhum lugar, exceto na função atual e, a princípio, o objetivo dela não estava claro para mim. Mas, se você observar com atenção, podemos assumir que essa variável é necessária apenas para um pequeno atraso entre o processamento de pressionamentos de tecla. ( Ele já está mal implementado neste rum, mas acho que sem essa variável seria muito pior ).

A seguir, temos uma grande quantidade de código que de alguma forma processa as sprite_pos_y
e sprite_pos_y
, que podem falar apenas de uma coisa - isso é necessário para exibir o sprite de seleção em torno do caractere selecionado no alfabeto.
Portanto, agora você pode nomear com segurança a função como update_selection
. Vamos seguir em frente.

O código verifica se os bits de algumas teclas pressionadas estão configuradas e chama determinadas funções. Vamos olhar para eles.
sub_D28 ()

Algum tipo de magia xamânica. Primeiro, o WORD
é obtido da variável word_FF0018
, em seguida, uma instrução interessante é executada:
bsr.w *+4
Este comando simplesmente salta para a instrução a seguir.
Em seguida é outra mágica:
move.l d0,(sp) rts
O valor no registro D0
é colocado no topo da pilha. Vale ressaltar que, para Shogi, como para alguns x86
, o endereço de retorno da função quando é chamada é colocado no topo da pilha. Consequentemente, a primeira instrução coloca um endereço no topo e a segunda a levanta da pilha e faz uma transição ao longo dela. Bom truque .
Agora você precisa entender qual é esse valor na variável, que passa por isso. Mas primeiro, vamos chamar essa variável jmp_addr
.
E as funções serão chamadas assim:
sub_D38
: goto_to_d0
sub_D28
: jump_to_var_addr
jmp_addr
Descubra onde essa variável é preenchida. Nós olhamos para a lista de referências:

Há apenas um lugar para gravar nessa variável. Vamos olhar para ele.
sub_3A4 ()

Aqui, dependendo da coordenada do sprite (lembre-se de que esse é provavelmente o endereço do caractere selecionado), esse ou aquele valor é inserido. Vemos a seguinte seção de código:

O valor existente é deslocado para a direita em 4 bits, um novo valor é colocado no byte baixo e o resultado é inserido na variável novamente. Em teoria, nossa variável jmp_addr
armazena os caracteres que podemos inserir na tela de introdução de teclas. Observe também que o tamanho da variável é WORD
.
De fato, a função sub_3A4
pode ser chamada de update_jmp_addr
.
sub_414 ()
Agora, temos apenas uma função restante no loop, que não é reconhecida. E é chamado sub_414
.

Seu código se assemelha ao código da função update_jmp_addr
, somente no final temos uma sub_45E
função sub_45E
. Vamos olhar lá.
sub_45E ()

Vemos que o número #$4B1E2003
inserido no registro D0
, que é então enviado para VDP_CTRL
, o que significa que estamos lidando com outro comando de controle VDP
. Pressionamos J
, recebemos um comando de registro na região com o mapeamento de $Cxxx
.
Em seguida, o código funciona com a variável byte_FF0014
, que não é usada em nenhum lugar, exceto na função atual. Se você observar atentamente como é usado, notará que o número máximo que pode ser instalado nele é 4
. Eu suponho que esse seja o comprimento atual da chave inserida. Vamos conferir.
Execute o depurador
Usarei o depurador da Smd Ida Tools
, mas, em essência, alguns Gens KMod ou Gens ReRecording serão suficientes. O principal é que existe um recurso com a exibição de endereços na memória.

Minha teoria foi confirmada. Portanto, a variável byte_FF0014
agora pode ser key_length
.
Há outra variável: dword_FF0010
, que também é usada apenas na função atual, e seu conteúdo, após adicionar ao comando inicial em D0
(lembre-se, esse era o número #$4B1E2003
), é enviado para VDP_CTRL
. Sem pensar add_to_vdp_cmd
, add_to_vdp_cmd
a variável add_to_vdp_cmd
.
Então, o que essa função faz? Eu suponho que ela desenhe o caractere inserido. Verificar isso é simples - iniciando o depurador e comparando o estado antes de chamar a função sub_45E
e depois:
Para:

Depois:

Eu estava certo - esta função desenha o caractere inserido. Nós o chamamos de do_draw_input_char
, e a função que o chama ( sub_414
) é draw_input_char
.
O que agora
Vamos verificar agora que a variável que chamamos de jmp_addr
realmente armazena a chave inserida. Usaremos o mesmo Memory Watch
:

Como você pode ver, a conjectura era verdadeira. O que isso nos dá? Podemos pular para qualquer endereço. Mas qual? Na lista de funções, todas são ordenadas, afinal:

Então comecei a rolar o código até encontrar o seguinte:

O olho treinado viu a sequência de $4E, $75
no final de bytes não alocados. Este é o código de operação da instrução rts
, ou seja, retornar da função. Portanto, esses bytes não alocados podem ser o código de alguma função. Vamos tentar designá-los como um código, pressione C
:

Obviamente, este é um código de função. Você também pode pressionar P
para tornar o código uma função. Lembre-se deste nome: sub_D3C
.
Então surge o pensamento: e se você pular no sub_D3C
? Parece bom, embora um único salto aqui obviamente não seja suficiente, porque não havia mais links para a variável word_FF0020
.
Então outro pensamento veio a mim: e se procurássemos outro código não alocado? Abra a caixa de diálogo Binary search
(Alt + B), digite a sequência 4E 75
, marque a caixa Find all occurrences
:

Clique em
para iniciar a pesquisa, obtemos os seguintes resultados.

Pelo menos mais dois lugares no rum podem conter um código de função, você precisa verificá-los. Clicamos na primeira das opções, rolamos um pouco para cima e novamente vemos uma sequência de bytes indefinidos. Denotá-los como uma função? Sim Pressione P
onde os bytes começam:

Legal! Agora temos a função sub_34C
. Tentamos repetir a mesma coisa com a última das opções encontradas, e ... temos uma chatice. Existem tantos bytes antes de 4E 75
que não está claro onde a função é iniciada. E, obviamente, nem todos esses bytes acima são código, porque muitos bytes duplicados.
Determine o início da função
Será mais fácil encontrar o início da função se descobrirmos onde os dados terminam. Como fazer isso? Na verdade, não é nada complicado:
- Torcemos antes do início dos dados (haverá um link para eles a partir do código)
- Seguimos o link e procuramos um ciclo no qual o tamanho desses dados deve aparecer
- Marque a matriz
Então, executamos o primeiro parágrafo ...:

... e imediatamente vemos que, em um ciclo da nossa matriz, 4 bytes de dados são copiados por vez (porque move.l
) para VDP_DATA
. Em seguida, vemos o número 2047
. A princípio, pode parecer que o tamanho final da matriz seja 2047 * 4
, mas o loop baseado em dbf
executa mais uma iteração, porque O último valor comparado não é 0
, mas -1
.
Total: o tamanho da matriz é 2048 * 4 = 8192
. Indique bytes como uma matriz. Para fazer isso, clique em *
e especifique o tamanho:

Torcemos até o final da matriz e vemos lá bytes, que são exatamente os bytes do código:


Agora temos a função sub_2D86
e temos tudo para resolver esse problema! Vamos ver o que a função recém-criada faz.
sub_2D86 ()
E apenas coloca o valor #$4147
no registrador D1
e chama a função sub_34C
. Dê uma olhada nela.
sub_34C ()

Vemos que aqui o valor da variável word_FF0020
é word_FF0020
. Se você olhar para os links para ele, veremos outro local onde o registro nesta variável está ocorrendo, e este será exatamente o local em que eu gostaria de pular a variável jmp_addr
. Isso confirma o palpite de que sub_D3C
definitivamente precisa pular para sub_D3C
.
Mas o que aconteceu a seguir foi muito preguiçoso para eu entender, então joguei o rum no GHIDRA , encontrei essa função e observei o código descompilado:
void FUN_0000034c(void) { ushort in_D1w; short sVar1; ushort *puVar2; if (((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,in_D1w ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; }
Vemos que a variável com o nome estranho in_D1w
e também a variável DAT_00ff0020
, que lembra com seu endereço a word_FF0020
mencionada anteriormente.
in_D1w
nos diz que esse valor é obtido do registro D1
, ou melhor, da metade mais jovem do WORD e define o registro D1
função que o passa. Lembra-se de #$4147
? Portanto, você precisa designar esse registro como o argumento de entrada para a função.
Para fazer isso, na janela com o código descompilado, clique com o botão direito do mouse no nome da função e selecione o item de menu Edit Function Signature
:

Para indicar que a função aceita um argumento através de um registro específico, a saber, não pelo método padrão da convenção de chamada atual, é necessário marcar a Use Custom Storage
e clicar no ícone com um sinal de mais verde :

Uma posição para o novo argumento de entrada é exibida. Clicamos duas vezes e obtemos uma caixa de diálogo indicando o tipo e o meio do argumento:

No código descompilado, vemos que in_D1w
é do tipo ushort
, o que significa que o especificaremos no campo type. Em seguida, clique no botão Add
:

Uma posição aparecerá para indicar o meio do argumento, precisamos especificar o registro D1w
em Location
e clique em OK
:

O código descompilado terá o formato:
void FUN_0000034c(ushort param_1) { short sVar1; ushort *puVar2; if (((ushort)(param_1 ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(param_1 ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,param_1 ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; }
param_1
que nosso valor param_1
é constante, passado pela função de chamada e é igual a #$4147
. Então, qual deve ser o valor de DAT_00ff0020
? Consideramos:
0x4147 ^ DAT_00ff0020 ^ 0x5e4e = 0x5a5a 0x4147 ^ DAT_00ff0020 ^ 0x4a44 = 0x4e50
Porque xor
- a operação é reversível, todos os números constantes podem ser discutidos entre si e obter o valor desejado da variável DAT_00ff0020
.
DAT_00ff0020 = 0x4147 ^ 0x5e4e ^ 0x5a5a = 0x4553 DAT_00ff0020 = 0x4147 ^ 0x4a44 ^ 0x4e50 = 0x4553
Acontece que o valor da variável deve ser 0x4553
. Parece que eu já vi um lugar onde esse valor é definido ...

Conclusões e decisão
Chegamos aos seguintes resultados:
- Primeiro você precisa pular para o endereço
0x0D3C
, para isso, digite o código 0D3C
- Vá para a função
0x2D86
, que define o valor de D1
para registrar #$4147
, para isso, é necessário inserir o código 2D86
Experimentalmente, descobrimos a tecla que precisa ser pressionada para verificar a tecla inserida: B
Tentamos:

Obrigada