Questo Forum utilizza i Cookies
Questo Forum fa uso di Cookies per memorizzare le informazioni di login se sei registrato o sulla tua ultima visita se non siete registrati. I Cookies sono piccoli documenti di testo memorizzati sul vostro computer; i Cookies impostati dal Forum possono essere utilizzati solo su questo sito e non costituiscono rischio per la sicurezza. I Cookies su questo forum servono anche a monitorare gli argomenti specifici che hai letto. Si prega di confermare se accettare o rifiutare i Cookies impostati.

Un cookie verrà memorizzato nel browser indipendentemente dalla scelta, per evitare che il Forum faccia nuovamente questa domanda. Sarete in grado di modificare le impostazioni dei cookies in qualsiasi momento tramite il link a piè di pagina

Rispondi 
[Nds] Informazioni base per Binary Hacking NDS
Avon
The down is real
**
Offline

Messaggi: 60
Registrato: 08-09-2014
Reputazione: 2
Mi Piace Ricevuti: 89
Pok√©Money: 160.00‚āĪ
Messaggio: #1
Informazioni base per Binary Hacking NDS
Ciao, apro il thread per dettagliare in una forma umanamente comprensibile quello che ho scoperto durante la ricerca descritta in questo post in R&D, cominciamo.

TL; DR

Too Long; Didn't Read: se siete interessati solo alla soluzione per la vostra hack senza tutta la teoria, vi rimando al capitolo finale: "Implementazione per i singoli giochi".


Introduzione

Per prima cosa, a chi è rivolta questa guida?
Questo post si rivolge a tutti coloro che hanno buone conoscenze del binary hacking GBA e vogliono approdare sull'NDS, in quanto penso sia abbastanza condivisbile l'opinione che l'hacking NDS sia pi√Ļ complesso di quello GBA, sia per l'aspetto tecnico, che per l'ovvia mancanza di risorse esaustive.
Mi aspetto quindi che chi legge:
  • Conosca terminologie quali "offset", "byte", "ROM", "RAM", "thumb", "struttura dati", e simili
  • Sappia almeno a grandi linee comprendere del codice ASM (in questa istanza utilizzeremo solo la modalit√† thumb)
  • Abbia almeno una vaga idea di come un gioco pok√©mon sia strutturato (flag, script, mappe, etc)
Se quindi stai leggendo questa introduzione e noti di essere carente in alcuni o tutti questi punti, sappi che verranno dati per scontati in quanto segue e che non è questo il thread nel quale si dettaglia il funzionamento del Binary Hacking.

Quello che segue √® una descrizione del funzionamento delle ROM NDS, con annessa metodologia di base per applicare l'inserimento di routine custom: quest'ultima pu√≤ essere vista come il "master cheat" di quando si usava l'Action Replay: senza questo √® impossibile (o molto pi√Ļ complesso) inserire delle routine custom.

Ultimo ma non ultimo, vorrei ribadire anche in questa sede che non sono un hacker NDS, né ho intenzione di diventarlo: questo post deriva da quello in R&D, che deriva a sua volta dalla richiesta di un utente, e visto che ho pensato potesse essere utile in futuro anche ad altre persone, sto scrivendo quanto ho scoperto durante la mia ricerca.



Strumenti e documenti

