Rubicon rivelato
Articolo di Raffox, pubblicato il 27-09-2017.
Categoria: Programmazione.

Rubicon è tra i videogame più protetti, a livello di codice oltre che di tracce e settori su disco, della storia del Commodore 64. Pubblicato nel 1991, la versione disco del programma è apparsa molto difficile da sproteggere anche per i cracker più esperti e prolifici della scena. Flavio Pasqualin (Flavioweb, membro del gruppo italiano di cracking Hokuto Force) ha voluto cimentarsi con questo videogame, riuscendo a deingegnerizzare il codice al punto da individuare tutte le "diaboliche" protezioni inserite in fase di programmazione. Quest'articolo è altamente tecnico e quindi rivolto per lo più agli addetti ai lavori. Tuttavia consigliamo la lettura anche ai meno esperti se non altro per avere un assaggio, seppur blando, di alcune tecniche di protezione e sprotezione. Al di là della complessità del tema trattato, l'articolo ha lo scopo primario di documentare il modus operandi adottato da Flavio nello sbrigliare efficacemente il codice da quello che si è da subito rivelato un intricatissimo dedalo di protezioni.


Com'è protetta la versione disco di Rubicon?

In questo testo descriverò, per sommi capi, il funzionamento delle protezioni applicate da Siefried Jagott (aka Sigi, Snaky/GP, SJ/AMOK) al gioco Rubicon, pubblicato nel 1991.

Scriverò le informazioni utili ad effettuare il "crack" del gioco, senza scendere troppo nel dettaglio, perché solo la descrizione della protezione applicata in fase di BOOT (Timex 3.0 più il codice di SJ) richiederebbe un testo a se stante, oltre al fatto che questo testo non vuole essere un tutorial su come crackare Rubicon.

Per iniziare, basti sapere che la protezione di boot si compone di due parti:

1. un codice completamente cifrato via timer;

2. la verifica di una particolare traccia sul disco (traccia 18, settore 18) che, a quanto è risultato, non è riscrivibile usando un normale drive CBM, rendendo di fatto il disco incopiabile usando copiatori come il Burst Nibbler oppure il Fast Hack'em con i suoi parametri.

Non sono sceso nei dettagli per capire cosa possa succedere qui, poiché questa prima parte della protezione non rappresenta il vero problema al crack del gioco.

Uno dei primi veri problemi che si incontrano, è il fatto che quasi tutto il codice eseguito e i dati gestiti in fase di boot sono codificati. Il metodo di codifica usato può essere definito "temporizzato": infatti si basa su dei precisi valori che i contatori CIA devono avere in un ben preciso istante dell'esecuzione. Questi valori sono utilizzati per decodificare il codice che sarà eseguito da quel momento in avanti.

Il problema che questo tipo di approccio causa a un cracker, è che anche il drivecode caricato nel drive per gestire tutto l'Input/Output da disco è codificato, e quindi non visibile o intercettabile. Il drivecode è necessario, assieme alla parte del loader residente sul C64, per poter accedere ed estrarre i file su disco.

Come estrarre il drivecode?

Non è possibile esaminare il codice con l'Action Replay per 2 motivi:

1 - la decodifica del codice è basata su timing, quindi il freeze, anche solo per esaminare il contenuto della memoria, causa dei crash della macchina.

2 - Il codice è farcito di opcodes illegali che rendono illeggibile il disassemblato.

Esistono 2 possibilità:

1 - riscrivere tutte le routine, passo dopo passo, in modo da decodificare le varie sezioni arrivando fino al momento in cui il codice fa l'upload del drivecode, evitando di mandare il comando "M-E" e lasciando così il drive in uno stato in cui è possibile analizzarne la RAM ed estrarre il codice. Per arrivare al compimento di questa ipotesi, dobbiamo destreggiarci fra opcodes illegali e I/O gestito attraverso l'uso dei registri "mirror" e non attraverso le locazioni "ufficiali" dei CIA. Nella migliore delle ipotesi: qualche mese di lavoro!

