|
Winzip Encryption Engine Reversing | ||
|
Data |
by "Winebag" |
|
|
04/03/2003 |
Published by Quequero | |
|
E' meglio una donna a 360 gradi ... |
Bravissimo davvero, il tutorial e' estremamente dettagliato, ottimo lavoro... Ma hai guardato il problema dal punto di vista sbagliato... O meglio, diciamo che hai soltanto studiato l'algoritmo e anzi, hai trovato anche il modo di fare un attacco ridotto (complimenti, ci hanno messo svariati anni per mettere a punto quel tipo di attacco) ma non hai fatto nessuna prova per vedere se effettivamente esisteva o meno il buco di cui parlavo... Vediamo le soluzioni successive, se ce ne saranno, cosi potrai farti magari un'idea piu' approfondita, nel frattempo complimenti! |
...o sono meglio quattro a 90? |
|
.... |
|
.... |
|
Difficoltà |
( )NewBies (x)Intermedio ( )Avanzato ( )Master |
|
Oggi non dovremo registrare nessun programmillo, perché il buon Quequero ci propone di reveresare un algoritmo di crittografia, per tentare di crittanalizzarlo.
|
Introduzione |
|
Tools usati |
|
URL o FTP del programma |
http://download.winzip.com/winzip81.exe
|
Notizie sul programma |
|
Essay |
sub1F10 proc near
push ebp
mov ebp, esp
push esi
mov esi, [ebp+arg_0]<---qui c'è l'indirizzo della password
mov dw1, 12345678h<--inizializza 3 dw che impareremo
a conoscere molto bene:-) Le specificheee ;ppp
NdQue
mov dw2, 23456789h
mov dw3, 34567890h
mov al, [esi]
test al, al
jz short loc1
loop:
and eax, 0FFh
push eax
call sub1EA0--F1<---funzione molto importante!
mov al, [esi+1]<--mette in al i caratteri
add esp, 4
inc esi
test al, al<--fino alla fine della stringa
jnz loop
loc1:
pop esi
pop ebp
retn
sub1F10 endp
Questa è la funzione che viene chiamata all'interno del ciclo, con argomento ogni carattere della password:
sub1EA0--F1 proc near
push ebp
mov ebp, esp
mov edx, dw1
mov eax, [ebp+arg_0]
mov ecx, edx
push esi
xor ecx, eax
and ecx, 0FFh
shr edx, 8
mov ecx, Sbox[ecx*4]
xor ecx, edx
mov edx, dw2
mov dw1, ecx
and ecx, 0FFh
add ecx, edx
mov edx, dw3
imul ecx, 8088405h
inc ecx
mov esi, edx
mov dw2, ecx
and esi, 0FFh
shr ecx, 18h
xor ecx, esi
pop esi
shr edx, 8
mov ecx, Sbox[ecx*4]
xor ecx, edx
mov dw3, ecx
pop ebp
retn
sub1EA0--F1 endp
Come potete vedere, la funzione riceve un parametro di 1byte e sostituisce i valori delle 3 dw, utilizzando i valori delle dw generate dal ciclo precedente e il parametro. Per fare questo si serve tra le altre cose di una S-box. Questo è un procedimento molto usato in crittografia: la S-box (sostitution box) altro non è che un array, che ad un certo valore dell'indice fa corrispondere un valore. In questo caso,l'array è un array monodimensionale di 256 dword. In particolare i valori in essa, sono quelli utilizzati negli algoritmi di CRC. La S-box in pratica è una funzione ed ha fondamentalmente 2 pregi:può essere usata per espandere i dati (come in questo caso da un indice di 8 bit,si ottiene un valore a 32) e inoltre cambia totalmente i valori e per questo non lascia punti di riferimento al crittanalista Non e' totalmente vero in realta', se l'indice e' fisso basta rigirarlo :) NdQue. Ma analizzeremo dopo più attentamente la funzione,ora ritorniamo all'assembler. Seguiamo con SIce la funzione 1FE0 e vediamo dove ci porta, troviamo questa funzione di cui ho tagliato alcune parti:
...<---prima vengono chiamate funzioni per generare
numeri casuali
loop_rand:
mov eax, ptr<--ptr punta ad un'array di dword riempito con
valori random
mov eax, [eax]
cmp eax, edi
jl loc1
add ptr, 4
jmp loc2
loc1:
call fill_ptr<--questa riempie l'array
loc2:
sar eax, 16h
mov [ebp+esi+var_C],al<--mette il byte alto in un'array di
byte sulla memoria locale
inc esi
cmp esi, 0Ah<--ripete 10 volte
jl loop_rand
...
mov ecx, [ebp+8]<--la password
push ecx
call sub1F10<---qui chiama la funzione vista prima
add esp, 4
loop2:
call sub1E80--F2<--questa ce la vediamo dopo
mov bl, [ebp+edi+var_C]
mov [ebp+arg_8], eax<--salva l'output della funzione F2
mov edx, ebx
and edx, 0FFh
push edx<---il parametro è uno dei byte random inseriti
sopra nell'array locale
call sub1EA0--F1<--funzione vista prima
mov cl, byte ptr [ebp+arg_8]
push 1<--1 char
xor bl, cl<---xora l'output con il byte random
push ebx
push esi
call sub1D100--pseudomemcpy<-per mezzo di diverse call
arriva ad una routine copia stringhe
add esp, 10h
test eax, eax
jz wrong
inc edi
cmp edi, 0Ah<--ripete per tutti e 10 i byte random
jl loop2
call sub1E80--F2
mov edi, [ebp+arg_4]<--parametro molto interessante
mov [ebp+arg_8], eax
mov ebx, edi
shr ebx, 10h
mov eax, ebx
and eax, 0FFh<---prende il 2°byte a partire dall'alto
push eax<--lo parametrizza in F1
call sub1EA0--F1
mov cl, byte ptr [ebp+arg_8]<--output di F2
push 1
xor bl, cl<--come sopra
push ebx
push esi
call sub1D100--pseudomemcpy
add esp, 10h
test eax, eax
jnz short OK
...
OK:
call sub1E80--F2
mov ebx, edi
mov [ebp+arg_8], eax
shr ebx, 18h<--byte più alto del parametro
push ebx
call sub1EA0--F1
mov cl, byte ptr [ebp+arg_8]
push 1
xor bl, cl<--tutto come prima
push ebx
push esi
call sub1D100--pseudomemcpy
add esp, 10h
...
Questo algoritmo utilizza un byte random come input per F1,dopodiché
lo xora con l'output di F2 e lo copia in una stringa, ripete il tutto per 10byte
e successivamente per i 2 byte più alti del parametro di questa funzione. Questo
parametro è il CRC,cioè una sorta di chiave hash del file non compresso. Questo
valore si trova nel file zip,dopo i dati compressi, e ovviamente non dipende
dal fatto che vi sia stata crittazione(vi ricordate prima quando abbiamo visto
le differenze tra z e zc, non veniva menzionato questo valore). L'algoritmo
che lo genera si trova nella funzione all'indirizzo 200246B0. Se volete dargli
un occhiata, vi accorgerete che esso usa i valori di Sbox esattamente come la
funzione F1,che quindi utilizza due volte la funzione CRC. Quindi capiamo che
Sbox è in effetti una tabella CRC Bravo davvero! NdQue.
Per quanto riguarda le 3 dw,esse
vengono modificate ancora da F1 che come abbiamo visto agisce per 12 volte, che
si aggiungono a quelle della password,in pratica è come se la password fosse
12byte più lunga.Chiaramente in fase di decrypt dovrà essere possibile
risalire a questi 12 byte:abbiamo detto che dopo essere stati xorati i
byte vengono salvati in una stringa,che sarà anteposta al messaggio criptato
nel file, spiegato finalmente cos'erano quei 12byte! Per completare l'analisi
diamo uno sguardo a F2:
sub1E80--F2 proc near
mov ecx, dw3<--usa dw3 ma non la modifica
or ecx, 2
mov eax, ecx
xor eax, 1
imul eax, ecx<---moltiplica
and eax, 0FFFFh
shr eax, 8<--restituisce il 3°byte a partire dall'alto del prodotto
retn
sub1E80--F2 endp
Questa funzione non altera i valori delle dw,non ha parametri in ingresso,
restituisce un valore di 8bit,l'unico input proviene da dw3,quindi dall'ultimo
passaggio di F1.Poichè F1 ,al contrario, ha un parametro e il valore restituito
non viene mai considerato, e in questo punto F1 e F2 si ripetono sempre
alternate, possiamo considerare F1 ed F2 come parti di una stessa funzione. Attenzione
però, prima F1 e poi F2, anche se nel ciclo compare prima F2, quindi il valore
restituito da F2 all'inizio del ciclo deriva dall'input del ciclo
precedente,ovvero dal byte random prima, e per il primo, esso è l'ultimo della
pass. Questo valore sarà usato per xorare quello dopo.
In simboli c[n]=r[n]^F(r[n-1]) questo può essere considerato analogo
ad un algoritmo di crittografia a blocchi(di un byte) in modalità CFB,dove cioè
prima si critta il blocco n-1 e poi lo si xora con il blocco n ottenendo il
blocco cifrato c[n], che va a costituire la stringa di 12byte.
Notate che fino ad adesso non abbiamo crittato un cazzo, ma abbiamo solo
preparato la strada producendo 3dw, che voglio sperare servano a qualcosa,
allora mettiamo un bpm di esse e premiamo F5. Arriviamo alla solita F1,risalendo
troviamo questa funzione:
sub2100 proc near
...
mov esi, [ebp+arg_0]<--puntatore al messaggio da
crittare(Plaintext)
imul eax, [ebp+lunghezza]
test eax, eax
jz short Loc1
push ebx
mov [ebp+arg_4], eax<--lunghezza
Loop1:
call sub1E80--F2
mov ebx, eax<--salva l'output di F(P[n-1])
xor eax, eax
mov al, [esi]<--prende P[n]
push eax
call sub1EA0--F1<--chiama F(P[n])
mov dl, [esi]
mov eax, [ebp+arg_4]
xor dl, bl<--già vista!
add esp, 4
mov [esi], dl<--sovrascrive P[n] con C[n]
inc esi
dec eax<--sempre la lunghezza
mov [ebp+arg_4], eax
jnz Loop1
pop ebx
loc1:
imul edi, [ebp+lunghezza]
mov ecx, [ebp+arg_0]<--puntatore al messaggio
crittato(C)
mov edx, [ebp+arg_C]
push 0
push edi
push ecx
push edx
call sub1D010<---un'altra funzione di copia stringhe
...
Come possiamo vedere l'algoritmo è lo stesso di prima, in modalità CFB(anzi
non proprio perché in quel caso da cosa ho capito si usa F(C[n]) e qui venendo
generato P[-1] a partire dalla pass,si usa F(P[n]),ma cambia poco) con la stessa
funzione. La particolarità di questa modalità è che la crittazione vera
e propria è fatta dallo xor, e non dalla funzione, che può quindi essere irreversibile
senza per questo pregiudicare la reversibilità di tutto l'algo. Come ad esempio
in questo caso, senza invertire F si può invertire l'algoritmo:conoscendo la
password genero le 3 dw uguali a quelle in fase di crittazione,uso l' ultimo
char in F e uso il valore ottenuto per xorare C[0] e ottengo P[0], che passerò
a F,per ottenere il valore per xorare C[1], e ripeterò questo procedimento
fino all'ultimo char. Per curiosità ho disassemblato anche la routine di decrypt,(trovata
con un bpm sulle 3dw) e ho ottenuto ciò che mi aspettavo Questa
e' stata un'ottima idea, complimenti NdQue.
sub2200decrypt proc near
push ebp
mov ebp, esp
sub esp, 0Ch
mov eax, dword_0_2007DDA8<--password
push esi
push eax
call sub1F10<---genera le 3 dw
mov ecx, [ebp+arg_0]<--indirizzo dei 12byte
add esp, 4
xor esi, esi
mov edx, [ecx]
mov [ebp+var_C], edx<--li ricopia sulla stack nel solito array R
mov eax, [ecx+4]
mov [ebp+var_8], eax
mov ecx, [ecx+8]
mov [ebp+var_4], ecx
loop1:
call sub1E80--F2<--F(R[n-1])
mov dl, byte ptr [ebp+esi+var_C]<--valore crittato dei byte random
xor dl, al<--decritta
mov al, dl
mov byte ptr [ebp+esi+var_C], dl<--lo sovrascrive decrittato
and eax, 0FFh
push eax<--chiama F(R[n])
call sub1EA0--F1
add esp, 4
inc esi
cmp esi, 0Ch<--per 12 byte
jl loop1
mov edx, off_0_2004606C
movzx ax, byte ptr [ebp+var_4+3]<--ultimo dei 12 byte
test byte ptr [edx+20h], 2
jz loc1
xor ecx, ecx
mov cl, byte_0_2009A937
cmp ax, cx<--controlla che decrittato sia uguale al valore
del CRC
jz OK
...
loc1:
mov edx, dword_0_2009A938+2
shr edx, 18h
cmp ax, dx
jz OK
...
OK:
...
loop2:
call sub1E80--F2<--F(P[n-1])
mov cl, [esi]<--C[n]
xor cl, al<--C[n]^^F(P[n-1])=P[n]
mov al, cl
mov [esi], cl<--copia P[n]
and eax, 0FFh
push eax<--chiama F(P[n])
call sub1EA0--F1
add esp, 4
inc esi
dec edi<--lunghezza
jnz short loop2
pop edi
loc2:
mov eax, 1<--Tutto OK
pop esi
mov esp, ebp
pop ebp
retn
decrypt endp
Da questa analisi vediamo ancora che prima di decrittare controlla che
l'ultimo dei 12 byte, decrittato, abbia il valore del CRC, altrimenti vuol dire
che è sbagliata la password.
L'approccio del reverser finisce qua; ora
proveremo quello più difficile:il crittanalista. Uno sguardo al cifrario
molto in generale l'abbiamo già dato, esso è una variante della modalità CFB,
usa la funzione F con il byte di plaintext e ottiene un valore per xorare il
byte successivo. Questa funzione, oltre al byte in ingresso, si serve di 3
chiavi,che abbiamo chiamato dw, e genera le 3 chiavi che saranno utilizzate al
ciclo seguente. Le chiavi per il primo ciclo sono generate con la password e i
12 byte random. Vediamo come lavora la funzione utilizzando una notazione in
pseudo-C; il disassemblato è sopra, diviso in F1 e F2,non penso di doverlo
ripetere qui, dato che per le considerazioni che dobbiamo fare, è meglio
scriverlo in forma più compatta, non formalizzatevi se c'è qualche errore di
forma; se trovate qualche errore di concetto scrivetemi.
char F(char a){
dw1=(dw1>>8)^Sbox[a^(dw1&0xFF)];
dw2=0x8088405*(dw2+(dw1&0xFF))+1;
dw3=Sbox[(dw2>>24)^(dw3&0xFF)]^(dw3>>8);
return (char)(((dw3|2)*((dw3|2)^1))&FFFF)>>8;}
Il nuovo valore di dw1 viene generato a partire dal suo valore precedente e dal byte in ingresso. La nuova dw2, utilizzando dw2 e il quarto byte di dw1; dw3,generata dal suo valore precedente e dal byte alto di dw2, servirà ad ottenere l'output. Queste poche righe basterebbero a spiegare l'algoritmo, ma Que ci ha chiesto di più: tentare un metodo d'attacco e cercare una falla. Io personalmente ho trovato questa parte ostica, data la mia poca familiarità con queste cose. L'attacco classico si basa sul brute-force della password e presenta chiaramente problemi maggiori all'aumentare della lunghezza della pass. Qui non vedo grosse falle,l'unica cosa che può tornarci utile è il fatto che conosciamo il valore di due byte, come abbiamo visto prima, gli ultimi due di quei 12 aggiunti. Conoscendo il valore di P[10] e di P[11], effettuando uno xor otteniamo P[10]^C[10]=F(P[9]), e allo stesso modo otteniamo F(P[10]). Il principio che sta alla base di questo attacco è che, per come funziona quest'algoritmo,senza conoscere la password conoscendo i valori in input di F, possiamo generare i successivi, dato che le dw sono ottenute direttamente, e il valore decrittato sarà il parametro successivo, ma con un bruteforce sono 13byte. Proviamo a percorrere a ritroso F:conosciamo F(P10) , esso è il terzo byte del prodotto tra (dw3|2) e ((dw3|2)^1), il penultimo bit di entrambi questi valori è settato a 1 dall'or, l'ultimo sarà 1 in uno e 0 nell'altro(a causa dello xor con 1), indipendentemente da dw3; quindi se un valore multiplo di 4 di dw3 da come risultato x, così faranno anche i 3 numeri seguenti. Trattandosi del 3°byte è come operare modulo 2^16(2 alla 16,NON 2 xor 16), quindi interesseranno solo gli ultimi 2 byte di dw3. Mediamente il 3°byte sarà uguale al valore desiderato una volta ogni 256, quindi provando 2^16 word(anche se come abbiamo visto ne proveremo 2^14), in media 256 daranno esito positivo. Provando circa 256 combinazioni per il primo valore e 256 per il secondo, ce ne sarà una in cui conosceremo il reale valore della word bassa di dw3 in 2 cicli successivi. Rappresentiamo con una tabella le 3 dword generate di cicli 9 e 10.
|
|
I valori in rosso sono quelli che conosciamo, quelli in blu sono quelli che troviamo con un bruteforcing di 65536 combinazioni. Analizziamo come viene generata dw3: dw3=Sbox[(dw2>>24)^(dw3&0xFF)]^(dw3>>8), ovvero prendo l'ultimo byte di dw3 precedente, lo xoro con il valore attuale del byte alto di dw2, e prendo il valore di S-box corrispondente a quell'indice e lo xoro con la vecchia dw3 shiftata di 8. Noi procediamo a ritroso, prendiamo c3 del ciclo9 e lo xoriamo con D3,del ciclo 10, otteniamo l'ultimo byte di S-box, che identifica in modo univoco l'indice: (dw2>>24)^(dw3&0xFF), ma noi conosciamo anche il d3(9) e ricaviamo il byte A2(10); sappiamo anche che il byte alto della S-box non viene xorato e viene copiato in A3, che quindi conosciamo. Xorando C3 con il terzo byte della S-box otteniamo b3.Possiamo quindi aggiungere 3 byte al nostro schemozzo:
|
|
Mancano ancora 10byte,ma a questo punto la strada si fa in salita: infatti questa funzione è fatta in modo da "incatenare" le tre dword, in modo che siano utilizzati tutti i byte, una e una sola volta; fanno eccezione a2 e d1 che sono utilizzati due volte,ma in due cicli successivi. L'uso delle S-box è sempre mediato da uno xor: l'indice è ottenuto xorando due byte di due cicli diversi,(P-d1 e A2-d3), il risultato è xorato da altri 3 byte(a1,b1,c1 e a3,b3,c3), mentre l'effetto di "incatenamento", è ottenuto utilizzando i byte D1 per ottenere Dw2, e A2 per Dw3. Un altro fattore che aumenta la sicurezza dell'algo è il fatto che dw2 è generata mediante una moltiplicazione modulare, che non ha proprietà in comune con lo xor, usato per dw1 e dw3; il numero utilizzato come secondo fattore,non è casuale: esso è dispari, pertanto non ha fattori in comune con il modulo(2^32), che potevano essere utilizzati come indicazioni per limitare il suo range. La scelta di questo numero mi lascia però qualche perplessità: infatti moltiplicandolo per i primi 256 numeri, si ottengono 256 dword, che non hanno nel byte alto tutti i valori possibili, ma solo 242. Questo vuol dire, che mantenendo le stesse dw, e variando il byte in input da 0 a FF, posso ottenere solo 242 valori di F(x); riscriviamo F(x) utilizzando i byte anziché le dw e cerchiamo di capire perché:
A1B1C1D1=00a1b1c1^Sbox[x^d1];
D1=c1^(Sbox[x^d1]&0xff);<--abbiamo
visto sopra che l'ultimo byte può assumere tutti i valori al variare delll'indice.
A2B2C2D2=0x8088405*(a2b2c2d2+D1)+1=(0x8088405*a2b2c2d2)+(0x8088405*D1)+1<--Ovviamente
tutto mod2^32, quindi A2 può assumere 242 valori
A3B3C3D3=Sbox[A2^d3]^00a3b3c3;<--sempre
solo 242
Questo fatto non significa che l'algoritmo sia insicuro, perché per come
funziona è molto difficile da sfruttare,anche perché lascia scoperti solo 14
valori su 242,io ce l'ho messo per completare l'analisi.
Ritornando al nostro metodo, abbiamo detto che procedendo in questo modo non
possiamo più andare avanti: ci rimangono 8byte scoperti, che moltiplicati per
le 2^16 combinazioni, è come fossero 10, e un bruteforce non sarebbe troppo
conveniente rispetto ad un dictionary attack: infatti 10byte equivalgono ad un
seriale di 18 caratteri che utilizzi maiuscole minuscole e numeri. L'unico
vantaggio è che si coprono TUTTE le possibilità con un numero finito di
tentativi.
Ciao a tutti
Winebag
|
Note finali |
Penso che per oggi possa bastare, non ho fatto il paragone con pkzip, perché non avrei potuto aggiungere nulla alla descrizione di Que, per quanto riguarda il tentativo di crackare l'algoritmo,prendetelo appunto come tentativo, utile soprattutto a me per capire meglio il suo funzionamento. Vi saluto tutti, salutando in modo particolare Zer0, autore di un tutorial sulla crittografia, apparso a puntate su OndaQuadra, che mi ha fornito dei fondamenti di crittografia che mi si sono fatti utili e consiglio a chi non ha le idee tanto chiare di darci un'abbondante letta, forse è un po' prolisso e filosofeggiante, ma ne vale la pena. Saluto anche GuZuRa, che gli ho risolto il MM2, ma forse non ci è arrivata l'e-mail.
|
Disclaimer |
Vorrei ricordare che il software va comprato e non rubato, dovete registrare il vostro prodotto dopo il periodo di valutazione. Non mi ritengo responsabile per eventuali danni causati al vostro computer determinati dall'uso improprio di questo tutorial. Questo documento è stato scritto per invogliare il consumatore a registrare legalmente i propri programmi, e non a fargli fare uso dei tantissimi file crack presenti in rete, infatti tale documento aiuta a comprendere lo sforzo immane che ogni singolo programmatore ha dovuto portare avanti per fornire ai rispettivi consumatori i migliori prodotti possibili.
Noi reversiamo al solo scopo informativo e di miglioramento del linguaggio Assembly.