Strumenti, con eventuali link:
  • DesMume (Memory viewer con annesso dump ed emulatore un po' pi√Ļ stabile)
  • No$Gba debugger: link (Vero e proprio debugger, con dump dello stack, disassemblatore, possibilit√† di inserire breakpoint, etc...)

Documenti:


Terminologia

Per comodit√†, andr√≤ a definire una serie di termini e notazioni che utilizzer√≤ d'ora in poi per rendere pi√Ļ fluida la lettura e stesura del documento:
NotazioneSignificato
bitCifra binaria
byte2 cifre esadecimali (8 bit)
HWord"Half-Word": 2 byte (4 cifre esadecimali, 16 bit)
Word2 HWord (4 byte, 8 cifre esadecimali, 32 bit)
?Rappresenta un semplice numero in decimale
0x?Rappresenta un numero esadecimale (0x10 = 16)
?bRappresenta un numero binario (11001b = 0x19 = 25)
9x?Indica un indirizzo, espresso in esadecimale big endian, che ha come base l'inizio del file arm9.bin
ROx?Indica un indirizzo, espresso in esadecimale big endian, che ha come base l'inizio della ROM
RAx?Indica un indirizzo, espresso in esadecimale big endian, che ha come base l'inizio della RAM
NOMEFILEx?Indica un indirizzo, espresso in esadecimale big endian, che ha come base l'inizio del file NOMEFILE. (notare che i file Overlay verranno espressi come "oXX", dove "XX" è l'id del file Overlay).
HEx?Indica un indirizzo, espresso in esadecimale big endian, che ha come base l'inizio dell'heap



Offset in RAM statici VS dinamici

La prima cosa che ho notato nella mia ricerca sull'NDS è quella che gli offset in RAM parevano cambiare di riavvio in riavvio del gioco, contrariamente ai giochi GBA, dove sappiamo che un dato offset in RAM conterrà sempre un dato X (sappiamo per esempio che su FR USA 1.0, il party del giocatore parte sempre a RAx02024284), ma sulle rom NDS non pare essere così.
Infatti, come √® possibile notare in altri funzionamenti che esploreremo in seguito, il NDS si comporta molto pi√Ļ come un calcolatore moderno utilizzando un'allocazione dinamica degli indirizzi, tramite delle funzioni che si possono tranquillamente paragonare al malloc ed al free del C; una parte di memoria RAM viene quindi dedicata ad essere quella che in informatica viene chiamato "heap", ovvero una parte di RAM con indirizzo variabile che contiene i dati utili al programma (in questo caso al gioco).

Filesystem VS ROM

E' utile sottolineare come, mentre i giochi GBA sono un'unico blocco di dati, le ROM NDS contengono un vero e proprio filesystem (quindi on'organizzazione di dati in file e cartelle), in particolare utilizza un filesystem di tipo FAT (link wikipedia al FAT; link a indirizzo e struttura della FileNameTable e FileAllocationTable nelle ROM NDS).
Oltre a ci√≤, i giochi pok√©mon (cos√¨ come molti altri giochi NDS) utilizzano dei file NARC (Nitro Archive), che possono essere visti come delle cartelle che contengono altri file (questo NON E' ACCURATO: i NARC sono pi√Ļ comparabili a dei filesystem in s√©, ma pensarli come cartelle aiuta a riportarli a qualcosa di conosciuto e non √® un'astrazione cos√¨ lontana dalla realt√†).
Sebbene questa distinzione sia importante, ci terrei a far presente che quella del filesystem √® solo un'astrazione: un file non √® altro che un segmento della ROM, nulla di pi√Ļ e nulla di meno, quindi cambiando un byte dentro al file si cambia un byte della ROM e viceversa, le due cose sono una.

Overlay

Un altro fenomeno che caratterizza l'NDS √® l'utilizzo di quello che in informatica viene chiamato "Overlay" (link all'articolo wikipedia per pi√Ļ info).
Infatti, al contrario dei giochi GBA, dove tutto il codice è contenuto nella ROM e la CPU ne ha libero accesso, l'Overlay consiste nel caricare e scaricare in RAM il codice "a blocchi", in base a quello che serve in quel dato momento; questi blocchi nelle ROM NDS pokémon sono contenuti in dei file che, giustamente, si chiamano "Overlay".
Questo avviene anche perchè la CPU NDS non ha visione diretta sulla ROM NDS, in altre parole può solo dire alla console che "Hey, mi serve questo pezzo di ROM, quindi caricamelo in RAM", non può fare come il GBA che dice "leggi questo dato dalla ROM".
Per fare un esempio, esiste un Overlay per la gestione dell'overworld, uno per le battaglie, uno per lo zaino, uno per i box, etc... e vengono caricati in RAM per essere eseguiti a seconda di quello che serve in quel dato momento.
Esistono anche due file Overlay speciali chiamati "arm9.bin" e "arm7.bin", che si differenziano dagli altri perchè, mentre tutti gli altri vengono caricati e scaricati a piacere, questi due file vengono caricati all'accensione del gioco in RAM e da lì non si schioderanno mai; questi file (in particolare l'arm9) contengono routine di funzionalità generale che sono sempre utili (tipo (re)set e check di flag, (de)crypt di pokémon, caricamento di dati a partire da dei file, malloc, free, etc...)

Numero di CPU

Semplicemente l'NDS ha due CPU: una ARM946E-S e un ARM7TDMI (ai quali d'ora in poi ci riferiremo come NDS9 e NDS7, rispettivamente), quest'ultimo è lo stesso che era presente sul GBA (anche se sulla carta ha il doppio dei MHz).
I due processori condividono l'instruction set (che vuol dire che i comandi ASM e la maniera nella quale vengono compilati non cambia tra i due processori), la differenza principale √® che il nuovo processore √® sulla carta molto pi√Ļ veloce (dico sulla carta, perch√® come riportato su GBATek, l'architettura NDS √® un filo problematica e alla fine spesso risulta essere addirittura pi√Ļ lenta di quella GBA).
Ad ogni modo, per i giochi NDS viene usato quasi esclusivamente l'NDS9: l'NDS7 è riservato per pochissime subroutine e per i giochi GBA.