2 - Creare una versione modificata delle rom del 1541, da "bruciare" su EPROM e utilizzare solo per caricare questo gioco fino allo "start" del drivecode. Questa "custom ROM" deve essere del tutto identica a quella ufficiale, tranne che per il suo checksum iniziale (così da permetterci la modifica della porzione di rom che ci serve realmente) e per la porzione di codice che esegue il comando "M-E" una volta inviato dal C64 al Drive.

Come potete ben immaginare, io ho scelto la seconda ipotesi.

Ovviamente con questo tipo di rom montata sul drive, il gioco non può funzionare... ma non è questo ciò che ci interessa. A noi serve che, al momento di far partire il codice del loader, il drive invece di eseguire il comando "M-E" torni alle sue operazioni "normali" come se niente fosse, aspettando ulteriori comandi normali dal C64.

A questo punto, possiamo freezare con la nostra fida Action Replay e andare ad analizzare la RAM del drive, avendo a $0200 la stringa "M-E" seguita dallo start address del loader ($04B3 in questo caso). Qui possiamo salvare il drivecode su disco, oppure stamparlo, oppure farne ciò che vogliamo. Io ho scelto di stamparlo per "trasformarlo" in codice sorgente compilabile, da usare per le successive fasi del crack.

Nel momento del "crash" salviamo anche la memoria del 64 da $CEA0 A $CFFF. Tale porzione di codice è quella che carica il "grande file" che occupa quasi tutta la RAM, a eccezione dell'area $CE00-$CFFF. Questo codice carica anche la ZP (Zero Page, ndr) e lo stack (da $00 a $1F utilizzato per indirizzi di ritorno e valori prelevati con PLA durante le prime fasi del boot). Una volta salvato questo file, scrivendo un codice ad hoc, carichiamo il drivecode nel drive ed eseguendo questa routine da $CF00 col disco originale nel drive, al termine della sua esecuzione (che corrisponde all'RTS alla locazione $CF62), avremo in memoria tutti i file necessari al boot del game, compresa la pagina zero iniziale (da salvare).

Ora ci serve il loader "lato C64".

La cosa più semplice da fare, quando si cerca un loader, è verificare se ci sono letture/scritture a $DD00. Sia che si proceda con il comando di ricerca del monitor della AR (Action Replay, ndr), oppure che si inizi a disassemblare la memoria partendo, magari per esperienza, da $0100, presto si arriva a:

.> 0120 4c fd  01  jmp $01fd
.> 0123 ad 00 dd  lda $dd00
.> 0126 29 0f      and #$0f
.> 0128 8d d6 01  sta $01d6
.> 012b 49 10      eor #$10
.> 012d 8d c7 01  sta $01c7
.> 0130 49 30      eor #$30
.> 0132 8d 70 01  sta $0170
.> 0135 49 b0      eor #$b0
.> 0137 8d 9d 01  sta $019d

[...]

cosa che ci dovrebbe fare immediatamente sospettare di trovarci nei paraggi di un loader.

Questa porzione di memoria sembra terminare con:

.,0338 PROTECTED BY SJ/AMOK 10/1991 !!@
.,0358 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
.,0378 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
.,0398 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
.,03b8 @@

Ok... direi che abbiamo ufficialmente un loader.
Come funzionerà questo loader?
Carichiamo il gioco fino all'intro e verifichiamo se c'è qualche chiamata a $0120:

.h 0000 ffff 20 20 01
050b 0515 063f

L'abbiamo individuata! Molto bene.

.> 0506 78        sei
.> 0507 a2  30    ldx #$30
.> 0509 a0  33    ldy #$33
.> 050b 20 20 01  jsr $0120
.> 050e 2c 11 49  bit $4911
.> 0511 a2 30      ldx #$30
.> 0513 a0 34      ldy #$34
.> 0515 20 20 01  jsr $0120
.> 063b a2 30      ldx #$30
.> 063d a4 d1      ldy $d1
.> 063f 20 20 01  jsr $0120

