Hi,
Premessa doverosa: NON sono un hacker NDS, prima di qualche ora fa sapevo meno di 0 e mentre sto scrivendo questo post
forse so 0; sto aprendo questo thread perchè sul gruppo telegram del forum oggi è stato chiesto se qualche hacker che se la cava nel binary hacking GBA potesse dare una mano a capire come dirottare routine nell'ambito NDS come è comune fare su GBA e caso vuole sia stato io a rispondere.
Chiedo quindi a te che stai leggendo di prendere tutto con almeno 4 grani di sale, perchè appunto fondmentalmente non so di cosa sto parlando, sto solo applicando principi di informatica generali ad una ROM NDS.
Detto questo, il post serve semplicemente a illustrare gli step che ho intrapreso oggi per trovare la routine che gestisce il countdown di passi nei giochi NDS, in particolare ho usato Platino perchè coincidentalmente in questo periodo sto facendo una nuzlocke su quel titolo.
Step 0: Necessario
Lista della spesa:
- Rom Pokémon NDS (in questo esempio PKMN Platino EU versione 10)
- Desmume
- no$gba versione debugger (link)
- IDA (opzionale, per controllare il nostro risultato)
- Un editor esadecimale (suggerisco HxD)
Step 1: Ipotesi
Per poter cercare qualcosa bisogna avere almeno una vaga idea di cosa si stia cercando: nel caso di cercare routine aiuta enormemente avere una vaga idea (
ipotesi) di come funzioni la stessa.
Per fare ipotesi sensate gli strumenti migliore che abbiamo sono l'esperienza ed un po' di logica: se dovessimo rifare noi adesso
da 0 la feature che è implementata dalla routine che stiamo cercando, cosa faremmo?
Nel nostro caso specifico, sicuramente scriveremmo in RAM il
numero di passi che mancano alla fine dell'effetto del repellente e ci allacceremmo alla routine di GF che gestisce la camminata del player per decrementare di 1 questo contatore ad ogni passo, verificare se abbia raggiunto lo 0 e, in caso, terminare l'effetto del repellente.
Questa è stata la mia ipotesi (aiutata molto anche dal fatto che avessi già fatto in passato la stessa identica ipotesi su un gioco di terza gen e si fosse rivelata veritiera, come dicevo l'esperienza decisamente aiuta).
Step 2: I dati
Come - spero - chiunque stia leggendo questo post sappia, il codice non è altro che
un mezzo per trasformare i dati, e le routine non fanno eccezione: in questo caso i dati sono il numero di passi rimanenti (che d'ora in poi chiamerò "
countdown" per comodità) e un qualcosa che dice al gioco di non far avvenire incontri selvatici, probabilmente qualcosa di simile ad una
flag che viene letta anche dalla routine che decide se farci incontrare un pokémon o meno.
Il modo migliore per trovare il codice che agisce su determinati dati è proprio quello di partire da questi dati, quindi vediamo di capire un po' quali dati cercare in questo caso specifico.
Abbiamo detto che c'è un flag e un countdown: il flag ha solo due valori con tutta probabilità (0 ed 1), e questi due valori sono troppo
troppo generici, la RAM è piena di 0 ed 1, quindi direi di cercare invece il countdown, che fa tutti i valori da 100 ad 1, che sono sicuramente meno comuni.
Step 3: Search for Cheat
Per trovare il countdown ci serviremo di uno strumento presente sul desmume (forse c'è anche su no$, non sono sicuro tho): il search for cheat.
Questo signore fa uno scan della RAM cercando un dato valore X che gli diamo, e salva poi tutti gli offset che in questo momento contengono il nostro valore X in una lista L.
La prossima volta che faremo una ricerca per un altro valore, tipo X', il search for cheat non farà uno scan di tutta la RAM, ma solo degli indirizzi che sono nella sua lista L e la aggiornerà con i valori che prima contenevano X, e ora contengono X'.
Possiamo ripetere questo step quante volte vogliamo con X'', X''', X
n finchè L non contiene un solo offset: quell'offset sarà quello che contiene l'informazione che ci serve.
Per fare un esempio più pratico, apriamo Platino con il Desmume, usiamo un repellente e andiamo su
Tools->
Cheats->
Search; in questo menù scegliamo Size:
1 byte (stiamo cercando un valore >=100, un byte è più che sufficiente), sign:
unsigned ed "
Exact value search", premiamo poi su "
Search" e scriviamo il nostro valore (in decimale, mi racommando), che in questo caso sarà "
100", perchè appena usato il repellente mancano ancora 100 passi allo scadere dell'effeto; premiamo "
Search" e chiudiamo la finestra.
Tornati al gioco, facciamo un passo e ritorniamo sul Search for Cheat, ma questa volta inseriamo come valore "99"; ripetiamo questo passo continuando a decrementare il numero ogni volta che facciamo un passo finchè la finestra di Cheat Search non ci darà
number of results: 1.
A questo punto abbiamo trovato l'offset che contiene il nostro valore di countdown, clicchiamo su "
View" e dovremmo vederlo; se state giocando a Platino EU versione 10 questo valore sarà
0x022861EB.
Step 4: I breakpoint
Ora che sappiamo dove sta il dato che ci interessa, possiamo facilmente trovare la routine che lo modifica mediante l'utilizzo di
breakpoint.
Un breakpoint è un punto del codice nel quale lo stesso deve fermare l'esecuzione, questo punto è definito dallo sviluppatore a fini di debug o, come in questo caso, reverse engineering.
In particolare ci sono 3 tipi fondamentali di breakpoint:
- Breakpoint del codice: diciamo al codice che quando arriva ad una certa istruzione, questo si deve fermare
- Breakpoint on read: diciamo al codice che quando legge un certo indirizzo, questo si deve fermare
- Breakpoint on write: diciamo al codice che quando scrive ad un certo indirizzo, questo si deve fermare
Noi ci serviremo del no$gba per mettere un breakpoint on read sull'indirizzo che abbiamo trovato precedentemente (potremmo usarne anche uno in write in questo caso e cambierebbe poco).
Apriamo quindi il nostro no$gba e apriamo la ROM...con delle opzioni di menù francamente
raccapriccianti:
File->
Cartridge menu(FileName).
Perchè "Open ROM" come un comune cristiano ci faceva schifo.
A questo punto possiamo utilizzare di nuovo il nostro repellente e, andando su
Debug->
Define Break/Condition possiamo definire un breakpoint di read con una sintassi ancora peggiore di quella usata per aprire la ROM: [OFFSET]? (quindi, nel nostro caso [0x022861EB]? ). e premiamo su OK. A questo punto avremo definito il nostro breakpoint!
Andiamo su
Run->
Run per far ripartire l'emulazione, facciamo un passo e...nulla.
Qui ho imprecato un po' prima di provare a fare una cosa: andando su
Window->
Debug Window->
Data la finestrella più in basso del debugger diventerà una visualizzazione in tempo reale della RAM; andando poi su
Search->
Goto, move cursor to address nnnn e inserendo il nostro indirizzo, troveremo 00, ma se faremo dei passi, noteremo che il valore
nella riga esattamente sotto continua a decrementare di uno. Si, tra no$gba e desmume c'è una discrepanza di 0x10 byte, non ho idea del perchè ma è così, magari è il mio desmume, boh.
Inserendo quindi il breakpoint in read come [0x022861
FB]? e facendo un passo
finalmente l'emulatore si stopperà all'esecuzione della routine che controlla quel valore, ovvero quella del repellente.
Sappiamo che quella routine sia quella corretta perchè:
- Nessun'altra legge quel valore (né avrebbe interesse nel farlo): se infatti anche facessimo Run di nuovo, l'esecuzione si arresterebbe sempre nello stesso punto
- Controllando da IDA o anche da no$gba il decompilato, esce fuori quella che sembra essere a tutti gli effetti la routine del repellente:
Codice:
PUSH {R4,LR}
MOVS R4, R1 @dunno
BL 0xFFF720F4 @dunno^2
BL 0xFFF7228C @dunno
LDRB R1, [R0] @r0 a questo punto dell'esecuzione contiene 0x022861FB
CMP R1, #0
BEQ loc_18B4E0 @Se quel valore contiene 0, allora vai alla fine, perchè vuole semplicemente dire che il player non ha in utilizzo un repellente
SUBS R1, R1, #1 @Altrimenti vai avanti e sottrai uno al numero di passi
STRB R1, [R0] @Scrivi in memoria il valore aggiornato
LDRB R0, [R0] @Ricarica il valore in r0 (a questo punto sarebbe la stessa cosa di MOV r0, r1
CMP R0, #0 @Se siamo arrivati a 0 vuol dire che il repellente era attivo ma ora ha finito l'effetto
BNE loc_18B4E0 @Se è diverso da 0, vai alla fine (nota il BNE)
MOVS R1, #0x7F @Questa parte di routine gestisce il fatto che sia finito l'effetto del repellente
MOVS R0, R4
LSLS R1, R1, #4
MOVS R2, #0
BL 0xFFF83140
loc_18B4E0:
MOVS R0, #1
POP {R4,PC}
Step 5: ROM e RAM
Ah, ma i più furbi di voi sanno che non abbiamo trovato l'indirizzo
vero della routine: se fossimo su GBA saremmo già in vacanza al Cairo, ma l'NDS ha un ultimo ostacolo: il codice viene caricato in RAM prima di essere eseguito, non viene eseguito dalla ROM, quindi a partire da quello in RAM dobbiamo trovare quello in ROM, come?
...
...
...
Non ne ho idea.
Quindi ho utilizzato un metodo molto molto molto molto molto
molto molto molto molto molto molto molto molto molto molto molto molto molto molto moltopoco ortodosso.
Ecco, vedete la rappresentazione esadecimale delle istruzioni (sotto spoiler)?
Ecco, invertendo i byte a due a due (mettendoli in little endian) otterremo la rappresentazione byte per byte di quella routine e... Beh, cercando quella lunga stringa di byte è praticamente impossibile ci siano più occorrenze quindi... si, ho semplicemente composto la routine in esadecimale, aperto un editor esadecimale e cercato lo stringone di byte.
In particolare per la routine evidenziata lo stringone esadecimale sarebbe:
Codice:
10 B5 0C 1C E6 F5 1C FE E6 F5 E6 FE 01 78 00 29 0C D0 49 1E 01 70 00 78 00 28 07 D1 7F 21 20 1C 09 01 00 22 F7 F5 32 FE 01 20 10 BD 00 20 10 BD
Cercando questa roba nell'editor esadecimale l'unico offset che corrisponde è 0x18B4B4, e quella è la nostra routine.
Step 6: To be continued
A questo punto abbiamo la routine, possiamo dirottarla, ma mi chiedo, come diavolo faremo a chiamare una routine nostra a partire da qua se il nostro codice realisticamente non sarà caricato in RAM?
Essendo appunto ignorante dell'hacking NDS non so (ancora) rispondere, magari esiste già un metodo testato e funzionante