Hearts reversing
From UIC
Hearts revesing
Contents |
| Hearts reversing | |
|---|---|
| Author: | SepticConvulsion |
| Email: | septicconvulsion@gmail.com |
| Website: | |
| Date: | 06/12/2008 (dd/mm/yyyy) |
| Level: |
|
| Language: | Italian |
| Comments: | Vediamo le carte degli altri giocatori |
Introduzione
Visto che ero stufo di continuare a perdere costantemente contro "giovanna", ho deciso che avrei utilizzato le mie neonate conoscenze di reversing per barare.
Questo e' il mio primo reversing e... come dire... state attenti che potrei aver scritto delle cretinate! :)
Notizie sul Programma
Il target e' Rete di Windows Hearts 5.1 distribuito con Windows XP Home ed. Credo che le altre versioni non si discostino molto da quella reversata qui.
Essay
Come prima cosa ho messo un bp sulla funzione "rand" e "srand": a priori prevedevo che queste due funzioni sarebbero state usate per generare la distribuzione casuale delle carte.
Altra regola generale è stato evitare di seguire tutte le chiamate esterne al modulo principale del programma (ovvero quelle fuori da "mshearts").
La prima chiamata di "srand" non ci dice niente di interessante sulle carte.
Il gioco chiede quindi di inserire il nome del giocatore e subito dopo brekka su una chiamata a "rand".
A occhio la sezione di codice non sembrava di nuovo quella cercata: la funzione rand veniva chiamata una sola volta, mentre per distribuire le carte (52 carte), era presumibile che rand si trovasse all'interno di un ciclo. In pratica qualcosa di simile a:
Per sicurezza mi sono messo cmq a steppare in di tutte le funzioni interne al modulo principale che seguivano. Non è servito a molto fino a che non sono entrato in 01007F89.
01007F8E |. E8 DD2C0000 CALL <mshearts.gestione_6>
01007F93 |. 81EC 04010000 SUB ESP,104
01007F99 |. 53 PUSH EBX
01007F9A |. 56 PUSH ESI
01007F9B |. 57 PUSH EDI
01007F9C |. 6A 34 PUSH 34
01007F9E |. 8BF1 MOV ESI,ECX
01007FA0 |. 33C0 XOR EAX,EAX
01007FA2 |. 59 POP ECX
01007FA3 |> 898485 F0FEFF>/MOV DWORD PTR SS:[EBP+EAX*4-110],EAX
01007FAA |. 40 |INC EAX
01007FAB |. 3BC1 |CMP EAX,ECX
01007FAD |.^ 7C F4 \JL SHORT mshearts.01007FA3
01007FAF |. 8365 EC 00 AND DWORD PTR SS:[EBP-14],0
01007FB3 |. 894D E8 MOV DWORD PTR SS:[EBP-18],ECX
01007FB6 |> FF15 08140001 /CALL DWORD PTR DS:[<&msvcrt.rand>]
01007FBC |. 99 |CDQ
01007FBD |. F77D E8 |IDIV DWORD PTR SS:[EBP-18]
01007FC0 |. 8B45 EC |MOV EAX,DWORD PTR SS:[EBP-14]
01007FC3 |. 6A 0D |PUSH 0D
01007FC5 |. 5F |POP EDI
01007FC8 |. 5B |POP EBX
01007FC9 |. 8BCA |MOV ECX,EDX
01007FCB |. 99 |CDQ
01007FCC |. F7FF |IDIV EDI
01007FCE |. 2B86 40010000 |SUB EAX,DWORD PTR DS:[ESI+140]
01007FD4 |. 8BFA |MOV EDI,EDX
01007FD6 |. 83C0 04 |ADD EAX,4
01007FD9 |. 99 |CDQ
01007FDA |. F7FB |IDIV EBX
01007FDC |. 8D848D F0FEFF>|LEA EAX,DWORD PTR SS:[EBP+ECX*4-110]
01007FE3 |. 8B18 |MOV EBX,DWORD PTR DS:[EAX]
01007FE5 |. C1E7 04 |SHL EDI,4
01007FE8 |. 8D8C96 300100>|LEA ECX,DWORD PTR DS:[ESI+EDX*4+130]
01007FEF |. 8B11 |MOV EDX,DWORD PTR DS:[ECX]
01007FF1 |. 895C17 1C |MOV DWORD PTR DS:[EDI+EDX+1C],EBX
01007FF5 |. 8B09 |MOV ECX,DWORD PTR DS:[ECX]
01007FF7 |. 33DB |XOR EBX,EBX
01007FF9 |. FF4D E8 |DEC DWORD PTR SS:[EBP-18]
01007FFC |. FF45 EC |INC DWORD PTR SS:[EBP-14]
01007FFF |. 837D EC 34 |CMP DWORD PTR SS:[EBP-14],34
01008003 |. 895C0F 28 |MOV DWORD PTR DS:[EDI+ECX+28],EBX
01008007 |. 8B4D E8 |MOV ECX,DWORD PTR SS:[EBP-18]
0100800A |. 8B8C8D F0FEFF>|MOV ECX,DWORD PTR SS:[EBP+ECX*4-110]
01008011 |. 8908 |MOV DWORD PTR DS:[EAX],ECX
01008013 |.^ 7C A1 \JL SHORT mshearts.01007FB
Questa sembra finalmente la parte cercata: contiene una bella chiamata a rand all'interno di un ciclo! Bene! Ora analizziamo la prima parte del codice.
ECX contiene il numero di carte contenute nel mazzo (52d = 42h) e viene passato come parametro dalla funzione chiamante.
01007FAA |. 40 |INC EAX
01007FAB |. 3BC1 |CMP EAX,ECX
01007FAD |.^ 7C F4 \JL SHORT mshearts.01007FA3
Qui abbiamo un ciclo for che inserisce tutti i numeri da 0 a 52 nell'array posizionato all'indirizzo EBP+EAX*4-110. Chiameremo questo array "carte". Se non siete famigliari a come si accede a un array (non che io sia esperto, eh!) l'indirizzamento funziona cosi: EBP-110 è fisso per questo ciclo, e in pratica indica da dove inizia l'array "carte". EAX varia nel loop ed è indice che scorre l'array. Notare che è moltiplicato per 4 perchè si stanno scrivendo DWORD nell'array: quindi tra un entry è l'altra devono esserci 4 byte! Riassumendo in C sarebbe:
carte(EAX) = EAX;
}
Tornando al listato assembler abbiamo:
01007FB3 |. 894D E8 MOV DWORD PTR SS:[EBP-18],ECX ; n_carte_disponibili=n_carte_totali
Queste istruzioni servono esattamente a settare a 0 la variabile n_carte_date (il numero di carte distribuite dal mazzo) all' indirizzo EBP-14. In EBP-18 vengono salvate il numero di carte ancora da distribuire (in questo caso ancora 52), che ora chiamiamo n_carte_disponibili.
Analizziamo quindi il mega ciclo che inizia in 01007FB6 e finisce in 01008013.
...
01007FFC |. FF45 EC |INC DWORD PTR SS:[EBP-14] ; n_carte_date++
01007FFF |. 837D EC 34 |CMP DWORD PTR SS:[EBP-14],34 ; n_carte_date == 52?
...
01008013 |.^ 7C A1 \JL SHORT mshearts.01007FB
Le condizioni per continuare a ciclare sono che n_carte_date sia < 52 (34h). In C:
...
}
Andando avanti col disassembato troviamo:
01007FBC |. 99 |CDQ
01007FBD |. F77D E8 |IDIV DWORD PTR SS:[EBP-18] ; EAX = EAX / n_carte_disponibili ; EDX = resto1
Rand ci sbatte valori random in EAX e ECX. CDQ non è che abbia gran utilita' qui (correggetemi se sbaglio). Quindi IDIV mette EAX = EAX / n_carte_disponibili e in EDX il resto di tale divisione. Dato che EDX puo' assumere valori compresi tra 0 e 52 questo fa presurre che EDX rappresenti appunto la carta che è stata estratta!
01007FC3 |. 6A 0D |PUSH 0D
01007FC5 |. 5F |POP EDI ; EDI = 13 = max_carte_per_giocatore
01007FC6 |. 6A 04 |PUSH 4
01007FC8 |. 5B |POP EBX ; EBX = 4 = n_giocatori
EAX assume n_carte_date, EDI e EBX prendono i valori di "max_carte_per_giocatore" (numero di carte da dare a ogni giocatore) e EBX il numero di giocatori; entrambe sono passati come paramentri dalla funzione chiamante.
01007FCB |. 99 |CDQ
01007FCC |. F7FF |IDIV EDI ; EAX = n_carte_date / max_carte_per_giocatore
; EDX = carte_giocatore_corrente
Ancora IDIV: con due semplici calcoli si capisce che EAX indica ora a quale giocatore stiamo ora distribunedo le carte mentre EDX indica quante carte abbiamo gia' distribuito al giocatore corrente. Esempio: n_carte_date = 22 -> 22 / 13 = 1 resto 9 -> stiamo dando le carte al giocatore 2 (ricordate che EAX è un indice e quindi parte da 0) e ha gia 9 carte in mano (gli sitiamo dando la decima).
01007FD4 |. 8BFA |MOV EDI,EDX ; EDI = carte_giocatore_corrente
01007FD6 |. 83C0 04 |ADD EAX,4 ; EAX = EAX + 4
01007FD9 |. 99 |CDQ
01007FDA |. F7FB |IDIV EBX ; EAX = EAX / 4; EDX = giocatore_corrente
La SUB di solito non fa nulla perchè il secondo operando è (solo nel mio caso?) sempre 0. Poi segue l'operazione matematica EAX = (EAX + 4) / 4 = EAX / 4 + 1 e mette il resto in EDX (valore tra 0 e 3). Onestamente penso che la ADD non sia molto utile in quanto il resto della divisione non cambia e il quoziente non viene piu' considerato nel resto del ciclo. Inoltre EDX rappresenta sempre a quale giocatore stiamo distribuendo le carte, esattamente come EAX dopo 01007FCC.
01007FE3 |. 8B18 |MOV EBX,DWORD PTR DS:[EAX] ; EBX = carta(resto1)
EAX ora punta all' array "carte" (in EBP-110), entry data da ECX. In EBX finisce il la vera e propria carta, o meglio il "codice carta" che rappresenta la carta (in questo caso rappresentata da un numero intero).
01007FE8 |. 8D8C96 300100>|LEA ECX,DWORD PTR DS:[ESI+EDX*4+130] ; ECX punta struttura mano_giocatore(EDX)
01007FEF |. 8B11 |MOV EDX,DWORD PTR DS:[ECX] ; EDX = mano_giocatore(ECX)
01007FF1 |. 895C17 1C |MOV DWORD PTR DS:[EDI+EDX+1C],EBX ; mano_giocatore(ECX).slot_carte(EDI) = EBX
SHL moltiplica EDI = EDI*16. ECX punta a un entry di un array di indirizzi, "mano_giocatore" (indirizzo in ESI+130) dove EDX appunto indica il giocatore corrente. EDX assume quindi il valore di un secondo array dove sono contenute le carte assegnate al giocatore corrente ("slot_carte"). L'ultimo MOV serve a immettere la carta scelta a caso nella mano del giocatore nell'entry segnalata da EDI*16 (ci sono 3 DWORD non scritte tra una volta e la seguente). In pratica: mano_giocatore(giocatore_corrente)->slot_carte(carte_giocatore_corrente) = carta(resto1)
Beh, ora il gioco è fatto! Abbiamo l'indirizzo in cui trovare le carte di ogni giocatore: tutti gli indirizzi contenuti nei 4 array "slot_carte". Per trovarli in memoria basta guardare i 4 indirizzi scritti in "mano_giocatore". Se facciamo un bel "follow in dump" del primo indirizzo (occhio che non è sempre lo stesso) si ha la figura seguente.
Il riquadro rosso rappresenta appunto "slot_carte": ke righe di "FF" sono entry a cui non e' ancora stata assegnata nessuna carta. Il riquadro verde sara' spiegato in seguito.
01007FF7 |. 33DB |XOR EBX,EBX ; EBX = 0
01007FF9 |. FF4D E8 |DEC DWORD PTR SS:[EBP-18] ; n_carte_disponibili--
01007FFC |. FF45 EC |INC DWORD PTR SS:[EBP-14] ; n_carte_date++
01007FFF |. 837D EC 34 |CMP DWORD PTR SS:[EBP-14],34 ; n_carte_date < 52?
ECX punta direttamente a slot_carte del giocatore corrente e EBX viene azzerato con la XOR. Quindi le due variabili n_carte_disponibili e n_carte_date vengono aggiornate. Il CMP serve a controllare se si deve ancora ricominciare il ciclo o meno.
01008007 |. 8B4D E8 |MOV ECX,DWORD PTR SS:[EBP-18] ; ECX = n_carte_disponibili
0100800A |. 8B8C8D F0FEFF>|MOV ECX,DWORD PTR SS:[EBP+ECX*4-110] ; ECX = carta(n_carte_disponibili)
01008011 |. 8908 |MOV DWORD PTR DS:[EAX],ECX ; carta(resto1) = carta(n_carte_disponibili)
01008013 |.^ 7C A1 \JL SHORT mshearts.01007FB6
Entra in gioco un nuovo array in posizione "ECX+28" che si accede 4 byte si 4 no tramite EDI. Si puo' notare che questo array è il "complementare" di "slot_carte".
01008007 e 0100800A servono a fare in modo che ECX contenga il valore della carta con entry piu' alto all'interno di "carte" (non è detto che sia la carta di valore piu' alto!). La MOV successiva sposta tale carta nel posto di quella appena distribuita: EAX punta ancora all'entry della carta estratta da 01007FDC. Si nota che la lunghezza dell'array "carte" è uguale a n_carte_disponbili. Queste operazioni servono quindi a fare in modo che "carte(0 a n_carte_disponibili)" contenga solo carte non ancora distribuite. JL finale fa ricominciare se il CMP di 01007FFF ha dato esito positivo o no.
Il problema si sposta quindi nell'associare il numero che rappresenta ogni carta con l'effettiva carta giocata e a chi corrisponde ogni mano puntata in "mano_giocatore". Io ho usato un trucchetto: ho semplicemente modificato "slot_carte" appena terminata la distribuzione delle carte mettendo carte ripetute in mano. Sappiamo che tutte le carte sono uniche in hearts: si modifica la memoria di slot_carte (indirizzo dato da mano_giocatore(0)) mettendo due volte uno stesso codice carta (per esempio "00"). Facendo ripartire l'esecuzione, il gioco mostrera' che nella mia mano ci sono due carte uguali. Quindi si puo' associare il codice carta 00 all'asso di fiori. Si puo' estendere il ragionamento a forzare in mano anche 3 codici uguali, 4 codici ugali e 5 carte uguali, ogni volta scelgiendo un codice carta diverso. Facendo ripartire il gioco ritroveremo quindi gli abbinamenti codice carta <-> carta effettiva
Note Finali
L'obbiettivo è stato raggiunto. Basandomi sul tute di AlexMark sul Memory Patching and DMA Defeating, ora sto scrivendo un programmino per leggere direttamente la memoria del gioco durante l'esecuzione e outputtare le carte di tutte le mani. Devo solo capire un attimo quali sono gli indirizzi che variano ogni riavvio del gioco.
Scusatemi ancora per le eventuali cazzate che ho scritto e per la mia pessima conoscenza della programmazione (sono arrugginito e pigro). Consigli, commenti e correzioni sono MOLTO benvenuti.
Disclaimer
I documenti qui pubblicati sono da considerarsi pubblici e liberamente distribuibili, a patto che se ne citi la fonte di provenienza. Tutti i documenti presenti su queste pagine sono stati scritti esclusivamente a scopo di ricerca, nessuna di queste analisi è stata fatta per fini commerciali, o dietro alcun tipo di compenso. I documenti pubblicati presentano delle analisi puramente teoriche della struttura di un programma, in nessun caso il software è stato realmente disassemblato o modificato; ogni corrispondenza presente tra i documenti pubblicati e le istruzioni del software oggetto dell'analisi, è da ritenersi puramente casuale. Tutti i documenti vengono inviati in forma anonima ed automaticamente pubblicati, i diritti di tali opere appartengono esclusivamente al firmatario del documento (se presente), in nessun caso il gestore di questo sito, o del server su cui risiede, può essere ritenuto responsabile dei contenuti qui presenti, oltretutto il gestore del sito non è in grado di risalire all'identità del mittente dei documenti. Tutti i documenti ed i file di questo sito non presentano alcun tipo di garanzia, pertanto ne è sconsigliata a tutti la lettura o l'esecuzione, lo staff non si assume alcuna responsabilità per quanto riguarda l'uso improprio di tali documenti e/o file, è doveroso aggiungere che ogni riferimento a fatti cose o persone è da considerarsi PURAMENTE casuale. Tutti coloro che potrebbero ritenersi moralmente offesi dai contenuti di queste pagine, sono tenuti ad uscire immediatamente da questo sito.
Vogliamo inoltre ricordare che il Reverse Engineering è uno strumento tecnologico di grande potenza ed importanza, senza di esso non sarebbe possibile creare antivirus, scoprire funzioni malevole e non dichiarate all'interno di un programma di pubblico utilizzo. Non sarebbe possibile scoprire, in assenza di un sistema sicuro per il controllo dell'integrità, se il "tal" programma è realmente quello che l'utente ha scelto di installare ed eseguire, né sarebbe possibile continuare lo sviluppo di quei programmi (o l'utilizzo di quelle periferiche) ritenuti obsoleti e non più supportati dalle fonti ufficiali.