I file vengono letti dal disco usando una coppia di valori in X e Y.

A questo punto è possibile assemblare un codice che carichi il drivecode salvato in precedenza nel drive e che esegua questo loader per caricare i nostri file. Ben presto ci accorgeremmo però che il loader non segnala errori per nessun valore di X/Y che noi possiamo andare a utilizzare... come se i file da caricare sul disco fossero infiniti. Quindi come possiamo fare per capire quali file sono necessari per il funzionamento del game?

Anche qui abbiamo 2 strade: analizzare il codice in RAM, oppure tramite l'Action Replay, piazziamo un break point (comando BRK, ndr) a $0120 e iniziamo a giocare al gioco, segnandoci i valori di X/Y ad ogni chiamata (il BP va rinnovato a ogni file).

Prima protezione in-game.

Freeziamo il gioco, settiamo i breakpoint, usciamo dal freeze, ri-freeziamo e tutto sembra funzionare correttamente... finché non perdiamo una vita! Invece di ripartire, il gioco crasha in modi strani.

Nel 1991 la cosa, probabilmente, ci avrebbe lasciato interdetti, facendoci pensare a chissà quale incompatibilità con la cartuccia o forse quale altro misterioso problema. Oggi sappiamo che ci troviamo, probabilmente, davanti a una protezione in-game. Solitamente le protezioni anti-cartuccia usano il timing NMI (Interrupt non Mascherabile, ndr) per capire se una cartuccia è entrata in azione. Come? Facendo partire il timer A e il timer B sfasati di qualche ciclo in più dei 4 classici degli opcode STore e controllando periodicamente che questa "sfasatura" rimanga costante. Perché la "sfasatura" cambia freezando il game? Semplice: perché la cartuccia, al momento di uscire dal freeze, resetta i contatori al loro valore iniziale e li fa partire con un numero di cicli di distanza molto variabile.

Quindi, sappiamo che dobbiamo cercare qualche porzione di codice che legga $DD04, $DD05, $DD06 e $DD07 e, in qualche modo, ne confronti i valori per controllare la sfasatura. La ricerca di $04/05/06/07 $DD non porta a buoni risultati ma, prima di darci per vinti, diamo un occhiata generale in memoria per vedere se qualcosa ci può sembrare "un controllo" sulla differenza di valore di 2 coppie di bytes.

Non ci dobbiamo spostare di troppo da dove eravamo, per vedere:

.> 08db ac d0 11  ldy $11d0
.> 08de b9 ae 11  lda $11ae,y
.> 08e1 85 30      sta $30
.> 08e3 b9 ca 11  lda $11ca,y
.> 08e6 85 31      sta $31
.> 08e8 b9 af 11  lda $11af,y
.> 08eb 85 32      sta $32
.> 08ed b9 cb 11  lda $11cb,y
.> 08f0 85 33      sta $33
.> 08f2 a2 00      ldx #$00
.> 08f4 ac 12 d0  ldy $d012
.> 08f7 d0 fb      bne $08f4
.> 08f9 b1 30      lda ($30),y
.> 08fb 85 34      sta $34
.> 08fd b1 32      lda ($32),y
.> 08ff 85 35      sta $35
.> 0901 ee 06 12  inc $1206
.> 0904 e8        inx
.> 0905 e0 14      cpx #$14
.> 0907 f0 0c      beq $0915
.> 0909 a5 34      lda $34
.> 090b 38        sec
.> 090c e5 35      sbc $35
.> 090e a8        tay
.> 090f 88         dey
.> 0910 d0 e2      bne $08f4
.> 0912 8c 06 12  sty $1206
.> 0915 20 c4 07  jsr $07c4

Rapido controllo a $11D0: $20

$11AE + $20  =  $11CE  =  $04
$11CA + $20  =  $11EA  =  $DD
$11AF + $20  =  $11CF  =  $06
$11CB + $20  =  $11EB  =  $DD

