Resolvendo um Crackme simples para Sega Mega Drive

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:


  1. Torcemos antes do início dos dados (haverá um link para eles a partir do código)
  2. Seguimos o link e procuramos um ciclo no qual o tamanho desses dados deve aparecer
  3. 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:


  1. Primeiro você precisa pular para o endereço 0x0D3C , para isso, digite o código 0D3C
  2. 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

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


All Articles