Ghostbusters: Generiamo il nostro Account Number Personale
Articolo di Zlin, pubblicato il 03-11-2008.
Categoria: Programmazione.

Questo articolo svela come viene generato l’account number di un determinato nome e come si può ricostruirlo partendo da nome e importo.

Ghostbuster  - Apertura Acconto Ghostbuster - Copertina
La scheda di Ghostbusters su Ready64.org


Brevi cenni

Ghostbusters aveva un particolarità che fino ad allora non avevo mai riscontrato in nessun altro videogame: una volta terminata la partita con successo, il programma generava un nuovo codice bancario che permetteva in futuro di ricominciare il gioco utilizzando i soldi guadagnati fino a quel momento.

Nell’era dove le memory-card non erano ancora utilizzate, trovai questo trucchetto semplice ma al tempo stesso geniale, in quanto teneva "memoria" di un valore pur non effettuando nessun tipo di registrazione magnetica (cosa tra l’altro difficilmente applicabile per i soli utilizzatori del datassette).

In seguito furono scoperti alcuni nomi utente e account che permettevano di poter partire con un conto molto elevato. Oltre a questi espedienti però non trapelò mai nulla su come venissero generati questi codici.

Dopo essermi portato dietro per 24 anni questa curiosità (parliamo di quasi ¼ di secolo di storia) ho deciso di analizzare il gioco per andare finalmente a scoprire il core del programma che genera i codici dell’account number.

Se anche voi in tutti questi anni avete nutrito lo stesso interesse, non perdetevi questo articolo. Chi non fosse interessato può invece andare direttamente alla parte finale dell’articolo prima del paragrafo "Osservazioni" dove può trovare, oltre al listato basic e al codice assembly, il download del file PRG.

Analisi

Strumenti utilizzati per l’analisi:

  • Un normale Monitor (utilizzato quello interno dell’emulatore CSS64)
  • Un qualsiasi hex editor per vedere e cercare nel dump della memoria.

Dal momento che il programma ci richiede un nome (Last, First) diamo per scontato che verrà memorizzato e mantenuto attivo da qualche parte in memoria, in quanto dovrà essere utilizzato in seguito per generare un nuovo codice a partita terminata.

Eseguendo un dump della memoria, scopriamo che il nostro nome utente viene memorizzato a partire dall’indirizzo $EAB3 (max lunghezza consentita 18 caratteri). Eventuali caratteri non digitati lasciano il campo uguale a 0 (zero).

A questo punto proviamo a cercare eventuali parti di programma che accedano in qualche modo all’indirizzo $EAB3. Sempre nel dump della memoria cerchiamo quindi la sequenza di bytes B3 e EA e vediamo se siamo fortunati.

La nostra ricerca ci dice che ci sono alcune parti di programma che contengono questi 2 bytes. Analizziamo il primo spezzone di codice trovato:

74BB A2 00      LDX #$00
74BD BD 00 EA   LDA $EA00,X
74C0 9D B3 EA   STA $EAB3,X
74C3 E8         INX
74C4 E0 14      CPX #$14
74C6 90 F5      BCC $74BD

Questa parte di codice trasferisce 20 caratteri memorizzati all’indirizzo $EA00 al nostro indirizzo $EAB3. Noi sappiamo già che in $EAB3 c’è il nostro nome. Possiamo allora ipotizzare che l’indirizzo $EA00 venga utilizzato come buffer per raccogliere i dati immessi su tastiera alla richiesta di nome e account number.

Cerchiamo allora di vedere dove viene memorizzato l’account number digitato. Verifichiamo se l’istruzione LDA $EA00,X (in esadecimale BD per l’istruzione e 00, EA per l’indirizzo) è stata utilizzata altrove e troviamo una corrispondenza anche al seguente indirizzo:

9CFC A2 00      LDX #$00
9CFE 86 23      STX $23
9D00 86 24      STX $24
9D02 86 25      STX $25
9D04 86 26      STX $26
9D06 BD 00 EA   LDA $EA00,X
9D09 F0 18      BEQ $9D23
9D0B 29 0F      AND #$0F
9D0D A0 03      LDY #$03
9D0F 06 26      ASL $26
9D11 26 25      ROL $25
9D13 26 24      ROL $24
9D15 26 23      ROL $23
9D17 88         DEY
9D18 10 F5      BPL $9D0F
9D1A 05 26      ORA $26
9D1C 85 26      STA $26
9D1E E8         INX
9D1F E0 08      CPX #$08
9D21 90 E3      BCC $9D06
9D23 60         RTS

Questa parte di codice trasferisce il contenuto degli 8 bytes dell’account digitato (memorizzati in $EA00) nei 4 bytes a partire dall’indirizzo $23.

Il fatto che in quattro bytes venga memorizzato il contenuto di otto e molto semplice da spiegare. Il programma si aspetta 8 cifre numeriche (ogni cifra può essere memorizzata in 4 bit).

Ipotizziamo di aver digitato l’account number 12345678. Questo account è così memorizzato in esadecimale in $EA00:

31 32 33 34 35 36 37 38 (8 bytes)

Il codice sopra riportato non fa altro che prendere le parti basse di ogni singolo byte (1, 2, 3, ecc.) e mano a mano le inserisce con delle istruzioni ROL negli indirizzi $23, 24, 25 e $26. Il nostro account alla fine sarà così memorizzato a partire da $23:

12 34 56 78 (4 bytes)

Essendo una subroutine (termina con RTS) andiamo a cercare se ci sono istruzioni che fanno un salto all’indirizzo $9CFC e troviamo questa corrispondenza (tradotto in linguaggio macchina con gli esadecimali 20 per l’istruzione JSR e FC, 9C per l’indirizzo).

7505 20 FC 9C   JSR $9CFC  ;subroutine che converte
7508 A9 00      LDA #$00   ;l'account number da 8 a 4 bytes
750A 85 59      STA $59
750C A5 23      LDA $23
750E 8D C7 EA   STA $EAC7
7511 A5 24      LDA $24
7513 8D C8 EA   STA $EAC8
7516 A5 25      LDA $25
7518 8D C9 EA   STA $EAC9
751B A5 26      LDA $26
751D 8D CA EA   STA $EACA
7520 20 55 91   JSR $9155

Quindi, questi 4 bytes che contengono l’account, vengono definitivamente memorizzati in $EAC7.

NOTA

In questo punto notiamo poi un salto all’indirizzo $9155. Lo vedremo più tardi. Continuiamo ancora la ricerca di EAB3, che troviamo all’indirizzo $91BC e notiamo qualcosa di interessante:

91B6 A9 00      LDA #$00
91B8 85 23      STA $23
91BA A2 13      LDX #$13
91BC BD B3 EA   LDA $EAB3,X
91BF 18         CLC
91C0 65 23      ADC $23
91C2 85 23      STA $23
91C4 CA         DEX
91C5 10 F5      BPL $91BC
91C7 60         RTS

Questa parte di programma prende dal nostro indirizzo di memoria ogni singolo carattere del nome e ne fa una somma.

NOTA

In questo calcolo viene considerata solo la parte bassa del risultato. Esempio 3 caratteri Z (hex 5A) daranno risultato:

5A+5A=B4 --> B4+5A= 10E --> risultato in $23 = 0E.

Ora sappiamo che il nostro nome è utilizzato per dare origine ad un certo numero. Anche in questo caso ci troviamo di fronte ad una subroutine. Proviamo allora a cercare l’istruzione che la chiama (JSR $91B6)

Otteniamo quindi 2 risultati:

90F6 20 B6 91   JSR $91B6
917B 20 B6 91   JSR $91B6

Conclusioni sulla decodifica

A questo punto, come visto in precedenza, abbiamo la routine in $7505 che memorizza l’account e poi salta in $9155. Da qui inizia la decodifica dell’account. Questa nuova parte di programma contiene una delle due istruzioni JSR $91B6, subroutine che calcola la somma dei caratteri del nome.

Analizziamo il listato tenendo presente che:

Il nostro nome è memorizzato all’indirizzo $EAB3
Il numero dell’account è memorizzato in $EAC7

9155 A2 03      LDX #$03
9157 A9 00      LDA #$00
9159 85 25      STA $25
915B 85 26      STA $26
915D 85 27      STA $27
915F BD C7 EA   LDA $EAC7,X
9162 86 28      STX $28
9164 A2 01      LDX #$01
9166 0A         ASL A
9167 A0 02      LDY #$02
9169 0A         ASL A
916A 26 27      ROL $27
916C 26 26      ROL $26
916E 26 25      ROL $25
9170 88         DEY
9171 10 F6      BPL $9169
9173 CA         DEX
9174 10 F0      BPL $9166
9176 A6 28      LDX $28
9178 CA         DEX
9179 10 E4      BPL $915F

Questa iterazione preleva i 4 bytes con il nostro account partendo dalla parte più a destra (nel nostro precedente esempio 12 34 56 78), li moltiplica varie volte(ASL) e inserisce di volta in volta il flag di carry all’interno degli indirizzi $25, $26 e $27 in questo modo:

ASL ---> CY

ROL $25 <--- CY <--- ROL $26 <--- CY <--- ROL $27 <--- CY (settato dalla precedente ASL)

Alla fine di queste operazioni avremo gli indirizzi di $25 impostati in questo modo:

$ 25 prima parte dell’importo
$ 26 un numero di controllo (tipo checksum)
$ 27 seconda parte dell’importo

Esempio: l’importo $ 657.800 sarebbe memorizzato come 65, xx (un numero checksum) e 78.

NOTA

Decine e unità non sono contemplate. Infatti il gioco incrementa sempre a partire dalle centinaia.

Poi esegue:

917B 20 B6 91   JSR $91B6

e salva la somma dei caratteri del nome in $23 (chiamiamolo "x" per convenzione).

Il programma preleva quindi i due valori dell’importo (in $25 e $27), li somma e compie per un numero di volte pari a "x" una serie di ASL e di ROL (vedere codice a partire da $91A6). Alla fine del calcolo avremo un valore di controllo memorizzato in $07.

Se questo valore non corrisponde a quello in $26 (il famoso checksum) il programma ci avviserà che il numero di conto non è corretto e dobbiamo digitarne un altro.

In caso affermativo abbiamo invece digitato un conto corretto. Le due parti dell’importo verranno definitivamente registrate in $57 e $58. Il byte in $59 sarà sempre pari a 00. Questi tre bytes durante il gioco determinano l’importo del nostro conto.

917E A6 23      LDX $23 ; preleva la somma dei caratteri del nome (x)
9180 A5 25      LDA $25
9182 18         CLC
9183 65 27      ADC $27 ; somma il valore in $25 con quello in $27
9185 D0 02      BNE $9189 ; se somma = 0 gli assegna valore 1
9187 A9 01      LDA #$01 ; altrimenti ASL e ROL daranno sempre 0.
9189 85 07      STA $07
918B 20 A6 91   JSR $91A6
918E CA         DEX ; compie "x" ASL e ROL su questa somma
918F D0 FA      BNE $918B
9191 A5 07      LDA $07 ; e memorizza il risultato in $07
9193 C5 26      CMP $26 ; fa il controllo del checksum
9195 F0 06      BEQ $919D ; se positivo rientra (importo memorizzato)
9197 A9 00      LDA #$00 ; se negativo azzera i valori dell'importo
9199 85 25      STA $25 ; e al rientro verremo dirottati verso
919B 85 27      STA $27 ; il codice che ci comunica che il conto
919D A5 25      LDA $25 ; è errato
919F 85 57      STA $57
91A1 A5 27      LDA $27
91A3 85 58      STA $58
91A5 60         RTS
91A6 A5 07      LDA $07 ; subroutine che calcola il checksum
91A8 0A         ASL A
91A9 45 07      EOR $07
91AB 0A         ASL A
91AC 45 07      EOR $07
91AE 0A         ASL A
91AF 0A         ASL A
91B0 45 07      EOR $07
91B2 0A         ASL A
91B3 26 07      ROL $07
91B5 60         RTS
91B6 A9 00      LDA #$00 ; subroutine che calcola il valore x dal nome
91B8 85 23      STA $23
91BA A2 13      LDX #$13
91BC BD B3 EA   LDA $EAB3,X
91BF 18         CLC
91C0 65 23      ADC $23
91C2 85 23      STA $23
91C4 CA         DEX
91C5 10 F5      BPL $91BC
91C7 60         RTS