Non c'è nemmeno bisogno di settare il break point. In più sappiamo che dalla lettura del primo registro a quella del secondo passano 8 cicli. Di conseguenza, facendo partire i contatori dallo stesso valore, ma con 7 cicli di "distanza" l'uno dall'altro, avremo sempre 1 come risultato della differenza fra i due. Per conoscere il valore di partenza a cui devono essere settati i contatori, possiamo sempre chiedere all' Action Replay:

.IO
.-d000 fe 00 fe 00 fe 00 32 79
.-d008 98 82 36 72 98 ab 98 c0
.-d010 3f 1b f1 9d 82 ff d6 00
.-d018 85 f1 f1 00 ff 00 ff ff
.-d020 f0 f0 fc fb f3 ff fb fa
.-d028 fa fa fa fa fa fa fa 00

.-dc00 7f ff ff 00 03 23 ff ff
.-dc08 00 00 00 81 d0 7f 01 08

.-dd00 82 ff 3f 00 7f 7f 7f 7f
.-dd08 00 00 00 91 00 7f 01 01

Possiamo facilmente vedere che $DC04/$05 sono da inizializzare a $03/$23, mentre $DD04/$05/$06/$07 a $7F/$7F/$7F/$7F tenedo a mente che i due contatori vanno fatti partire con 7 cicli di differenza, il primo dal secondo.

Queste informazioni ci serviranno per ottenere un crack funzionante.

Giusto per stare tranquilli, durante lo studio del codice, per ora è sufficiente forzare il valore $01 in A al posto di far effettuare la sottrazione al codice.

Recuperiamo i file.

È giunto il momento di dumpare i file dal disco originale e salvarli per rielaborarli nel crack.

Sempre con un codice ad hoc, carichiamo il drivecode nel drive e il loader nella RAM del C64, richiamando il loader a $0120 con in X e Y i valori del file da caricare. Ogni file "puntato" da una coppia di valori X/Y è in realtà un batch di molteplici file. In momenti differenti, il loader memorizza nelle locazioni $7A/$7B lo start address e l'end address di ciascun file presente nel batch che sta caricando. Man mano che il loader carica i file, basta risalvarli su un altro drive con un nome oppurtanamente creato per ricordarci di quale batch facciano parte, e il gioco è fatto.

La soluzione più semplice per ottenere un crack funzionante di Rubicon è quella di alterare meno cose possibile! Le varie in-game protection rilevate nel gioco hanno lo scopo di controllare sia alcuni valori settati in fase di boot dell'area I/O, sia dei valori di alcune locazioni nell'area del loader in RAM nel C64. Inoltre vengono anche effettuati dei checksum di alcune porzioni di codice per verificare che il gioco non sia stato "manomesso".

Piccolo esempio: il settaggio del timer $DC04/$05 è necessario per la gestione dei nemici. Un settaggio non corretto causa un diverso funzionamento di tutti i nemici che incontriamo durante il gioco. Non che il gioco smetta di funzionare, solo che lo fa in maniera differente rispetto all'originale.

Non volendo essere questo testo un tutorial su come crackare Rubicon su RH, ma uno spunto per chi volesse avventurarsi nel suo codice, direi che possiamo chiudere qui le trasmissioni.

Happy hacking =)

| | Share
Commenti
Commenta gioco Ci sono 0 commenti per questo articolo. Registrati se vuoi lasciarne uno.
Commodore 64
Utenti Online
Ciao, ospite!
(Login | Registrati)

Cerca un gioco
Random Game
Ultimo Commento
Clicca per leggere tutti i commenti
Rampage (Versione Europea)
"L'anno prossimo uscirà il film con The Rock, sarà un'americanata, ma il trailer mi ha gasato non poco!!!"
- Scritto da thecoach
Ult. Commento Art.
Alieni, piccioni e palline da golf (parte 2)
"Articolo fantastico! Finalmente sono riuscito ad avere un'idea più chiara di come possano essere contenuti i video nel piccolo spazio offerto dal C64! Grazie davvero per divulgare le tue conoscenze - e il tuo lavoro è davvero notevole! "
- Scritto da MarC-ello
Ospitato su