Reversing, come iniziare
Ok, intanto una breve intro. Il mio scopo è di fornire un aiuto a chi vorrebbe iniziare a giokare kol debugger, principalmente per i wannabecrackers che non sanno dove sbattere la testa. Se infatti è vero che la Rete offre una vagonata di testi e tutorialz, questi sono in inglese e soprattutto prevedono ke ki li legge sia già kapace di fare kuello ke viene spiegato (un po' kome le banke ke per darti dei soldi pretendono ke tu li abbia già...).
Cerkerò anke di dare qualke dritta sui toolz e su kome usarli, sempre partendo dal presupposto di skrivere per persone ke di crackin' non sanno (ankora;) nulla. Io stesso non sono un guru del Reverse Engeneering, sono anni ke non metto mano al debugger, pratikamente da kuando trovo kuello ke mi serve già pronto sulla Rete, e sono sikuramente più agile usando il mio vekkio DEBUG.COM ke i più recenti SoftICE, IDA & C.
Bene, that's all, si komincia.
Innanzitutto crackare un programma (io ho il vizio di dire 'sproteggere') vuol dire modificarne il comportamento al fine di ottenerne un utile, ovvero per esempio evitare ke ci kieda kodici di registrazione, eliminare i fastidiosi NAG screen (quelle skermate ke ci fanno il pistolotto affinkè si metta mano al portafogli), evitare di dover kopiare tutto un CD pieno zeppo di minkiate filmati orripilanti ke okkupano inutilmente centianaia di Mb un tempo preziosi e ora inkredibilmente sprekati); insomma, crakkare! :)
Sfortunatamente non esiste *IL* metodo per crakkare, esistono solo alcune linee-guida generali. Il vero cracker deve trovare sempre la soluzione migliore kaso per kaso, a volte mi è bastato cerkare un po' nei files di certi gioki per trovare qua e là le password e mettere sempre la stessa ovunkue (parlo di kuei gioki per DOS di svarati anni fa ke kiedevano la tal parola in tale pagina o amenità simili, vedi MechWarrior, Search for the King e moltissimi altri, era una protezione molto in voga insieme ai diski kiave).
Il metodo più ovvio comunque rimane quello di mettere mano al codice eseguibile del programma, e a questo punto occorre necessariamente fare un passo indietro. Creare un programma vuole dire creare un codice sorgente in un linguaggio ad 'alto' livello (C/C++, Pascal, Basic (blearp)) e poi passarlo ad un compilatore che crea il codice oggetto, e ankora passare il codice oggetto ad un linker per creare l'eseguibile finale, che ora come ora possiamo immaginare essere una sequenza di istruzioni elementari in linguaggio macchina. Il problema nasce dal fatto che mentre il sorgente è abbastanza chiaro, perlomeno per i nostri scopi (vi sfido a kapire kualkosa o trovare *UN* kommento nei miei programmi ...), una volta kompilato e ottenuto l'eseguibile finale (.EXE, .COM o .DLL per WinZozzo) si perde pratikamente ogni informazione di massima sul programma, ke deve essere kuindi ispezionato kon un debugger per tracciarne l'esekuzione e rikostruirne il funzionamento. Diciamo ke la creazione di un file eseguibile è un procedimento one-way, ovvero che non esiste un metodo per ricostruire esattamente il sorgente originale partendo dal suo compilato, sempre appunto a kausa della perdita di informazioni (nomi di variabili, tipi di routines etc. etc.).
Ho fatto kuesta premessa perkè vorrei far kapire ke la diffikoltà più grossa per un cracker è kuella di farsi un' idea del funzionamento del programma, di kome è strutturato e di dove andare a cerkare la parte di kodice su kui intervenire. Veniamo al sodo ora.
Eravamo rimasti al linguaggio makkina, urge spiegazione ;) Il linguaggio makkina è un codice ke è direttamente eseguibile dal processore, senza ulteriori trasformazioni. Il metodo per leggere il linguaggio macchina è di utilizzare un debugger, un altro programma che ci permette di visualizzare in un codice mnemonico più 'umano' le operazioni che il processore andrà a kompiere ed eventualmente tracciarne passo passo l'esekuzione. Si tratta ovviamente di operazioni elementari come somme, shiftamenti, salti & kompagnia bella, visualizzate kon diciture kriptike come SUB AL,BL (Subtract BL from AL), JNZ XYZK (Jump if NotZero to XYZK) e altre amenità.
Purtroppo non ho il tempo di fare un korso di l.m. , probabilmente nemmeno le konoscenze, per questo konsiglio kaldamente a tutti di imparare un minimo di linguaggio makkina prima di tentare qualsiasi approccio; Forse riuscirete ad arrivare lo stesso alla fine del tutorial, ma di fronte ad un programma vero probabilemte vi verrà la voglia di lasciar perdere tutto e fankulo il crackin'.
Nella mia 'karriera' di cracker ho imparato ke le istruzioni più interessanti sono davvero poke, non okkorre essere dei maniaci dell'assembler e questo di certo facilita le cose. Una delle istruzioni più incontrate in percentuale all'interno di un programma è di sicuro quella di kiamata di procedura, una sorta di GOSUB ke in realtà è kodifikata come CALL. La sua sintassi è CALL [indirizzo]. Vediamo un esempio stupido per fissare le idee. Immaginiamo per un momento di essere nel bel mezzo dell'esekuzione di un programma DOS, con una rappresentazione del kodice del tipo segmento:offset (non kambia kuasi nulla sotto Win, l'importante è il koncetto) :
SEGM:OFFS CODICE MNEMONICO
1F47:0100 BB1000 MOV BX,0010
1F47:0103 B82000 MOV AX,0020
1F47:0106 E8F71E CALL 2000
1F47:0109 3D0000 CMP AX,0000
1F47:010C 742B JZ 0139
. . .
. . .
Allora, kuesto esempio davvero banale serve ad illustrare alkune istruzioni: La prima e la sekonda istruzione karicano il valore 0x0010 esadecimale nel registro BX e 0x0020 esadecimale nel registro AX; i registri del processore sono una sorta di 'variabili' interne utilizzabili più o meno liberamente per eseguire dei kalkoli, delle assegnazioni o per tenere l'indirizzo di posizioni partikolari di memoria, e sono ovviamente più di 2. La terza istruzione è una kiamata di procedura, kome dicevo una sorta di GOSUB e RETURN del BASIC. Kuando il processore inkontra una CALL, per prima kosa salva nello stack il valore korrente dell' offset all'interno del segmento, valore ke si trova nel registro IP (EIP nella modalità 386 estesa), in modo da potervi tornare una volta terminata la procedura, dopodikè salta alla posizione 1F47:2000 dove presumibilmente è kontenuto il kodice della procedura. Vediamolo :
SEGM:OFFS CODICE MNEMONICO
1F47:2000 51 PUSH CX
1F47:2001 52 PUSH DX
. . .
[ kodice della routine ]
. . .
1F47:24AE 5A POP DX
1F47:24AF 59 POP CX
1F47:24B0 C3 RET
Le prime due istruzioni salvano il valore dei registri CX e DX nello stack, una zona di memoria riservata individuata dai registri SS:SP (Stack Segment:Stack Pointer) ke funziona kome una pila di oggetti, nella kuale il primo oggetto estratto è kuello ke è stato inserito per ultimo. Per kuesto le due istruzioni POP finali hanno l'ordine invertito rispetto alle PUSH.
L'istruzione RET ha il kompito (normalmente) di far tornare il programma all'istruzione immediatamente successiva a kuella della kiamata, nel nostro kaso a 1F47:0109 CMP AX,0000 , in pratika facendo una sorta di 'POP IP'. Immaginiamo ora ke la procedura kompresa tra 1F47:2000 e 1F47:24B0 esegua svariati kompiti, ke kontenga ank'essa a sua volta delle kiamate, tra kui magari la verifika della registrazione del kodice del programma stesso, e ke subito prima di ritornare a 1F47:0109 imposti il registro AX a 0 o a 1 , a seconda ke il programma sia registrato o meno. In kuesto kaso non ci importa un kakkio di KOME tale verifika venga fatta, a noi importa sapere ke il proseguimento del programma è influenzato (per kuel ke riguarda la registrazione dello stesso) dal valore assunto da AX.
l' istruzione 1F47:0109 CMP AX,0000 konfronta il valore (CoMPare) di AX kon 0, e viene settato un flag sempre del processore (il flag Zero o ZF; in realtà anke il Carry Flag, CF, se i 2 operandi sono Unsigned, ma a noi non ce ne frega una cippa :);
Se i due operandi, qui AX e 0 kombaciano, ovvero se AX=0 kuesto benedetto flag ZF viene posto a 1, tutto qui; l'istruzione successiva, 1F47:010C JZ 0139 (Jump if Zero to 0139) è un salto kondizionato dal valore del flag ZF : se ZF vale 0 il programma kontinua il suo flusso regolarmente, mentre se ZF vale 1 il processore 'salta' alla lokazione 1F47:0139. A kuesto punto è kiaro ke tra 1F47:010C e 1F47:0139 si troverà la parte di kodice ke visualizza il NAG screen, magari una procedura di registrazione o kualke altra rottura di palle, e ke a 1F47:0139 invece inizia il programma vero e proprio. Inutile dire ke in questo kaso banale e fin troppo inventato basta mettere un salto incondizionato nella locazione di memoria 1F47:010C ovvero un bel JMP 0139 (JuMP to 0139) ed ottenere :
SEGM:OFFS CODICE MNEMONICO
1F47:0100 BB1000 MOV BX,0010
1F47:0103 B82000 MOV AX,0020
1F47:0106 E8F71E CALL 2000
1F47:0109 3D0000 CMP AX,0000
1F47:010C EB2B JMP 0139 < ---- MODIFIKATO
. . .
. . .
L'ultima kosa per questa introduzione è il fatto ke nel modifikare le istruzioni del programma bisogna assulutamente rispettare gli 'spazi' a disposizione ! Kuesto è MOLTO importante, per non inkasinare il kodice del programma. Se io avessi voluto ad esempio mettere l'istruzione di salto inkondizionato subito dopo la kiamata, ovvero al posto del CMP AX,0000 (oramai assolutamente inutile), poteva succedere una kosa del tipo :
SEGM:OFFS CODICE MNEMONICO
1F47:0100 BB1000 MOV BX,0010
1F47:0103 B82000 MOV AX,0020
1F47:0106 E8F71E CALL 2000
1F47:0109 EB2E JMP 0139 < ---- MODIFIKATO
1F47:010B 00742B ADD [SI+2B],DH < ---- SPUTTANATO
. . .
. . .
Ke kazz è successo? Semplice, l'istruzione CMP AX,0000 è 'lunga' 3 byte, mentre JMP 0139 solo 2 byte, e kuindi il terzo byte '00' è stato inkorporato nell'istruzione successiva, ke il processore ha interpretato kome ADD [SI+2B],DH, una kosa in kuesto kaso partikolare irrilevante perkè non verrà mai eseguita, ma decisamente brutta e potenzialmente distruttiva in altre situazioni (kompito a kasa : tradurre ADD [SI+2B],DH ;-). Ma se sono testardo e voglio a tutti i kosti mettere il mio JMP nella lokazione 1F47:0109 ?!?! Ekkeppalle , ok ok... C'è una istruzione dei processori 80X86 ke serve a fare nulla, si kiama NOP e corrisponde all'esadecimale 0x90 (kuale cracker non lo sa?!?!), lunga un byte e kon la kuale 'aggiustare' il kodice zoppikante. L'ultima versione potrebbe essere kuella ottenuta mettendo all ' indirizzo 1F47:010B l'istruzione NOP ottenendo :
SEGM:OFFS CODICE MNEMONICO
1F47:0100 BB1000 MOV BX,0010
1F47:0103 B82000 MOV AX,0020
1F47:0106 E8F71E CALL 2000
1F47:0109 EB2E JMP 0139 < ---- MODIFIKATO
1F47:010B 90 NOP < ---- AGGIUNTO
1F47:010c 742B JZ 0139 < ---- RIPRISTINATO (ma non viene mai eseguito)
. . .
. . .
Ovviamente si potrebbe NOPpare anke il JZ 0139, ma perkè farlo ? :-) Kosì va ugualmente, almeno per kuesto esempietto. Al posto dei NOP, se sono tanti e si sospetta ke il programma in kualke maniera possa kontrollare se il proprio kodice è stato alterato kontando appunto le okkorrenze di NOP consekutivi si possono aggiungere istruzioni 'inutili' kome una sequenza pari di DEC AX e INC AX o tutto kuello ke puo' venirvi in mente ke non alteri il kodice; Per favore non fate kritike sulla banalità dell'esempio, era voluta e so benissimo ke in kasi reali le kose non vanno kosì (perlomeno non più; Terminator 2 era un giokino skifoso almeno kuanto la sua protezione, addirittura era tutto kontenuto in una CALL, è bastato NOPparla e addio skermata di protezione ;-).
La domanda ke mi aspetterei è : "Ma kome kazzo fai a sapere ke la CALL 2000 esegue le verifike e ke in AX c'è il risultato?!?!?" La risposta ci riporta a kuello ke dissi nella intro : è tutta questione di kapire kome funziona il programma, non ci sono skemi fissi. Si prova, si inventa, si tenta, si spera in una botta di kulo (serve SEMPRE) e prima o poi i programmi cedono. A volte mi son bastati 10 minuti (vedi Terminator 2) altre volte è stata questione di svariate ore totali (il Conseal 1.04 ad esempio, ma li' è kolpa dell'inesperienza kon le Win32 :), altre volte ho rinunciato (si, il tempo è tiranno...).
Se volete provare ad assemblare l'esempio ke ho riportato, da WIn9X aprite una finestrella DOS e scrivete 'debug'; vi apparirà un trattino ed il prompt, voi skrivere A e date invio, dovreste ottenere una kosa del tipo 'XYZK:0100 _ ' : ora potete digitare le istruzioni (MOV BX,0 etc etc ), e kuando avete finito date CONTROL+C; date U 100 e invio, se avete fatto tutto bene dovreste trovare l'esempio, segmento a parte (kuello kambia).
Happy Crackin' a tutti, e andate a rakkattare un buon libro sul linguaggio makkina degli 80x86. Alla prossima lezione.