La Codifica

Ricapitoliamo un momento come viene tradotto l’account number al momento della decodifica:

1) dal nome viene ricavato il valore somma "x";
2) dall’account number di 4 bytes viene ricavato l’importo ed il checksum (totale 3 bytes);
3) le "x" operazioni sui 2 bytes dell’importo generano un numero che deve essere uguale al checksum.

Avendo capito il meccanismo possiamo procedere ad eseguire la codifica con il processo inverso:

1) dal nome ricaviamo il valore somma "x";
2) dall’importo voluto calcoliamo il valore del checksum (tramite le "x" operazioni);
3) con i 3 bytes (2 per l’importo e 1 del checksum) generiamo l’account number di 4 bytes dal quale preleveremo le 8 singole cifre.

Le prime 2 operazioni sono identiche sia per la codifica che per la decodifica.

Per il punto 3 dobbiamo invece invertire le istruzioni. Se nella decodifica sono state utilizzate le istruzioni ASL e ROL, nel processo di codifica dovremmo utilizzare esattamente le istruzioni inverse (ossia LSR e ROR).

Creiamo il nostro generatore di Account Number

Dal nostro nome calcoliamo il valore somma "x" (codice nella subroutine $91B6).

Con i 2 bytes dell’importo calcoliamo il valore checksum (codice nella subroutine $91A6), es. $999.900, teniamo solo 9999 e usiamo solo il 99 in un byte e 99 nell’altro.

Per la codifica dobbiamo infine prendere i 3 bytes con importo e checksum e trasformarli in 4 bytes. Il salto al codice che compie questa operazione lo avevamo già trovato. Infatti avevamo trovato in precedenza 2 istruzioni che saltavano al calcolo del valore "x". La prima era all’indirizzo $917B (inserito nel ciclo decodifica che inizia in $9155). La seconda istruzione era all’indirizzo $90F6 (inserito nel ciclo di codifica che inizia in $90EE).

Esaminiamo quindi nel dettaglio il codice:

90EE A5 57      LDA $57 ; preleva $57 e $58 i due bytes dell'importo
90F0 85 25      STA $25
90F2 A5 58      LDA $58
90F4 85 27      STA $27 ; li memorizza in $25 e $27
90F6 20 B6 91   JSR $91B6 ; subroutine calcolo valore "x" dal nome
90F9 A6 23      LDX $23
90FB A5 25      LDA $25
90FD 18         CLC
90FE 65 27      ADC $27 ; somma i due bytes dell'importo
9100 D0 02      BNE $9104
9102 A9 01      LDA #$01
9104 85 07      STA $07
9106 20 A6 91   JSR $91A6 ; subroutine calcolo checksum 2 bytes importo
9109 CA         DEX ; esegue queste operazioni le "x" volte
910A D0 FA      BNE $9106
910C A5 07      LDA $07 ; Inserisce checksum in $26. Ora abbiamo i 3
910E 85 26      STA $26 ; bytes per generare account in $25, $26 e $27
9110 A2 02      LDX #$02
9112 A9 00      LDA #$00
9114 8D C7 EA   STA $EAC7
9117 8D C8 EA   STA $EAC8
911A 8D C9 EA   STA $EAC9
911D 8D CA EA   STA $EACA
9120 85 29      STA $29
9122 B5 25      LDA $25,X ; Subroutine che dai 3 bytes genera
9124 A0 07      LDY #$07 ; i 4 bytes contenenti l'account
9126 4A         LSR A
9127 6E CA EA   ROR $EACA
912A 6E C9 EA   ROR $EAC9
912D 6E C8 EA   ROR $EAC8
9130 6E C7 EA   ROR $EAC7
9133 48         PHA
9134 E6 29      INC $29
9136 A5 29      LDA $29
9138 C9 03      CMP #$03
913A D0 11      BNE $914D
913C A9 00      LDA #$00
913E 85 29      STA $29
9140 18         CLC
9141 6E CA EA   ROR $EACA
9144 6E C9 EA   ROR $EAC9
9147 6E C8 EA   ROR $EAC8
914A 6E C7 EA   ROR $EAC7
914D 68         PLA
914E 88         DEY
914F 10 D5      BPL $9126
9151 CA         DEX
9152 10 CE      BPL $9122
9154 60         RTS
; al termine delle operazioni abbiamo i
; 4 bytes con le cifre dell'account number
; in $EAC7. Dobbiamo solo estrapolarle.