Gestione della RAM risultante

Il risultato delle 3 sezioni appena discusse è il seguente:
  • Al bootup del gioco, vengono caricati i file arm9.bin e arm7.bin nella RAM (l'indirizzo al quale vengono caricati √® reperibile nell'header della ROM, come indicato su GBATek)
  • Viene eseguita la routine "main" dell'arm9, che inizializza le varie strutture dati e aree di memoria che saranno utili al resto del gioco (tipo l'heap), viene anche caricato il primo Overlay del gioco (quello contenente intro e titlescreen, verosimilmente), parte l'esecuzione del mainloop: il loop infinito che controlla gli input del giocatore ed eventualmente delega le conseguenze all'Overlay idoneo caricato in memoria in quel momento.
  • Mano a mano che si va avanti nel gioco e "si fanno cose", verranno caricati i vari file nelle sezioni appropriate di RAM (i file Overlay negli indirizzi specificati nella loro tabella dedicata, i vari dati relativi alla run del giocatore nell'heap, i file contententi dati relativi alla grafica nella VRAM, etc..)



What to do next?

L'idea sarebbe di riportare la metodologia GBA sull'NDS, ma sappiamo che ci sono delle complicazioni associate a questo punto, vale a dire che:
  1. Non possiamo semplicemente schiaffare la routine in una parte libera della ROM perchè la CPU non ne avrebbe visibilità: dovremmo trovare una parte libera della RAM dove infilare il nostro codice
    Soluzione: in maniera simile a quanto si fa gi√† nelle hack di I/II/III gen, la cosa pi√Ļ semplice ed ignorante da fare √® disabilitare alcuni box, che tanto 18 box sono decisamente troppi. Questo funziona ancora meglio sulle hack NDS rispetto a quelle delle gen precedenti visto che ci sono pi√Ļ box e i pok√©mon occupano pi√Ļ spazio.
  2. Dobbiamo trovare il modo di caricare il nostro codice in quella parte libera della RAM
    Soluzione: Dirottare il codice, idealmente quando inizializza l'heap: il gioco utilizza infatti la funzione di "malloc" e non "calloc", che vuole fondamentlamente dire che l'area di memoria predisposta dal malloc non è detto sia stata inizializzata a 0, e questo potrebbe dare dei probelmi.
    Per ovviare a questo problema, la ROM esegue una routine che, termine tecnico, pialla l'heap prima di caricare il salvataggio.
    L'ideale è di intromettersi in questo processo per far si che immediatamente dopo il piallamento, la nostra routine venga caricata al posto dei box rimossi.
  3. Dobbiamo trovare un punto della ROM che si possa sovvrascrivere per inserire le nostre routine custom, idealmente un file non utilizzato o programmare l'inserimento di un nuovo file manipolando la FNT e la FAT; non sono sicuro della fattibilità di questa cosa, quindi per questa guida ci atterremo alla prima ipotesi e scarteremo un file.



Come ho reperito le informazioni utili

Allora, sarò molto onesto: per molte cose ho barato.
Ho infatti lavorato (visto che avevo una Nuzlocke in corso su quel titolo) su Pokèmon Platino (quando mi pare di capire lo standard sia HG?) e caso vuole che esista PokePlat, un disassembly fantastico su questo progetto, per il quale vanno assolutamente dati cento miliardi di ringraziamenti e crediti a JimB16 e huderlem.
Questo non vuole però dire che quanto scoperto (o la metodologia) non possa essere utile ad altre hack, quindi cercherò di offrire i miei two cents su quanto fatto, offrendo poi in una sezione successiva le conclusioni pratiche raggiunte su Pokémon Platino.
Se qualcuno in futuro dovesse riuscire a fare la stessa cosa su altri titoli, risponda pure al post qua sotto.

Trovare gli offset dei dati

La metodologia di base √® quella con il Search For Cheat (SFC) descritta nel primissimo post del thread in R&D, quindi vi rimando a quello per saperne di pi√Ļ.

La primissima cosa che ho fatto è stato quindi trovare l'indirizzo contente l'indice del box attuale e dove fossero i dati dei box veri e propri: il primo trovato con un SFC mentre cambiavo box, il secondo trovato scambiando le posizioni dei pokémon nei box, facendo dei dump della RAM (Funzione "Dump All" del Memroy Viewer del DesMume) con i pokémon in varie posizioni e confrontando i risultati con WinMerge.

Come descritto prima, gli indirizzi erano però variabili, e serviva quindi anche trovare il pointer che inica quale sia l'inizio dell'heap: qui ho scommesso sul fatto che i ragazzi di PokePlat lo avessero già individuato, ed infatti nel file source/arm9_variableareas.s:150 esiste la funzione "InitVariableAreaAdresses" che fa un malloc (riga 156) e carica il risultato all'offset RAx21c0794, che è l'offset che contiene HEx0 (in notazione C potremmo dire
struct Heap **ptr = (Heap **)0x21c0794; 
: un puntatore al puntantore dell'inizio dell'heap, oppure in una cosa pi√Ļ simile all'ASM potremmo dire
heap_start = [0x21c0794]
).

Questo, come dicevo, √® stato barare per Platino, ma mi √® venuto in mente un metodo per ricavare questo indirizzo sugli latri giochi, che poi ho testato ed ha funzionato su Heart Gold: quello che ho fatto √® stato arrivare al punto di poter mettere un pok√©mon nel box (il perch√® diventer√† evidente pi√Ļ tardi), dopodich√® ho salvato, e riavviato il gioco arrivando fino al men√Ļ dove puoi caricare il salvataggio, cambiare le opzioni, fare il dono segreto, etc.
Qui ho fatto un dump della RAM intera, poi ho riavviato, sono arrivato allo stesso punto e ho ri-dumpato la memoria (ho fatto il dump della memoria a questo punto perchè in RAM non dovrebbero essere ancora caricate cose che potrebbero fare "interferenza" come posizioni di NPC o simili)
Ho poi confrontato i due dump con WinMerge, aspettandomi di trovare dei dati "sfasati" di un po' di byte, cioè gli stessi dati ma spostati di un po' tra i due casi e così è stato, sotto spoiler un esempio:
Come notate il secondo file (quello a dx) ha "14 21 6e ..." prima rispetto al primo file ( in particolare appare 0x74 byte prima).
Sono andato avanti a scrollare verso l'alto finchè sono giunto al punto in cui i due file erano identici, questo per identificare il punto in cui i due dump iniziano a diventare diverse: se nello stesso dump è presente un offset che punta all'offset nel quale i due dump si differenziano, dovremmo aver trovato l'offset dell'inizio dell'heap o qualcosa di molto vicino a quest'ultimo.
Sono quindi arrivato a questo punto, e da lì in poi entrambi i dump avevano una fila di 0 per svariate centianaia (forse migliaia) di byte
Notate che in basso a dx è evidenziato l'offset, e se cerchiamo il little endian di questo offset nello stesso dump troviamo una sola occorrenza a 0x1d1584 (che quindi srebbe RAx021d1584).
Facendo un paio di esperimenti (continuando a riavviare, cercando con il SFC l'indirizzo dell'indice del box, facendo la differenza tra quello dell'indice e quello contenuto in RAx021d1584 e verifando che questa distanza sia sempre costante), posso dire con relativa certezza che per HeartGold l'offset dell'inizio dell'heap sia contenuto a RAx021d1584 e che questo metodo possa essere replicato per tutti i giochi NDS.

A questo punto ho avuto un mezzo infarto perchè ho notato che gli slot vuoti nei box non sono riempiti a 0 come nel GBA, ma sono riempiti con dati placeholder, ho quindi anche dovuto controllare quando e dove questi dati placeholder venissero controllati: ho provato a riempire di 0 una entry e ho cercato di capire quando questa viene controllata ed eventualmente corretta o interpretata come bad egg: fortunatamente, questo viene fatto quando si "apre" il box che contiene quella entry, quindi disabilitandoli questi dati non dovrebbero venire mai toccati.

La Metodologia Bruttatm

In teoria ora andrebbero ricavati gli offset delle funzioni per poterle modificare, ma c'è un ma: gli offset vedremo che vanno ricavati a runtime, e sappiamo che l'indirizzo che otterremo è quello di quando la funzione viene eseguita in RAM, non quello che poi dobbiamo andare a modificare nella ROM!
Come ovviare? Beh, semplice, con La Metodologia Bruttatm!
Quest'operazione è stata già descritta in alcuni post del thread in R&D, la riporto qui sotto spoiler, fondamentalmente copia/incollandola, per completezza.
Trovare gli offset delle funzioni

Per tutto il capitoletto utilizzerò il concetto di "breakpoint", per sapere come inserirli sul no$gba consultate la documentazione apposita, thank u

Il primissimo indirizzo che ci serve a livello di funzioni è quello che fa il check dell'indice dei box, ricavabile con un breakpoint in lettura sull'indirizzo dell'indice del box che abbiamo trovato precedentemente e la Metodologia Bruttatm per ricavare l'indirizzo in ROM utile.
In particolare, ci aspettiamo di trovare due offset: il primo che controlla che l'id del box non sia <= 0, ed in caso lo metta a 0x12 (18, il massimo, per fare il wrap around) ed uno che controlla che l'id del box non sia >=0x13 ed in caso lo metta a 0 (sempre per fare il wrap around).
Per modificare i box selezionabili dal giocatore dovrebbe bastare modificare le istruzioni CMP associate a questi controlli.
NB: nei giochi GBA, queste istruzioni erano replicate 4 volte, una per lo Sposta Pokémon, una per il Deposita Pokémon, una per il Ritira Pokémon ed una per il Muovi Oggetti; su platino una sola CMP pare essere utilizzata per tutte e 4 le funzioni, ma non posso garantire per gli altri giochi di IV o quelli di V Gen, quindi sarebbe meglio controllare.

L'ultimo inidirizzo mancante è quello della routine che inizializza i dati presenti nel box: una volta scoperto dove questi siano basta mettere un breakpoint in scrittura su quell'indirizzo e far partire il gioco: ho notato che i dati dei box vengono scritti in 3 occasioni diverse:
  1. All'inizio del gioco, quando questi dati vengono "piallati" dall'Overlay della titlescreen
  2. Poco dopo essere stati piallati, da una funzione che:
    1. Inizializza tutti gli slot dei box con i dati "placeholder" se non esiste nessun salvataggio
    2. Carica quello che era precedentemente contenuto nei box dal salvataggio, se ne esiste uno
  3. Ovviamente, quando depositiamo/ritiriamo un pokémon, duh.

Osserviamo quindi come, il piallamento e l'inizializzazione dei dati avvengano in due momenti diversi, ma come anche i box vengano semplicemente caricati dal salvataggio se ne esiste uno, e come quindi ci basti sovvrascrivere solo il comportamento del primo bootup (perchè poi sappiamo che i dati del box non vengono controllati e sovvrascritti a meno che non vi si acceda!).



Metodologia finale

Una volta che abbiamo ricavato tutte queste info, la tattica migliore è questa:
  1. Preparazione:
    1. Scegliere un file da sacrificare, piallarlo completamente.
    2. Copia/incollare la routine che inizializza i dati dall'overlay della titlescreen all'inizio del file sacrificato, sistemanto eventuali jump
    3. Inserire nel nostro file sacrificato, in maniera che venga eseguita dopo l'inizializzazione, la routine che carica il nostro file al posto dei box (Questa d'ora in poi la chiamerò "Routine Load")
    4. Scrivere una routine che faccia un malloc della grandezza del file sacrficato e caricarlo all'indirizzo che ci viene restituito, per poi fare un bx a quell'indirizzo (Questa d'ora in poi la chiamerò "Routine Hijack") ed inserirla al posto della rotuine che fa l'inizializzazione dei dati.
      NB: Questa routine deve occupare spazio <= a quello che è occupato di default dalla chiamata alla routine di inizializzazione, perchè ovviamente va a sostituirla e non vogliamo sovvrascrivere altra roba
  2. Esecuzione:
    1. Il gioco verrà eseguito normalmente fino al punto dove l'overlay della titlescreen proverà ad inizializzare le aree dei box
    2. A questo punto entrerà in gioco la Routine Hijack che allocherà nella RAM lo spazio per il nostro file e lo caricherà lì, delegandogli l'esecuzione (andremo quindi ad eseguire la Routine Load)
    3. La Routine Load avrà il compito di eseguire quella che sarebbe la normale routine di inizializzazione (infatti i box che non usiamo vanno comunque inizializzati) e poi sovvrascrivere i box che abbiamo scelto di sacrificare con il nostro file.
    4. L'esecuizone dovrebbe poi ritornare al flusso normale, con il risultato che però ci troveremo il file caricato nei box, che possiamo utilizzare come se fosse lo spazio libero in ROM del GBA.
  3. Migliorabilità:
    1. Per ora, l'unica cosa che manca è eventualmente una terza routine, che consisterebbe nel liberare lo spazio allocato con il primo malloc, perchè si sa mai.



Implementazione per i singoli giochi

Qui troveremo le implementazioni per i singoli giochi, fornirò tutti gli offset utili e i sorgenti delle routine.
Ad oggi esiste solo quella per Platino EU/US, se qualche baldo rivoluzionario volesse applicare questa metodologia ad altri giochi e dovesse riuscirci, cercherò di aggiornare il post anche con quelle.

PLATINO US/EU:
Heart Gold US (WIP):

That's all, folks.
Se avete domande postate sotto, blablablabla, conoscete la tiritera, andate ad inserire tutte le regioni su Platino, susu
29-12-2020 05:04 PM
Cerca Cita
 Mi piace ricevuti da: MegaTorterra , IvanFGK , JackHack96 , PacoScarso , Bonnox , Flygon
Rispondi 




Utente(i) che stanno guardando questa discussione: 1 Ospite(i)
Powered by MyBB, © 2002-2021 MyBB Group. Copyright ¬© | 2021 Pok√©mon Hacking