segunda-feira, 30 de julho de 2012

Highway Encounter e Speedlock 1

Desta vez eu estou apresentando o esquema de proteção Speedlock 1, na adaptação do jogo Highway Encounter. A série Speedlock é bastante famosa e empregada em numerosos jogos.


A estrutura parcial do arquivo TZX, visto com TZX Show, é:
Block 1: text description
    "Created with Ramsoft MakeTZX"
Block 2: header
    Saved name: "H E       "
    Type: BASIC program
    Auto-start line: 0
    Program size without variables: 1149
    Total length: 1530
Block 3: data bytes
    Length: 1530
Block 4: group start
    Group name: Speedlock 1 Block 1
Block 5: loop start
    Repetitions: 7
Block 6: pure tone, 2165 pulses of 18 T-state period
Block 7: pulse sequences, 714 T, 714 T
Block 8: pure tone, 2165 pulses of 18 T-state period
Block 9: pulse sequences, 714 T, 714 T
...
Carregador BASIC

O carregador BASIC é relativamente grande, devido à presença de código de máquina. Este carregador tem a listagem camuflada e deve ser examinado pelo Basic Lister. A listagem real torna-se:

0 REM Protected by SPEEDLOCK
0 BORDER 0: PAPER 0: INK 0: BRIGHT 1: CLS : POKE 23624,0
0 POKE (PEEK 23641+256*PEEK 23642),PEEK 23649: POKE (PEEK 23641+256*PEEK 23642)+1,PEEK 23650
0 POKE (PEEK 23613+256*PEEK 23614),PEEK 23627: POKE (PEEK 23613+256*PEEK 23614)+1,PEEK 23628
0 POKE PEEK 23662,PEEK 23618: POKE 23663,PEEK 23619: POKE 23664,PEEK 23621

65417 HSGN SQR STR$ ...

A última linha (65417) só contém sujeiras e causará erro de sintaxe. Há uma série de linhas com POKE, mas são distrações, o que realmente importa é a 4ª linha que envolve a variável de sistema P_ERR ou ERR_SP (23613-23614). Esta variável aponta para o elemento da pilha do Z80 com endereço de retorno em caso de erro o BASIC que, neste caso, é alterado para coincidir com o início da área de variáveis BASIC (apontado por VARADD ou VARS, em 23627-23628). Em outras palavras, quando houver um erro no BASIC (causado pela linha 65417), será executada o código de máquina no endereço dado por:
PEEK 23627+256*PEEK 23628
que, no caso, é 24904.

Fazendo disassembly nesse endereço:

24904   LD L,L
        LD B,L
        LD B,B
...     ...

observa-se uma sequência de instruções que não fazem muito sentido. O papel delas é confundir a interpretação da listagem.

Descodificadores

O que se pode fazer no emulador é colocar um breakpoint em 24094 e ir executando passo a passo o código de máquina para ver o que acontece com os valores dos registradores (no Fuse, use o botão 'Single Step'). Em certo ponto, acaba-se deparando com um decodificador:

25058   LD A,R          ; Altera byte no endereço apontado
25060   XOR (HL)        ;por HL fazendo XOR com conteúdo do
25061   LD (HL),A       ;registrador R.
25062   LDI             ; Copia byte de (HL) para (DE) e diminui BC.
25064   RET PO          ; Salta para (SP) se BC=0,
25065   DEC SP          ;senão salta para (SP-2)
25066   DEC SP
25067   RET PE

Esta rotina é um laço (loop) que não é implementado com desvios (JP ou JR), mas com engenhosa manipulação de RET e da pilha do Z80. A instrução LDI transfere um byte do endereço apontado por HL para o apontado por DE, seguido da diminuição de BC por 1. Se o BC for 0, o flag P/V será 0 e o RET em 25064 será acionado e a execução salta para endereço apontado por SP; caso contrário, como SP é diminuído em 2, o RET em 25067 é acionado e a execução salta para endereço apontado por SP-2.

Quando a execução é interrompida em 25058, os valores dos registradores são: HL=24317, DE=60522 e BC=587. Significa que 587 bytes são transferidos decodificados a partir de 24317 para 60522.

Para saber os destinos dos desvios via RET, deve-se examinar a pilha de máquina. No caso do Fuse, pode-se usar o Memory broser, ou ainda executar o código de máquina passo a passo. Verifica-se que SP=65364, (SP)=60522 e (SP-2)=25058, portanto a rotina entre 25058 a 25067 é um laço que irá sair para 60522.

