Zoom Icon

Hearts reversing

From UIC

Hearts revesing

Contents


Hearts reversing
Author: SepticConvulsion
Email: septicconvulsion@gmail.com
Website:
Date: 06/12/2008 (dd/mm/yyyy)
Level: Working brain required
Language: Italian Flag Italian.gif
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:

for(52 volte){ carta = rand }

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.

01007F89 >/$  B8 93B40001   MOV EAX,mshearts.0100B493                
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.

01007FA2  |.  59            POP ECX                                  ;  ECX = n_carte_totali = 52

ECX contiene il numero di carte contenute nel mazzo (52d = 42h) e viene passato come parametro dalla funzione chiamante.

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

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:

for (EAX=0; EAX < 52; EAX++){
        carte(EAX) = EAX;
}

Tornando al listato assembler abbiamo:

01007FAF  |.  8365 EC 00    AND DWORD PTR SS:[EBP-14],0              ;  n_carte_date=0
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.

01007FB6  |>  FF15 08140001 /CALL DWORD PTR DS:[<&msvcrt.rand>]      ; [rand (EAX e ECX random)
...
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:

for (n_carte_date=0; n_carte_date < 52; n_carte_date++){
        ...
}

Andando avanti col disassembato troviamo:

01007FB6  |>  FF15 08140001 /CALL DWORD PTR DS:[<&msvcrt.rand>]      ; [rand (EAX e ECX random)
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!

01007FC0  |.  8B45 EC       |MOV EAX,DWORD PTR SS:[EBP-14]           ;  EAX = n_carte_date
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.

01007FC9  |.  8BCA          |MOV ECX,EDX                             ;  ECX = resto1
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).

01007FCE  |.  2B86 40010000 |SUB EAX,DWORD PTR DS:[ESI+140]          ;  EAX = giocatore_corrente
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.

01007FDC  |.  8D848D F0FEFF>|LEA EAX,DWORD PTR SS:[EBP+ECX*4-110]    ;  EAX punta carta(resto1)
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).

01007FE5  |.  C1E7 04       |SHL EDI,4                               ;  EDI * 16
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.


Mem dump jpg.JPG


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.

01007FF5  |.  8B09          |MOV ECX,DWORD PTR DS:[ECX]              ;  ECX = mano_giocatore(ECX)
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.

01008003  |.  895C0F 28     |MOV DWORD PTR DS:[EDI+ECX+28],EBX       ;  azzera [resto2 * 8 + vettore(resto3) + 28]
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.