Routine Assembly e listato Basic

Per raggiungere il nostro risultato finale non dobbiamo fare altro che decidere dove memorizzare i dati necessari alla codifica e far compiere alla routine i calcoli necessari. Potremmo riscrivere e personalizzare il nostro programma per avere una versione completamente differente da quella originale.

Per comodità sfruttiamo le routine originali del gioco, avendo premura di sostituire gli indirizzi di prelievo e deposito dei dati con quelli da noi desiderati. Per la parte assembly è stata utilizzata la zona di memoria che parte da $C000 (dopo la ROM del basic) in modo da non avere interferenza con le variabili del programma.

Qui di seguito i link per scaricare il codice assembly, il listato basic o direttamente il file PRG:

OSSERVAZIONI - Caratteri alfanumerici inseriti nell'account number

Alcuni di voi si chiederanno a questo punto com'è possibile che vengano generati degli importi validi inserendo delle lettere nell'account number.

Questo è possibile in quanto gli 8 byte digitati su tastiera alla richiesta dell'account number, vengono prima "scremati" dei bit alti attraverso una AND $0F. Il programma si aspetta infatti un carattere numerico (in esadecimale dal valore 30 al 39).
Dopo aver azzerato i bit alti, il byte dovrebbe contenere esattamente il numero che è stato digitato (da 0 a 9). Quindi se con nome ZLIN al posto dell'account 31363546 (33 31 33 36 33 35 34 36) scrivo l'account CACVSEDF (43 41 43 56 53 44 46) il risultato finale sarà esattamente identico.

Vediamo quindi cosa succede ad esempio se rispondiamo con la sequenza Nome LUCA, Account LUCA:

Incominciamo a dire che la somma dei caratteri del nome LUCA (hex 4C, 55, 43, 41) è uguale a $25 (37 decimale). L'account LUCA è invece uguale a questi 8 byte : 00 00 00 00 4C 55 43 41 (00 sono i caratteri non digitati).

La AND $0F da come risultato 00 00 00 00 0C 05 03 01
L'account verrà registrato nei 4 bytes in questo modo: 00 00 C5 31

Eseguendo il calcolo spiegato nell'articolo ricaviamo da questo account i 3 bytes con importo e numero checksum:

66 50 00

Il primo e il terzo byte indicano l'importo 66 00, ai quali verrà accodato 00 di decine e unità. Questi numeri (6 6 0 0 0 0) verranno poi visualizzati con una OR #$30 che genererà i corrispettivi alfanumerici.

Il valore $50 è quello che chiamiamo checksum. La somma dei byte importo (66 + 00 = 66 in hex) "trattata" per 37 volte dal ciclo ASL/EOR/ROR, restituisce il valore $50 che corrisponde al checksum. Il conto è quindi valido. Il funzionamento di LUCA - LUCA è chiaramente una pura coincidenza.

Proviamo con Nome ZLIN e account ZLIN:

Somma caratteri del nome uguale a $3D (61 decimale)
Account : 00 00 00 00 5A 4C 49 4E
AND $0F
Account : 00 00 00 00 0A 0C 09 0E (8 byte)
Account : 00 00 AC 9E (4 byte)