Agora basta definir um breakpoint em 60522 e prosseguir. O disassembly resultante mostrará um trecho com instruções sem muito sentido e, a partir de 60558, o mesmo decodificador que listado acima. Para contorná-lo, basta usar a mesma técnica para chegar ao endereço 60568. A partir deste endereço há mais um decodificador, porém é convencional pois usa instruções JP para executar desvios. Analisando a listagem, acaba-se chegando ao trecho que realmente faz o carregamento da fita:

61049   LD IX,32768     ; Carrega header em 32768.
        LD DE,17
        CALL 60920
61059   LD IX,16384     ; Carrega bloco 16384,39168 (16384-55551).
        LD DE,39168
        CALL 60686
        LD A,(61035)    ; Se byte em 61035 não for 0, resulta crash.
        CP 0
61074   JP NZ,60647
61077   LD HL,1343
        LD DE,60659
        LD BC,375
        LDIR
        JP 45280        ; Entrada para o jogo.

60647   LD IY,0         ; Entra em loop infinito e apaga a RAM.
60651   LD (IY+117),0
        INC IY
        JR 60651

Resumindo, a rotina acima carrega da fita um bloco de 39168 bytes na memória a partir do endereço 16384. Se houver erro de carregamento, o computador é travado. Após carregamento bem sucedido, inicia-se o jogo em 45280.

Proteção com registro R

Na etapa anterior parece que as proteções foram quebradas, mas existe mais uma que diz respeito ao bit 7 do registrador R. Os 7 bits menos significativos (0 a 6) deste registrador é incrementado regularmente, para refrescar as DRAM. Entretanto o bit 7 permanece inalterado, a não ser que um valor seja atribuído pela instrução LD R,A. Normalmente este bit teria valor 0, mas a esta proteção faz com que seja verificado o valor que, se não for 1, trava o computador:

37318   LD A,R          ;Pega valor do registrado R
        SLL A           ;e coloca o bit 7 no CARRY.
        LD HL,(36609)
        LD A,(36631)
        LD C,A
        JR NC,37318     ;Se bit 7=0, entra em loop infinito.

Portanto a rotina de desproteção deverá levantar o bit 7 de R antes de rodar o programa. Este tipo de proteção é encontrado em vários jogos. Se no debugger for apontado R maior que 128, há chances de que esta proteção seja usada.

Adaptação para Beta 48

Depois de desprotegido, o jogo pode ser facilmente adaptado para o disquete, pois há bastante espaço de RAM disponível. Basta salvar separadamente a tela e o bloco de códigos entre 23296-55551 no disco. Um carregador em linguagem de máquina deve se encarregar de mover o bloco para o local correto (pois ele tem que ser carregado em um lugar que não se sobreponha ao BASIC) com LDIR, levantar o bit 7 do registrador R e saltar para o início do jogo.

O procedimento de adaptação é explicado pormenorizadamente no documento contido neste arquivo disponível no Google Drive ou 4 Shared. Os arquivos Hobeta do Highway Encounter também estão lá.

4 comentários:

  1. Fantástico Flávio! Queria entender de asm Z80 e programação como você... :-D Eu sempre vi sobres esses esquemas de proteção "Speedlock", usado em vários programas nos cassetes originais. Sei tem que vários (speedlock 1,2,3 etc.). Você pretende abordar todos eles, ou ao menos, alguns outros? Abraço!

    ResponderExcluir
    Respostas
    1. Talvez eu aborde os demais Speedlocks, se encontrar algum jogo interessante que use tais esquemas. O Speedlock 2 é usado pelo Revolution (Vortex) e eu já quebrei essa proteção.

      Excluir
  2. Uma dúvida, esses esquemas de proteção eram "copyrighted"? Digo, se uma determinada softhouse desejasse usar um esquema ela deveria pagar ao inventor ou eles eram livremente usados?

    ResponderExcluir
    Respostas
    1. Eu nunca pesquisei isto, mas presumo que havia licença sobre tais rotinas de proteção. Havia esquemas usadas por uma única softhouse, como o Bleeploader da Firebird, portanto esta companhia devia ser a detentora dos direitos.

      Por outro lado, não sei até que ponto a legislação sobre software estava desenvolvida naquela época.

      Excluir

Seu comentário é bem vindo, mas peço que use este espaço adequadamente.