Questo articolo svela come viene generato l’account number di un determinato nome e come si può ricostruirlo partendo da nome e importo.
La scheda di Ghostbusters su Ready64.org
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.
Strumenti utilizzati per l’analisi:
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.
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.
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
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
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).
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.
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:
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.