wiki:Ruzzle-cheater
Last modified 4 years ago Last modified on 04/01/13 00:52:33

Ruzzle Cheater

Ruzzle? Progettato male, favorisce solo i bari

Parlando su irc con sand mi ha fatto venire la voglia di indagare di piu' su ruzzle per capire come funziona, un bel divertimento per il tempo libero. La prima cosa che ho scoperto e' stato che se ti colleghi via ssh al cellulare e porti indietro l'orologio puoi aumentarti il tempo all'infinito, ma questo trucchetto non era abbastanza bello da sfamare la mia curiosita', sono trucchetti semplici.

Quindi scarico l'apk sul pc e tramite un software di cui non ricordo il nome ho trasformato l'apk in un jar. Fatto questo ho estratto tutto il contenuto del jar (ho controllato e si chiama dex2jar) . Estratto i jar vedo che quello che usano e' una libreria C loro piu' tutto codice java. Non avevo mai usato un decompiler java e non pensavo uscisse qualcosa di leggibile da umano, ma ho provato lo stesso e il risultato e' stato molto ma molto stupefacente. Codice umanamente leggibile, a parte qualche piccola eccezione ma per il resto facilmente leggibile.

guardiamo un po' la struttura del codice:

src $ ls
android  com  org  se

dentro se/maginteractive/rumble/ troviamo il codice di ruzzle:

ls
activities  ad  adapters  api  BuildConfig.java  datastructure  GCM  GLES2  
Manifest.java  model  R.java  Rumble.java  util  view

la prima cosa che faccio e' cercare dove inviano i dati, e l'ho trovato facilmente dentro il file api/RumbleConnector.java. Guardando la funzione che invia ogni cosa noto una cosa che nemmeno un principiante con un minimo di cervello farebbe:

private static String post(String s, String s1)
        throws RumbleException
    {
        HttpPost httppost;
        HttpResponse httpresponse;
        int i;
        int j;
        Log.d("RumbleConnector", (new StringBuilder()).append("URL: ").append(s).toString());
        Log.d("RumbleConnector", (new StringBuilder()).append("Data").append(s1).toString());
        BasicHttpParams basichttpparams = new BasicHttpParams();
        SchemeRegistry schemeregistry = new SchemeRegistry();
        schemeregistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        SSLSocketFactory sslsocketfactory = SSLSocketFactory.getSocketFactory();
        Scheme scheme = new Scheme("https", sslsocketfactory, 443);
        schemeregistry.register(scheme);
        DefaultHttpClient defaulthttpclient = new DefaultHttpClient(new ThreadSafeClientConnManager(basichttpparams, schemeregistry), basichttpparams);
        httppost = new HttpPost(s);
        httppost.addHeader("Content-Type", "application/json");
        httppost.addHeader("User-Agent", (new StringBuilder()).append("Android/Rumble/").append(RumbleDataSource.getInstance().getCurrentVersion()).toString());
        HttpResponse httpresponse1;

In pratica mettono nei log non solo la url ma anche i dati che inviano. Non potevo credere ai miei occhi alzi la mano chi non pensa che sia una cosa stupida! (fucilate a vista chi alza la mano per favore :D). Utilizzano questa post quasi per tutto, poi esiste una post2 che non e' altro che un copia e incolla della post cancellando le righe di log, la utilizzano per inviare il punteggio.

A questo punto mi collego via ssh al cellulare, faccio su per ottenere i permessi di root e lancio logcat ridirezionando l'output su file, apro ruzzle e inizio a fare le operazioni tipiche.

Velocizzando il racconto, inizio a capire il tutto come funziona e scrivo un programma in python, facendo l'errore di usare twisted (non e' che non mi piace twisted, anzi, ma per quello che dovevo fare non serviva avere qualcosa di asincrono). Roba noisa e ripetiva, bisogna inviare roba codificata in json verso un server http e leggere il risultato in json. Roba molto noiosa di cui non mi va di parlare.