Dopo il calcolo, i 3 bytes danno origine a:
39 40 00 quindi importo 390.000 ed un checksum pari a $40
(39 + 0 = 39) "trattato" per 61 volte genera un valore pari a $8C che è diverso dal checksum e quindi mi avvisa che il conto è invalido.

Analizziamo ancora un altro esempio famoso: Nome ANDY - Account 777

Somma caratteri del nome uaguale a $2C
Account : 00 00 00 00 00 37 37 37
AND $0F
Account : 00 00 00 00 00 07 07 07 (8 byte)
Account : 00 00 07 77 (4 byte)

Dopo il calcolo, i 3 bytes danno origine a:

FC 70 00

Come visto in precedenza, FC e 00 (aggiungendo ancora un 00) daranno origine all'importo. In questo caso, al momento della visualizzazione, la OR $30 darà questo risultato : 3F 3C 30 30 30 30 che equivale a ?>0000. Il checksum è $70 e corrisponde a quello ricavato da FC+00 "iterato" per 44 volte con ASL/EOR/ROR.
Il conto è valido e l'importo asseganto è pari ?>0000.

Non appena avrete un guadagno, il vostro conto verrà però reimpostato al valore massimo ossia 999900 (oltre questo valore non verrà più incrementato). Al momento topico (PK Energy a 9999) il programma farà un confronto tra importo iniziale (FC 00 00) e attuale (99 99 00) e quest'ultimo risulterà inferiore.

La partita terminerà con un insuccesso.

| | Share
Commenti
Commenta gioco Ci sono 4 commenti per questo articolo. Registrati se vuoi lasciarne uno.
Ho potuto visionare per primo l'articolo inviato da Zlin per fare da proofreader e sono rimasto colpito dall'opera di reversing e dalla chiarezza di esposizione. Bravissimo, grazie per l'interessante lettura.
# - postato da iAN CooG/HF - 03 November 2008 [14:27]
Volevo subito inserire il commento che con "luca-luca" inserito la primissima volte che ci giocai feci cadere dalla sedia i miei amici... Poi scrollando ho visto che anche tu ne facevi accenno. :-O E' un nome "magico" per questo gioco... :)
# - postato da Luca - 05 November 2008 [00:17]
Davvero strabiliante, altro che sudoku!
# - postato da maz - 13 November 2008 [00:42]
Grazie veramente per questo lavoretto di reverse engineering! Proprio come per te, erano 25 anni che avevo questa curiosità! All'epoca il "trucchetto" di Luca mi fu rivelato dal negoziante che mi vendette il gioco (o meglio, che lo vendette a mio padre!), ma ho sempre pensato che fosse per via di una versione "tarocca" del gioco stesso. Quando, in tempi moderni ho giocato con l'emulatore mediante la rom "originale" (passami il termine "rom") ho trovato il "mio" account ancora pieno di soldi :-) e non ho più saputo cosa pensare! Fino ad oggi! :-)))))
# - postato da Matteo - 09 January 2009 [07:43]
Commodore 64
Pannello Utente
21 Visitatori, 0 Utenti

Ciao, ospite!
(Login | Registrati)
Ready64 Store
Ultimo Commento
Clicca per leggere tutti i commenti
International Karate
"Gioco fatto benissimo e di difficoltà progressiva rispettabile. Da ex karateka confermo che dopo la cintura gialla le difficoltà aumentavano notevolmente e non solo in combattimento. L'unica pecca è la cintura viola ( inesistente nel karate) ..."
leggi »
- warrior77300
Ult. Commento Art.
Le Collane Avventurose in Italia - Parte II: La Catalogazione
"Ciao a tutti, gli inserti della guida per giocare e scrivere avventure, apparsi sui primi sei numeri di Explorer, erano stati riuniti in un pdf nel 2004 sul sito del Progetto Lazzaro, da tempo offline. Mancava il quarto, che fu poi recuperato ..."
- Scritto da Gyrev
RetroCampus