Comunque inviato il login mi accorgo di una cosa che farebbe inorridire anche chi ancora non lo ha fatto, In pratica dopo il login ti viene inviato un numero di sessione che puoi usare all'infinito ed e' sempre uguale, anche se cambi la password!!! (Perche' fare cose del genere? chiedetelo a quelli di ruzzle che io non ho nessuna voglia di chiedere). In pratica se qualcuno si logga su ruzzle utilizzanod il mio cellulare, mi ritrovo tra i log il numero di sessione che potro usare all'infinito per giocare con l'account dell'amico, molto comodo. Piu' vado avanti e piu' mi sembrano dei principianti.

Sorvoliamo questo fatto ed andiamo avanti, invio una richiesta di elenco di partite e noto un altra cosa da principianti, in pratica mi arriva il punteggio che ha fatto l'avversario anche dei round che ancora non ho inviato. PERCHE???!!!! ALLORA VOLETE AIUTARE CHI BARA???

La cosa preoccupante e' che non mi arriva in nessun modo la scacchiera, ma tra i dati che mi arrino per ogni round noto 3 valori che mi incuriosiscono:

 'rounds': [{'gameId': '9108341182362867352', 
'player2Moves': '204540459E3049A3049530563112212341267341267A3126A512659E4126504126BE4126953126331273512736A51273651154156733156A5156A9E5156A724156BE315634156326156327A21593159A3159E2150415840', 
'player2Done': 'true', 
'seed1': '800038988', 'seed2': '830013712', 'seed3': '129958401', 
'player1Moves': '21233621536210110372652623362732A623A6233A6733156A49A67349A6723BA624BA6253A6153A6105459A725459A733459A36A9546A9503159A5159A633736A5736A952BEA3ED95326953156335673356723367A33695415840495840', 
'player2Score': '357', 'persistedRound': 'false', 'round': '0', 'player1Score': '335', 'player1Done': 'true'}, 
{'gameId': '9108341182362867352', 'player2Moves': '10120154015AB4015AE401523401526601526AB10450459EB50459634045AB4045AE5045AEB5045A6350452763048C5048CDE7048CDEA5304856048527B604852762049305AE405AEB405A631122123212631265212541254041258C11531540315AB315AE415AEB415A633152641527B4152733104521453148C4148DE', 'player2Done': 'true', 'seed1': '1370501326', 'seed2': '995886043', 'seed3': '906124985', 'player1Moves': '4326AB376AB476ABE376AE2ABE49548C4958404DC8414DC8454DC840396233967B3962159625405962541396AB396AE3DEA63DEAB2B733B7233B7213B72533721252635263552637B5526BAE345A6345AB445273595237649527649527B4952734AB7234AB7254FB7234FB7252376532104543258C09', 
'player2Score': '665', 'persistedRound': 'false', 'round': '1', 'player1Score': '696', 'player1Done': 'true'}, {'gameId': '9108341182362867352', 'player2Score': '0', 'player2Done': 'false',
 'seed1': '362894109', 'seed2': '7957389', 'seed3': '578740907', 
'player1Moves': '20123562125632B7326732DA6',
 'persistedRound': 'false', 'round': '2', 'player1Score': '30000', 'player1Done': 'true'}],


cio' che mi incuriosisce e':

 'seed1': '1370501326', 'seed2': '995886043', 'seed3': '906124985',

Torno al codice, e vado nel file model/Board.java e noto che come immaginavo la soluzione sta in questi valori. seed1 viene inviata ad unaa funzion srand dentro la libreria c loro, e poi richiamando la random dentro la stessa funzione escono dei numeri random che servono a generare la scacchiera. seed2 invece e' il seme che serve per calcolare i bonus. Di seed3 invece non vedo traccia nel codice, sembra essere alquanto inutile, della serie noi lo inviamo se poi ci viene un idea lo usiamo :D.

Il problema e' che non riesco ad usare questa libreria sul pc, ma solo su android, allora inizio a guardare i numeri generati con i vari seed e li confronto con la rand di python e niente. Poi ci penso su e provo a generare numeri in C e con mia immenso stupore vedo che i numeri generati usando lo stesso seme sono gli stessi!! Quindi passo a riscrivere la funzione in python che estrae la scacchiera:

import ctypes
LIBC = ctypes.cdll.LoadLibrary("libc.so.6")
letterChar = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
letterCount = [114, 18, 50, 20, 105, 14, 24, 8, 124, 0,
                0, 36, 36, 60, 82, 23, 1, 85, 63, 71,
                18, 19, 0, 1, 0, 17]
letterPoints = [
                1, 5, 2, 5, 1, 5, 8, 8, 1, 10,
                10, 3, 3, 3, 1, 5, 10, 2, 2, 2,
                3, 5, 10, 10, 10, 8
                ]
numChar = 26

bonusDist = [[1, 1, 0, 0, 14],
             [2, 1, 1, 0, 12],
             [1, 2, 2, 1, 10]]

bonusTiles =['D', 'T', 'V', 'W', ' ']

tiles = ""
points = []
bonus = ""

def generateLettersUsingSeed(seed1, seed2, round):
        global tiles
        global points
        global bonus
        a = []
        p = []
        tiles = ""
        points = []
        bonus = ""

        for i in xrange(numChar):
                for j in xrange(letterCount[i]):
                        a.append(letterChar[i])
                        p.append(letterPoints[i])
        seed1 = int(seed1)
        seed2 = int(seed2)
        round = int(round)
        LIBC.srand(seed1)
        for i in xrange(16):
                k = LIBC.rand()
                k %= len(a)
                tiles += a.pop(k)
                points.append(p.pop(k))
        a = []
        for i in xrange(5):
                for j in xrange(bonusDist[round][i]):
                        a.append(bonusTiles[i])
        LIBC.srand(seed2)
        for i in xrange(16):
                k = LIBC.rand()
                k %= len(a)
                bonus += a.pop(k)

In pratica e' un modo per estrarre delle lettere a caso dando un peso ad ogni lettera. Non mi va di commentarlo, ho solo ricopiato quello che fa ruzzle in java. Niente di particolare.

Un altra cosa interessante e' player1Moves una stringa che contiene le mosse fatte. E' molto semplice ogni carattere e' un numero in esadecimale, il primo indica la lunghezza della parola parola successiva (considerando 0 come parola da 1 carattere e cosi' via) e le cifre successive indicano la tessera (0 la prima in alto a sinistra e f l'ultima in basso a destra) corrispondente. Quando ho scritto questa parte all'inizio ho sbagliato qualcosa, all'inizo avevo dimenticato di agigunre la dimensione all'inizo di ogni parola, e con ormai non piu' immenso stupore vedo che il punteggio mi viene accreditato lo stesso e se provi a vedere le parole che ho inviato non fa altro che far crashare l'applicazione (chissa' magari si potrebbe sfruttare in modo da eseguire codice remoto, o forse no non ho indagato). Ecco altre prove che chi ha scritto cio' non e' un bravo programmatore.

Capito cio' l'unico ostacolo che mi impediva di inviare un punteggio era la post2, la funzione che non loggava l'invio del punteggio. Be mi armo di pazienza e torno al codice in java. Capisco cio' che devo inviare e come, ma c'e' un ostacolo di cui non vi ho ancora parlato. ogni richiesta contiene un codice misterioso chiamato ticket:

"ticket":"257adf8958630692d2d6be12fa73586bb9dc4b03"

Torno al codice java e purtroppo viene generato dalla libreria C non dal codice java. Fortunatamente per me la cattiva programmazione di ruzzle mi aiuta ancora, il ticket non deve per forza cambiare da una richiesta all'altra, basta inviare sempre lo stesso timestamp e il ticket e' sempre uguale (viene generato usando il timestamp inviato, la sessione e il tipo della richiesta). Quindi invece di continuare ad indagare su come viene generato (forse lo faro' in futuro ma non vedevo l'ora di finire la prima versione) ho deciso di farmi generare dalla libreria di ruzzle un ticket valido. Quindi ho scritot un applicazione android che utilizza la libreria C e genera i ticket validi. Generato un ticket lo incollo nel codice ed inizio ad inviare punteggi.

Non mi soffermo sull'algoritmo per risolvere la scacchiera visto che navigando in rete si trova tanta gente che spiega come fare. La soluzione non e' calcolare tutti i percorsi possibile, ma organizzare il dizionario in un albero, ed esplorare solo i percorsi finche' c'e' un ramo percorribile sull'albero. Rileggendomi non capisco nemmeno io che voglio dire, in pratica inutile calcolare tutti i percorsi che iniziano con "CC" se non ci sono parole nel dizionario che iniziano con CC.

A questo punto decido di rendere il programma piu' autonomo possibile. Il risultato finale e' un'applicazione che richiede nuove partite quando il numero di partite in corso scende sotto il limite impostato. Per ogni round aspetta la mossa dell'avversario, e quando l'avversario ha finito di giocare legge il risultato dell'avversario e inviare un risultato simile all'avversario.

Per far insospettire meno possibile l'avversario ci sono scelte casuali su che round vincere e di quanto vincere, senza mai esagerare. Il fatto che invio un risultato simile a quello dell'avversario spinge l'avversario a chiedere la rivincita, e cosi' potevo testare l'affidabilita' del programma nel sembrare un giocatore pulito. Inoltre per evitare che ad un punteggio troppo basso dell'avversario corrispondesse un risultato basso del mio bot, ho aggiunto una soglia minima di punteggio per ogni round. Cosa che ha tolto i a qualcuno che ha fatto di queste prove.

Dopo una settimana dal momento in cui ho avviato il mio programma (a parte qualche bug causato da qualche modifica affrettata o da un copia incolla errato) ha inziato a vincere una partita dopo l'altra. In una settimana il mio programma ha giocato quasi 2000 partite e solo pochissime persone (a parte i bari) hanno sospettato qualcosa, e spesso solo dopo 10 partite giocate. Il risultato di una settimana di gioco quasi ininterrotto e' questo:

/raw-attachment/wiki/Ruzzle-cheater/ruzzle.png

Con un solo account sono arrivato in classifica. Oggi chattando con qualche baro ho scoperto che chi e' in classifica per non rischiare di perdere punti non gioca con i propri account ma si genera una decina di account che accumulano punteggio e che poi fanno giocare con il proprio account in classifica.

Conclusioni

La domanda tipica che viene fatta in questi casi e': "Cosa ci provi a vincere facile barando?" Beh e' una domanda che mi chiedo anche io nel caso dei lamer che utilizzano applicazioni scritte da altre persone senza nessuno sforzo. Nel mio caso il bello non e' vincere barando ma scoprire come funziona un applicazione, e il cercare soluzioni e nel programmare. Il far giocare le partite e' solo un modo per vedere che la propria applicazione funziona veramente.

Beh le conclusioni di questa avventura sono deludenti nel vedere come un applicazione scritta male possa fare molti soldi cosi'. Certo l'idea di ruzzle potra' anche essere anche innovativa e divertente, ma vedendo l'implementazione e vedendo come e' semplice barare fa passare ogni voglia di giocarci. Finalmente dopo tanto duro lavoro mi e' passata la mania di ruzzle. Non capisco come ha fatto a diventare cosi' utilizzata quando e' piena di bari e non c'e' nessuno sforzo da parte degli sviluppatori di dare vita difficile a chi bara.

Se hai giocato contro di me (mancausoft su ruzzle) ed hai perso, mi dispiace averti fatto giocare contro un baro, ma questo e' il mio divertimento, non quello di trovare le parole, ma idagare su come funzionano le cose. Prendere il giocattolo smontarlo, capire come funziona e rimontarlo. Mi diverto cosi'.

Per ogni insulto/consiglio/dubbio potete contattarmi via email, la mail e' scritta nella home del sito.

Update

Che dire... Conversando un po' con altri giocatori ho scoperto che c'era un bug che ti permette di inviare il punteggio dell'avversario usando un terzo giocatore... Ma nessuno sapeva i dettagli, lo utilizzano staccando la rete, entrando come un altro utente, attivare la rete e avendo ancora in cache le partite dell'altro utente e giocando alcune partite il risultato e' che invii il punteggio come avversario.

Allora ho deciso di provare a ricreare questa situazione nel mio programma. Ho provato a inviare il punteggio delle mie partite invece che come utente mancausoft con un altro utente registrato ad-hoc. Il risultato era che in alcuni casi inviavo il mio punteggio, in altri casi il punteggio dell'avversario. Per capire il motivo bisogna vedere come e' strutturato ruzzle. Per ogni partita c'e' un player1 e un player2. Se si usa un utente che non e' ne' il player1 ne' il player2, si invia sempre il punteggio del player2.

Questo fa pensare che dopo l'autenticazion del player, ad un codice del genere sul server:

if (playerId == player1Id)
{
   //Codice per settare il punteggio del player1
}
else
{
   //Codice per settare il punteggio del player2
}

Ecco un altro motivo per cui ruzzle e' scritto male :).

/raw-attachment/wiki/Ruzzle-cheater/ruzzle1.png /raw-attachment/wiki/Ruzzle-cheater/ruzzle2.png

Attachments