Ultra Edit Keygenning
From UIC
Keygenniamo UltraEdit 15
| Infos | |
|---|---|
| Author: | Tonyweb |
| Email: | tonyweb_mail@yahoo.it |
| Website: | - |
| Date: | 20/04/2009 (dd/mm/yyyy) |
| Level: |
|
| Language: | Italian |
| Comments: |
E' il mio primo tutorial, siate comprensivi. |
Introduzione
In questo mio primo tutorial vorrei condividere con voi la mia più difficile esperienza di keygenning finora. Il programma in oggetto non ha affatto bisogno di presentazioni perciò non perdiamoci in chiacchiere e cominciamo.
Allegato
Tools & Files
Gli strumenti utilizzati sono quelli classici.
- PeID 0.95
- OllyDbg 1.10
- eXeScope (o qualunque altro visualizzatore di risorse)
- Visual Studio (per la scrittura del keygen)
URL o FTP del programma
Essay
Nel corso di questo tutorial analizzeremo in dettaglio le routine predisposte al controllo dei seriali immessi dall'utente ed illustreremo un modo per reversare il tutto e generare una chiave valida.
Il primo avvio
Avviamo il programma per la prima volta e ci viene mostrata una Reminder Nag che ci informa del fatto che abbiamo a disposizione soltanto 45 giorni per provare il prodotto e che, se vogliamo utilizzarlo oltre questo periodo, dobbiamo acquistare una licenza.
Cliccando sul pulsante "Registra" si ha la possibilità di inserire un nome utente e un codice di autorizzazione per registrare il programma. Se proviamo ad inserire una coppia nome utente / seriale il programma ci chiederà di chiudere il programma e riavviarlo per validare le informazioni inserite.
Cosa abbiamo capito finora ? Che il programma accetta una combinazione nome utente / seriale e che verifica che sia corretta all'avvio. Se siamo particolarmente attenti possiamo anche notare che, prima di chiudersi, il programma salva un file dal nome uedit32.reg nella cartella "%userprofile%\Application Data\IDMComp\UltraEdit". Sarà da questo file che saranno prelevate le informazioni digitate dall'utente nella dialog di registrazione e successivamente controllate. Notiamo che detto file viene creato solo se, almeno formalmente, il seriale è corretto. Un seriale è corretto se è costituito da otto gruppi di cinque caratteri separati da un trattino:
Quando ho studiato la versione precedente del target, per individuare la routine di registrazione ho pensato di sfruttare questo comportamento posizionando dei BP sulle API di apertura e lettura dei file (CreateFileA/W, ReadFileA/W, ecc.) ma, per steppare fino al punto giusto, ho perso un sacco di tempo prezioso.
Questa volta, invece, che il target lo conoscevo, ho utilizzato un approccio molto più diretto ... ne parleremo dopo.
Alla ricerca di Packer
Dato che ci siamo decisi ad affrontarlo vediamo che tipo di nemico abbiamo di fronte. Lanciamo PEiD e guardiamo cosa ha da dirci.
Meno male. Nessun unpacking necessario (sono molto scarso nel MUP-ing). Il risultato del plugin KANAL potrebbe spaventarci ... ma non c'è niente di cui preoccuparsi, ve l'assicuro. Ora che abbiamo qualche informazione in più possiamo aprire il nostro target (Uedit32.exe) in OllyDbg e fare un primo giro sulla giostra.
Possiamo partire.
Accennavo prima ad un metodo veloce per rintracciare la routine di controllo; questo è il momento di svelare qual è. Il programma controlla le informazioni di registrazione (se inserite) e, se risultano non valide, mostra nuovamente la Nag illustrata in precedenza, con la quale richiede di specificare dati corretti questa volta.
L'idea è quella di sfruttare la Nag e di risalire, dal punto in cui viene creata, alla routine di registrazione. Qual è il vantaggio rispetto ad altre tecniche ? La risposta è che in questo programma, una volta individuata la creazione della nag, c'è un modo per rintracciare quasi istantaneamente la routine cui siamo interessati.
Ma non perdiamoci in chiacchiere. Apriamo il file Uedit32.exe con un editor di risorse e spulciamo tra i dialoghi alla ricerca della nag.
Ops! ... non ce ne sono. Allora apriamo, sempre con l'editor di risorse, il file ueres.dll che troviamo nella stessa cartella dell'eseguibile principale (ha un nome veramente esplicativo Uedit Resources ... non può che essere il file giusto :) ). In esso, infatti, troviamo molte delle risorse utilizzate dal programma e soprattutto troviamo un numero molto elevato di template per finestre di dialogo.
Dopo un po' di tempo speso alla ricerca del dialog corretto, ci imbattiamo finalmente nella risorsa con id (decimale) 28269 ... che sembra proprio quello che stavamo cercando. A questo punto basta pensare al modo in cui l'applicazione recupera questo template. In genere si utilizza l'API FindResource cui viene passato come argomento l'id della risorsa da prelevare.
Non ci resta dunque che cercare tutte le occorrenze dei comandi PUSH 6E6D ( 6E6D è la controparte esadecimale dell'id del dialog.). Andiamo in OllyDbg (che abbiamo già aperto credo ;) ) e scegliamo Search For | All Commands ... ottenendo come risultato cinque occorrenze:
Poniamo un BP su tutte e avviamo l'applicazione (F9). Scatta subito un BP, ma è ancora un po' presto (la Nag appare dopo la finestra principale) quindi rimuoviamo il BP e proseguiamo con un altro F9. Scatta nuovamente un breakpoint ma questa volta siamo nel punto giusto, dal momento che la finestra principale del programma è già visualizzata e ci sono dei riferimenti a stringhe molto convincenti quali "Days to expire" ;).
Ci troviamo all'indirizzo
Ora saliamo un po' verso l'inizio della procedura, esattamente qui:
0043ECA2 . 68 B086A500 PUSH Uedit32.00A586B0
0043ECA7 . 64:A1 0000000>MOV EAX, DWORD PTR FS:[0]
Ora posizioniamoci in linea 0043ECA0 e facciamo CTRL+R (Find References). Troviamo un unico riferimento. Clicchiamoci su e ci ritroviamo all'indirizzo 00446220:
0044621C MOV DWORD PTR SS:[ESP+70], EBX ; |
00446220 MOV DWORD PTR SS:[ESP+74], Uedit32.0043ECA0 ; |
00446228 MOV DWORD PTR SS:[ESP+78], EBX ; |
0044622C MOV DWORD PTR SS:[ESP+7C], EBX ; |
00446230 MOV DWORD PTR SS:[ESP+80], EDX ; |
00446237 MOV DWORD PTR SS:[ESP+84], EBX ; |
0044623E MOV DWORD PTR SS:[ESP+88], EBX ; |
00446245 MOV DWORD PTR SS:[ESP+8C], EBX ; |
0044624C MOV DWORD PTR SS:[ESP+90], EBX ; |
00446253 MOV DWORD PTR SS:[ESP+94], Uedit32.00ADA1E8 ; |ASCII "UEAfx:200:0x0008:0:010"
0044625E CALL DWORD PTR DS:[<&USER32.RegisterClassA>] ; \RegisterClassA
00446264 MOV EAX, DWORD PTR DS:[D02B30]
:
:
0044628B PUSH EBX ; /lParam = NULL
0044628C PUSH ECX ; |hInst = NULL
0044628D PUSH EBX ; |hMenu = NULL
0044628E PUSH EAX ; |hParent = 7FFDD000
0044628F PUSH EBX ; |Height = 0
00446290 PUSH EBX ; |Width = 0
00446291 PUSH EBX ; |Y = 0
00446292 PUSH EBX ; |X = 0
00446293 PUSH EBX ; |Style = WS_OVERLAPPED
00446294 PUSH Uedit32.00ADA1E8 ; |WindowName = "UEAfx:200:0x0008:0:010"
00446299 PUSH Uedit32.00ADA1E8 ; |Class = "UEAfx:200:0x0008:0:010"
0044629E PUSH EBX ; |ExtStyle = 0
0044629F MOV DWORD PTR SS:[ESP+CC], EBX ; |
004462A6 CALL DWORD PTR DS:[<&USER32.CreateWindowExA>] ; \CreateWindowExA
004462AC LEA ECX, DWORD PTR SS:[ESP+14]
004462B0 PUSH ECX
004462B1 LEA EDX, DWORD PTR SS:[ESP+28]
004462B5 PUSH EDX ;
004462B6 MOV DWORD PTR DS:[D02B10], EAX
004462BB MOV EAX, DWORD PTR DS:[D02B34]
004462C0 PUSH Uedit32.00D02AEC
004462C5 PUSH EAX
004462C6 PUSH ESI ;
004462C7 MOV BYTE PTR DS:[D05AD4], BL
004462CD MOV DWORD PTR DS:[D02AEC], 2A
004462D7 MOV DWORD PTR SS:[ESP+28], EBX
004462DB CALL Uedit32.00440D90
004462E0 MOV EBP, DWORD PTR DS:[<&USER32.SendMessageA>] ;
004462E6 ADD ESP, 14
004462E9 TEST AL, AL
004462EB JE Uedit32.0044660E
004462F1 CMP DWORD PTR DS:[D02B14], EBX
004462F7 JNZ Uedit32.0044660E
004462FD CMP DWORD PTR SS:[ESP+14], EBX
00446301 JE SHORT Uedit32.0044632B
Se abbiamo già analizzato la versione precedente di UltraEdit, sappiamo che questo codice è quello che si occupa di visualizzare la finestra principale, ma soprattutto ci mostra il nostro punto di attacco: la routine di registrazione.
Come faccio a dirlo ? Sicuramente l'aver analizzato la versione precedente mi rende assolutamente certo che ci troviamo nel posto giusto e che la routine in linea 004462DB (col relativo TEST AL,AL) è la routine di controllo della registrazione. Se non ci si vuole affidare a "ricordi passati" basta guardare un po' più in basso e rendersi conto che avere AL = 0 ci fa saltare porzioni di codice molto interessanti, come la seguente.
00446336 0F8C 44010000 JL Uedit32.00446480
0044633C 68 D0A6AD00 PUSH Uedit32.00ADA6D0 ; /Arg2 = 00ADA6D0 ASCII "Extension License"
00446341 51 PUSH ECX ; |Arg1 = 00D052D8
00446342 E8 60B95800 CALL Uedit32.009D1CA7 ; \Uedit32.009D1CA7
00446347 83C4 08 ADD ESP, 8
0044634A 3BC3 CMP EAX, EBX
0044634C 0F84 2E010000 JE Uedit32.00446480
Possiamo tranquillamente fare la prova del nove. Poniamo un BP in linea 004462DB e riavviamo, questa volta avendo cura di impostare AL = 1 prima di eseguire l'istruzione di TEST. Il programma si avvia senza la nag e in stato registrato. Inoltre se possediamo una registrazione corretta per la versione 14 (un file uedit32.reg) vedremo il nostro nome comparire nella finestra di about.
Assodato che ci troviamo nella posizione voluta, non ci resta altro che studiare la routine di registrazione. Prima di cominciare, voglio segnalare che il codice che mostrerò presenta delle label che ho posizionato in modo da rendere più chiaro il tutto: alcune routine, dunque, avranno nomi (label appunto) come FillWithZero o CopiaStringa che le rendono d'immediata comprensione.
La routine di registrazione è divisa sostanzialmente in tre macro aree che analizzeremo, per quanto possibile, separatamente.
Routine di Registrazione - Parte 1: Generazione del vettore di Lookup.
D'accordo abbiamo trovato la routine di registrazione e sappiamo anche che utilizza il valore di AL per restituire l'indicazione sullo stato di registrazione. Rimuoviamo tutti i BP che abbiamo finora e poniamo un bel BP sull'inizio di questa routine e riavviamo. Ci troviamo qui:
00440D92 |. 68 6B8BA500 PUSH Uedit32.00A58B6B
00440D97 |. 64:A1 0000000>MOV EAX, DWORD PTR FS:[0]
00440D9D |. 50 PUSH EAX
00440D9E |. 81EC 9C020000 SUB ESP, 29C
00440DA4 |. A1 3CFECF00 MOV EAX, DWORD PTR DS:[CFFE3C]
00440DA9 |. 33C4 XOR EAX, ESP
00440DAB |. 898424 980200>MOV DWORD PTR SS:[ESP+298], EAX
con in EAX e in ESI i puntatori al nome dell'utente e al seriale da controllare. Chiaramente se non abbiamo inserito nessun dato nella finestra di registrazione punteranno entrambi a delle dword pari a 0.
La routine che ci troviamo davanti può sembrare lunga ma, una volta capito come funziona, risulterà agevole seguirla. Procediamo un passo alla volta.
00440DBE LEA EAX, DWORD PTR SS:[ESP+2B0]
00440DC5 MOV DWORD PTR FS:[0], EAX
00440DCB MOV EBP, DWORD PTR SS:[ESP+2C4] ; << Seriale da controllare
00440DD2 MOV ECX, DWORD PTR SS:[ESP+2CC] ; << ind. di ritorno
00440DD9 MOV EDX, DWORD PTR SS:[ESP+2D0] ;
00440DE0 MOV EDI, DWORD PTR SS:[ESP+2C0] ; << UserName
00440DE7 MOV EAX, DWORD PTR SS:[ESP+2C8] ; << ptr a 2A = 42 (indicatore per file *.reg)
00440DEE MOV DWORD PTR SS:[ESP+1C], ECX
00440DF2 MOV DWORD PTR SS:[ESP+18], EDX
00440DF6 TEST EBP, EBP ; << Inserito un seriale ?
00440DF8 JE SHORT Uedit32.00440E04
00440DFA MOV DWORD PTR DS:[D02AE8], 1 ; << si, inserito !
00440E04 MOV DWORD PTR DS:[EAX], 0 ; << azzera il 2A
00440E0A MOV EAX, EDI ; << userName
00440E0C LEA EDX, DWORD PTR DS:[EAX+1]
00440E0F NOP
00440E10 /MOV CL, BYTE PTR DS:[EAX]
00440E12 |INC EAX
00440E13 |TEST CL, CL
00440E15 \JNZ SHORT Uedit32.00440E10
00440E17 SUB EAX, EDX
00440E19 CMP EAX, 6 ; << Verifica che la lunghezza dello userName sia almeno 6
00440E1C JB Uedit32.0044100C
00440E22 MOV EAX, EBP ; << Seriale
00440E24 LEA EDX, DWORD PTR DS:[EAX+1]
00440E27 /MOV CL, BYTE PTR DS:[EAX]
00440E29 |INC EAX
00440E2A |TEST CL, CL
00440E2C \JNZ SHORT Uedit32.00440E27
00440E2E SUB EAX, EDX
00440E30 CMP EAX, 2F ; << La lunghezza del seriale deve essere almeno 47
00440E33 JB Uedit32.0044100C
Inizialmente troviamo i classici controlli sulla presenza o meno delle informazioni di registrazione e sulla loro validità formale. Da queste poche istruzioni già si comprende che il nome utente deve essere almeno di 6 caratteri e che il seriale deve comporsi di non meno di 47 caratteri.
La cosa un po' scocciante, in questo codice, è che, di continuo, sono eseguite delle operazioni che sostanzialmente non hanno nulla a che vedere con la determinazione della validità della registrazione, ma si occupano di preparare il file uedit32.reg dove saranno posizionati, in forma criptata, i dati di licenza.
La routine prosegue ...
00440E3B LEA EAX, DWORD PTR SS:[ESP+75]
00440E3F PUSH 0 ; << carattere con cui riempire
00440E41 PUSH EAX ; << indirizzo da riempire
00440E42 MOV BYTE PTR SS:[ESP+23], 1 ; << segna un flag
00440E47 MOV BYTE PTR SS:[ESP+7C], CL ; << azzera il primo byte
00440E4B CALL <Uedit32.FillWithZero> ; << entro
00440E50 PUSH 1FF ; << dimensione ( 511 bytes )
00440E55 LEA ECX, DWORD PTR SS:[ESP+BD]
00440E5C PUSH 0 ; << carattere di riempimento
00440E5E PUSH ECX ; << indirizzo da riempire ( 0024F9D1 ) - per copia userName
00440E5F MOV BYTE PTR SS:[ESP+C4], 0 ; << azzera il primo byte
00440E67 CALL <Uedit32.FillWithZero>
00440E6C PUSH EDI ; << push nome utente
00440E6D LEA EDX, DWORD PTR SS:[ESP+C8]
00440E74 PUSH 200 ; << 200h = 512d (max len username)
00440E79 PUSH EDX ; << indirizzo appena svuotato (il primo dei due)
00440E7A CALL <Uedit32.CopiaStringa> ; << Copia UserName
... preparando (svuotando) due aree di memoria al fine di poter eseguire delle copie del nome utente (vediamo che il nome utente volendo può raggiungere i 512 caratteri) e, successivamente, di una stringa estratta dal seriale. Ah, dimenticavo. Mi sembrava scontato dirlo, ma tenete presente che alcuni indirizzi, come quelli scritti nei commenti, possono essere diversi nel vostro sistema.
Ora stiamo attenti, il nome utente e il seriale sono passati come argomento alla routine che io qui ho chiamato GenerazioneVettoreEdEstrazioneStringa che mi sembra un buon modo per ricordarmi cosa faccia questa routine.
00440E80 LEA EAX, DWORD PTR SS:[ESP+7C] ; << 0024F978
00440E84 PUSH EBP ; << push seriale
00440E85 PUSH EAX ; << push indirizzo (doppio ptr. a zona in cui andranno scritte le info dei match
00440E86 CALL <Uedit32.GenerazioneVettoreEdEstrazioneStringa> ; << entro
Entriamo al suo interno con F7 e guardiamoci un po' in giro. Il codice che ci interessa comincia da qui:
0043F7C9 MOV ESI, DWORD PTR SS:[ESP+A3C] ; << username
0043F7D0 XOR EBX, EBX ; << EBX := 0
0043F7D2 PUSH 3B ; << dimensione da riempire
0043F7D4 LEA EAX, DWORD PTR SS:[ESP+9E9]
0043F7DB PUSH EBX ; << carattere di riempimento
0043F7DC MOV DWORD PTR SS:[ESP+1C], EBX ; << azzera primo byte
0043F7E0 PUSH EAX ; << ind. da riempire
0043F7E1 MOV DWORD PTR SS:[ESP+A38], EBX ; << zero
0043F7E8 MOV DWORD PTR SS:[ESP+28], EBP
0043F7EC MOV BYTE PTR SS:[ESP+9F0], BL ; << azzero primo byte
0043F7F3 CALL <Uedit32.FillWithZero> ; << Libera zona per copiare il seriale
0043F7F8 PUSH EDI ; |<< Seriale
0043F7F9 LEA ECX, DWORD PTR SS:[ESP+9F4] ; |
0043F800 PUSH 3C ; |<< 60
0043F802 PUSH ECX ; |<< dove copiare il seriale
0043F803 CALL <Uedit32.CopiaStringa> ; \CopiaStringa
0043F808 MOV EAX, ESI ; << Nome Utente
0043F80A ADD ESP, 18
0043F80D XOR EDX, EDX ; <<< EDX := 0
0043F80F LEA EDI, DWORD PTR DS:[EAX+1] ; << calcola len username
0043F812 /MOV CL, BYTE PTR DS:[EAX]
0043F814 |INC EAX
0043F815 |CMP CL, BL
0043F817 \JNZ SHORT Uedit32.0043F812
0043F819 SUB EAX, EDI ; << EAX = lungh. username
0043F81B XOR ECX, ECX ; ECX := 0
0043F81D CMP EAX, EBX ; << Hai specificato un userName ?
0043F81F JLE SHORT Uedit32.0043F82C ; << se si' non salta
0043F821 /MOVSX EDI, BYTE PTR DS:[ESI+ECX] ; << Hash username - serve ad inizializzare il vettore
0043F825 |INC ECX
0043F826 |ADD EDX, EDI
0043F828 |CMP ECX, EAX
0043F82A \JL SHORT Uedit32.0043F821
0043F82C OR EDX, 4F71A22 ; << Somma i caratteri del nome utente ed effettua OR con costante
In pratica, la prima parte della routine esegue una copia del seriale e determina la lunghezza del nome utente, al fine di poter calcolare una sorta di hash dato dalla somma dei codici dei caratteri che lo compongono OR-ati (passatemi il termine) con una costante.
A cosa serve questo hash ? Serve ad inizializzare il processo di generazione di un vettore di dword che verrà utilizzato durante l'elaborazione del seriale inserito (più avanti in questa stessa routine).
0043F833 LEA ECX, DWORD PTR SS:[ESP+24] ; << ind. vettore
0043F837 CALL <Uedit32.GenerazioneVettoreIniziale> ; << Genera un vettore iniziale in base
; << all'hash utente calcolato
La routine di generazione del vettore è riportata di seguito.
00654AD4 PUSH ESI ; << nome utente
00654AD5 PUSH EDI ; << 2E = 46
00654AD6 LEA EAX, DWORD PTR DS:[ECX+8]
00654AD9 MOV ESI, 68 ; << esi := 68h (iterazioni ciclo)
00654ADE MOV EDI, EDI ; ??? inutile ???
00654AE0 MOV EDI, EDX ; Prima DWORD – Inizializzata con hash Nome Utente
00654AE2 IMUL EDX, EDX, 10DCD ; << EDX := ( EDX * 10DCDh )
00654AE8 AND EDI, FFFF0000 ; << EDI := lascia solo HIWORD(EDX)
00654AEE INC EDX ; << EDX += 1
00654AEF MOV DWORD PTR DS:[EAX-8], EDI ; << salva EDI
00654AF2 MOV EDI, EDX ; << EDI := EDX
00654AF4 IMUL EDX, EDX, 10DCD ; << EDX := EDX * 10DCDh
00654AFA SHR EDI, 10 ; << EDI := EDI / 2^16 (recupera parte alta della dword)
00654AFD OR DWORD PTR DS:[EAX-8], EDI ; << OR-ing della dword appena scritta
00654B00 INC EDX ; << edx++
00654B01 MOV EDI, EDX ; << ESEGUE GLI STESSI DUE PASSI per la prossima dword>>
00654B03 IMUL EDX, EDX, 10DCD
00654B09 AND EDI, FFFF0000
00654B0F INC EDX
00654B10 MOV DWORD PTR DS:[EAX-4], EDI ; Seconda Dword
00654B13 MOV EDI, EDX
00654B15 IMUL EDX, EDX, 10DCD
00654B1B SHR EDI, 10
00654B1E OR DWORD PTR DS:[EAX-4], EDI
00654B21 INC EDX
00654B22 MOV EDI, EDX ; << terza dword
00654B24 IMUL EDX, EDX, 10DCD
00654B2A AND EDI, FFFF0000
00654B30 INC EDX
00654B31 MOV DWORD PTR DS:[EAX], EDI
00654B33 MOV EDI, EDX
00654B35 IMUL EDX, EDX, 10DCD
00654B3B SHR EDI, 10
00654B3E OR DWORD PTR DS:[EAX], EDI
00654B40 INC EDX
00654B41 MOV EDI, EDX ; << quarta dword
00654B43 IMUL EDX, EDX, 10DCD
00654B49 AND EDI, FFFF0000
00654B4F INC EDX
00654B50 MOV DWORD PTR DS:[EAX+4], EDI
00654B53 MOV EDI, EDX
00654B55 IMUL EDX, EDX, 10DCD
00654B5B SHR EDI, 10
00654B5E OR DWORD PTR DS:[EAX+4], EDI
00654B61 INC EDX
00654B62 MOV EDI, EDX ; << quinta dword
00654B64 IMUL EDX, EDX, 10DCD
00654B6A AND EDI, FFFF0000
00654B70 INC EDX
00654B71 MOV DWORD PTR DS:[EAX+8], EDI
00654B74 MOV EDI, EDX
00654B76 IMUL EDX, EDX, 10DCD
00654B7C SHR EDI, 10
00654B7F OR DWORD PTR DS:[EAX+8], EDI
00654B82 INC EDX ; << sesta dword
00654B83 MOV EDI, EDX
00654B85 IMUL EDX, EDX, 10DCD
00654B8B AND EDI, FFFF0000
00654B91 INC EDX
00654B92 MOV DWORD PTR DS:[EAX+C], EDI
00654B95 MOV EDI, EDX
00654B97 IMUL EDX, EDX, 10DCD
00654B9D SHR EDI, 10
00654BA0 OR DWORD PTR DS:[EAX+C], EDI
00654BA3 INC EDX
00654BA4 ADD EAX, 18 ; << passa alle prossime 6 dword
00654BA7 SUB ESI, 1 ; << decremento contatore ciclo
00654BAA JNZ Uedit32.00654AE0 ; << continua ciclo
00654BB0 POP EDI
00654BB1 MOV DWORD PTR DS:[ECX+9C0], 270 ; << segna inizializzazione in posizione prima di seriale
00654BBB POP ESI
00654BBC RETN 4
Come si nota si esegue la stessa operazione per 6 dword consecutive e per un totale di 68h ( = 104 ) iterazioni. Pertanto otterremo un vettore di 104 x 24 = 2496 byte (9C0 in hex). E' bene ribadire il fatto che i valori di questo vettore dipendono dal nome utente dal momento che la prima dword ad essere elaborata è proprio l'hash utente che abbiamo appena calcolato.
Inoltre si noti che viene memorizzato un valore (270h) alla fine del vettore. Questo serve ad indicare che il vettore è stato appena costruito. E' un indicatore che sarà utilizzato per stabilire se occorre eseguire o meno un'ulteriore elaborazione (v. LookupInVettore_conRicalcolo nel file di testo allegato).
In C++ possiamo codificare la generazione del vettore con queste due routine (parte del codice allegato):
{
const int MUL_CONST = 0x10DCD;
const int AND_CONST = 0xFFFF0000;
const int SHIFT_RIGHT_CONST = 0x10;
int edi = edx;
edx = edx * MUL_CONST;
edi = edi & AND_CONST;
edx++;
this->vettore[tableIdx] = edi;
edi = edx;
edx = edx * MUL_CONST;
edi = ( (unsigned) edi >> SHIFT_RIGHT_CONST );
this->vettore[tableIdx] |= edi;
edx++;
return edx;
}
void TableHandler::creaVettore()
{
// Vettore riempito con un ciclo di 68h (=104) iterazioni scrivendo
// ad ogni iterazione 6 DWORD (= 24 byte ).
// per cui si ha 104x24 = 2496 byte (9C0 in hex) !!!
this->vettore = new int[624]; // 624 = 2496/SIZEOF(DWORD) = 2496/4
// Avvia il ciclo di 68h (104) iterazioni
int max_esi = 0x68;
int edx = this->userHash; // Valore iniziale: hash utente
int tableIdx = 0;
// Ciclo a ritroso
for ( int esi = 0; esi < max_esi; esi++ )
{
// 1a DWORD
edx = this->modificaVettore( edx, tableIdx++ );
// 2a DWORD
edx = this->modificaVettore( edx, tableIdx++ );
// 3a DWORD
edx = this->modificaVettore( edx, tableIdx++ );
// 4a DWORD
edx = this->modificaVettore( edx, tableIdx++ );
// 5a DWORD
edx = this->modificaVettore( edx, tableIdx++ );
// 6a DWORD
edx = this->modificaVettore( edx, tableIdx++ );
}
}
Una volta costruito il vettore si procede ad elaborare il seriale inserito:
0043F83D MOV DWORD PTR SS:[EBP+18], 0F ; << 15
0043F844 MOV DWORD PTR SS:[EBP+14], EBX ; << 0
0043F847 PUSH Uedit32.00AD46F0
0043F84C MOV ECX, EBP
0043F84E MOV BYTE PTR SS:[EBP+4], BL ; << 0
0043F851 CALL <Uedit32.NonImportante1> ;
0043F856 MOV AL, 30 ; << '0'
0043F858 MOV EDI, 1 ; << EDI := 1
0043F85D MOV DWORD PTR SS:[ESP+A2C], EBX ; << 0
0043F864 MOV DWORD PTR SS:[ESP+14], EDI ; << 1
0043F868 MOV BYTE PTR SS:[ESP+9E8], AL ; << Ignora il quinto carattere di ogni sotto-gruppo
0043F86F MOV BYTE PTR SS:[ESP+9EE], AL
0043F876 MOV BYTE PTR SS:[ESP+9F4], AL
0043F87D MOV BYTE PTR SS:[ESP+9FA], AL
0043F884 MOV BYTE PTR SS:[ESP+A00], AL
0043F88B MOV BYTE PTR SS:[ESP+A06], AL
0043F892 MOV BYTE PTR SS:[ESP+A0C], AL
0043F899 MOV BYTE PTR SS:[ESP+A12], AL
0043F8A0 LEA ESI, DWORD PTR SS:[ESP+9E4] ; << seriale
0043F8A7 CMP BYTE PTR SS:[ESP+9E4], BL ; << finito ?
0043F8AE JE SHORT Uedit32.0043F8FA
0043F8B0 /CMP BYTE PTR DS:[ESI], 2D ; << trovato il trattino ?
0043F8B3 |JE SHORT Uedit32.0043F8F5 ; << ignora il trattino
0043F8B5 |LEA ECX, DWORD PTR SS:[ESP+20] ; << vettore_inizializzazione
0043F8B9 |CALL <Uedit32.LookupInVettore_conRicalcolo>
0043F8BE |XOR EDX, EDX ; << EDX := 0
0043F8C0 |MOV ECX, 0A ; << ECX := 0A (10)
0043F8C5 |DIV ECX ; << divisione di EAX (restituito da CALL)
; << per 0A e il resto in EDX
0043F8C7 |MOV AL, BYTE PTR DS:[ESI] ; << AL := carattere del seriale
0043F8C9 |SUB AL, DL ; << sottrai ad esso il resto
0043F8CB |SUB AL, 41 ; << e 41h ('A')
0043F8CD |MOVSX EDX, AL ; << a questo punto EDX := AL
0043F8D0 |MOVZX EAX, BYTE PTR DS:[EDX+ADA054] ; << Lookup di valore in 'X6A3KVMOQ8D2HCPI'
0043F8D7 |MOV BYTE PTR SS:[ESP+18], AL ; << salva poi il valore lookup-ato (BYTE ptr)
0043F8DB |MOV ECX, DWORD PTR SS:[ESP+18] ; << ... e lo copia in ECX (tutta la dword pero')
0043F8DF |PUSH ECX ; << PUSH di questo valore
0043F8E0 |PUSH 1 ; << 1
0043F8E2 |MOV ECX, EBP ; << Dove salvare il carattere lookup-ato
0043F8E4 |CALL <Uedit32.SalvaCarattereLookup> ; <<
0043F8E9 |INC EDI ; << prox char
0043F8EA |CMP EDI, 5 ; << finito un sottogruppo ?
0043F8ED |JNZ SHORT Uedit32.0043F8F5
0043F8EF |MOV EDI, 1 ; << riparte da 1 per EDI
0043F8F4 |INC ESI
0043F8F5 |INC ESI
0043F8F6 |CMP BYTE PTR DS:[ESI], BL ; << finito il seriale ?
0043F8F8 \JNZ SHORT Uedit32.0043F8B0
0043F8FA MOV EAX, EBP
:
:
0043F91D ADD ESP, 0A1C
0043F923 RETN
In linea 0043F8D0 il codice del ciclo fa riferimento alla locazione ADA054. Ecco cosa contiene.
Cerchiamo di riassumere il comportamento di questa porzione di codice.
Innanzitutto si procede al recupero del seriale e ad una sua pre-elaborazione: osserviamo, infatti, che il quinto carattere di ogni sottogruppo è sostituito da uno '0' (ecco il senso di tutti quei MOV BYTE PTR SS:[ESP+?], AL). Ciò significa che, qualunque carattere "terminatore di gruppo" è ignorato. Proseguendo incontriamo un ciclo nel quale, per ogni carattere del seriale, viene:
- eseguita una lookup nel vettore calcolato precedentemente (letta una dword)
- effettuata una divisione della dword letta per 10, conservando il resto
- utilizzato il resto come sottraendo per ricavare un indice (0-based) nella stringa ' X6A3KVMOQ8D2HCPI'
- Concatenati i caratteri estratti da detta stringa.
In totale avremo una nuova stringa di 32 caratteri che sarà composta, se il seriale è corretto, dai soli caratteri appartenenti a "X6A3KVMOQ8D2HCPI". Perché 32 caratteri ? Allora non siete stati attenti. ;) Il processo ignora completamente il quinto carattere di ogni sottogruppo passando al prossimo carattere; viene trattato alla stregua del trattino separatore.
La routine LookupInVettore_conRicalcolo non fa altro che pescare un valore dal vettore costruito in precedenza sulla base della posizione del carattere corrente nel seriale (valori da 0 a 31). Solo la prima volta (per l'indice 0) sono eseguiti due ulteriori sotto-cicli di rielaborazione dei valori nel vettore (v. file di testo allegato per il listato commentato).
Ricapitoliamo cosa abbiamo visto finora:
- Il nome utente viene elaborato sommando i codici ASCII dei suoi caratteri OR-ati con una costante.
- Con il valore così ottenuto si costruisce un vettore di lookup.
- Sono analizzati i 32 caratteri del seriale (ignorati trattini e tutti i fine gruppo), per ognuno dei quali viene eseguita una lookup nel vettore.
- Col valore recuperato dal vettore è eseguita una divisione per 10 il cui resto è usato per ricavare un indice nella stringa 'X6A3KVMOQ8D2HCPI'.
- Sono concatenati i caratteri estratti da questa stringa fissa ("X6A3KVMOQ8D2HCPI") per comporre una nuova stringa di 32 caratteri.
Routine di Registrazione – Parte 2: Data di sistema, Versione Programma e Generazione dei match.
Torniamo alla routine principale. Dopo la call a GenerazioneVettoreEdEstrazioneStringa il codice prosegue copiando la stringa di 32 caratteri estratta dal seriale per una successiva analisi e procede a convertire la data corrente in FileTime
00440E8E CMP DWORD PTR SS:[ESP+6C], 10 ;
00440E93 MOV EAX, DWORD PTR SS:[ESP+58] ; << Stringa estratta dal seriale
00440E97 MOV DWORD PTR SS:[ESP+2B8], 0 ;
00440EA2 JNB SHORT Uedit32.00440EA8 ; v
00440EA4 LEA EAX, DWORD PTR SS:[ESP+58]
00440EA8 PUSH EAX ; /<< push stringa estratta dal seriale
00440EA9 LEA ECX, DWORD PTR SS:[ESP+74] ; |
00440EAD PUSH 3C ; |<< dimensione stringa
00440EAF PUSH ECX ; |<< dove copiarla (sopra il nome utente)
00440EB0 CALL <Uedit32.CopiaStringa> ; \CopiaStringa
00440EB5 ADD ESP, 0C
00440EB8 PUSH Uedit32.00AD9C40 ; /pModule = "kernel32.dll"
00440EBD CALL DWORD PTR DS:[<&KERNEL32.GetModuleHandleA>] ; \GetModuleHandleA
00440EC3 LEA EDX, DWORD PTR SS:[ESP+44] ;
00440EC7 MOV ESI, EAX ; << salva kernel32.dll imagebase
00440EC9 PUSH EDX
00440ECA LEA EAX, DWORD PTR SS:[ESP+38] ;
00440ECE PUSH EAX
00440ECF LEA ECX, DWORD PTR SS:[ESP+2C] ; << currentSystemTime
00440ED3 PUSH ECX
00440ED4 CALL <Uedit32.SystemTime_FileTime> ; << Determina Data Corrente (in SystemTime) +
<< altro che non ci interessa (Genera 2
<< FileTime e li converte in SystemTime)
00440ED9 LEA EDX, DWORD PTR SS:[ESP+30] ; << ptr. a currentSystemTime
00440EDD PUSH ESI
00440EDE PUSH EDX ; << push currentSysTime
00440EDF CALL <Uedit32.DataCorrenteInFileTime> ; <<
Perchè è importante questo ? Perchè, come vedremo tra poco, il valore della data corrente in FileTime partecipa attivamente alla determinazione della validità del seriale.
Dopo la conversione della data si effettua un'altra semplice trasformazione. Il valore della versione "master" del programma 15.00.00 viene elaborato in questa routine:
00440EFD CALL <Uedit32.DoBSWAP_Parametro> ; << bswap della versione del programma
che riporto di seguito.
0040187A . 892C24 MOV DWORD PTR SS:[ESP], EBP
0040187D . 54 PUSH ESP
0040187E . 5D POP EBP ;
0040187F . 50 PUSH EAX
00401880 . 53 PUSH EBX
00401881 . E8 00000000 CALL Uedit32.00401886
00401886 $ 5B POP EBX ;
00401887 . 81EB 6E080000 SUB EBX, 86E ; << DoveSalvare
0040188D 8B45 08 MOV EAX, DWORD PTR SS:[EBP+8]
00401890 . F7D0 NOT EAX ; << NOT
00401892 . 0FC8 BSWAP EAX ; << BSWAP
00401894 . 8903 MOV DWORD PTR DS:[EBX], EAX ; << salva valore
00401896 . 5B POP EBX ;
00401897 . 58 POP EAX ;
00401898 . 8B2C24 MOV EBP, DWORD PTR SS:[ESP] ;
0040189B . 83C4 08 ADD ESP, 8
0040189E .- FF6424 FC JMP DWORD PTR SS:[ESP-4] ; << Torna al chiamante
Niente di che, semplicemente un NOT e un BSWAP per non renderla immediatamente visibile in seguito.
Adesso, di nuovo attenti. Nome utente e stringa estratta dal seriale sono entrambe passate alla routine che ho chiamato DeterminaMatchEStabilisciReg. All'uscita dalla routine un determinato valore (presente nella dword all'indirizzo 401096, cui ho assegnato la label "controllo") viene salvato in EBX. Questo valore è "calcolato" dalla call che stiamo per esaminare ed è questo che, in definitiva, ci consente di sapere se siamo o meno registrati. Teniamolo a mente per dopo.
00440F09 PUSH EDX
00440F0A LEA EAX, DWORD PTR SS:[ESP+94] ; << stringa estratta da seriale
00440F11 PUSH EAX
00440F12 CALL <Uedit32.DeterminaMatchEStabilisciReg>
00440F17 MOV EBX, DWORD PTR DS:[<controllo>] ; << Salva il valore della dword di controllo
<< in EBX (la call modifica la dword in
<< oggetto per altri scopi)
OK. F7 per entrare nella routine, ed ecco cosa abbiamo di fronte. La routine è un po' lunghetta per cui l'analizzeremo a pezzi. Lo stile di questa routine è leggermente diverso dal resto del codice ... forse per renderci la vita un po' più complessa (o interessante ?).
004018D6 PUSH DWORD PTR SS:[EBP+C] ; << username
004018D9 PUSH EAX ; << stringa estratta da serial
004018DA CALL Uedit32.004018DF ; << riga successiva
004018DF POP EAX ;
004018E0 ADD EAX, 9
004018E3 XCHG DWORD PTR SS:[ESP], EAX ; << Controlla il valore di EAX - e' modificato in 'ElaboraUserName'
004018E6 JMP EBX ; << CALCOLA HASH USERNAME
004018E8 ADD ESP, 4
004018EB MOV DWORD PTR SS:[EBP-8], EAX ; << salva hash di username
Questa prima parte calcola un hash dello username. Ma questo è utilizzato solo per scrivere il file uedit32.reg, per cui non è interessante per i nostri scopi. Nelle righe seguenti troviamo:
004018F1 MOV EDX, DWORD PTR SS:[EBP-4]
004018F4 SUB EDX, 1B2 ; << Routine da invocare
004018FA PUSH EBX
004018FB PUSH EAX
004018FC CALL Uedit32.00401901 ; << dopo
00401901 POP EAX ;
00401902 ADD EAX, 9
00401905 XCHG DWORD PTR SS:[ESP], EAX ; <<
00401908 JMP EDX ; << bswap stringa estratta da seriale
0040190A ADD ESP, 4
0040190D PUSH EBX ; << stringa estratta (Bswappata)
0040190E MOV EDX, DWORD PTR SS:[EBP-4]
La stringa estratta da seriale (di 32 caratteri) viene dapprima BSWAP-pata (passatemi il termine), eseguendo dei BSWAP a coppie di word, come si può vedere di seguito:
00401709 MOV DWORD PTR SS:[ESP], EBP ;
0040170C PUSH ESP
0040170D POP EBP ;
:
00401712 MOV ECX, 0F ; << ecx := 0F
00401717 MOV EBX, DWORD PTR SS:[EBP-8] ; << stringa da seriale
0040171A MOV EDX, 1C ; << EDX := 1C (28)
0040171F MOV EAX, DWORD PTR DS:[EBX+EDX] ; << ultimi 4 char
<< (e poi si sposta di 2 char alla volta indietro)
00401722 BSWAP EAX ; << BSWAP
00401724 MOV DWORD PTR DS:[EBX+EDX], EAX ; << salva il BSWAP nel posto originale
00401727 SUB EDX, 2
0040172A LOOPD SHORT Uedit32.0040171F
:
00401730 MOV EBP, DWORD PTR SS:[ESP] ;
00401733 ADD ESP, 8
00401736 JMP DWORD PTR SS:[ESP-4] ; torna al "chiamante"
Proseguendo nella routine ci rendiamo conto che la stringa di 32 caratteri è elaborata a gruppi di 8 caratteri, in modo da ricavare quelli che io chiamo match, ovvero singole dword in cui ogni cifra (esadecimale) rappresenta l'indice - nella stringa "X6A3KVMOQ8D2HCPI" (lunga 16 caratteri) del carattere corrente.
| Carattere | Indice |
|---|---|
| X | 0 |
| 6 | 1 |
| A | 2 |
| 3 | 3 |
| K | 4 |
| V | 5 |
| M | 6 |
| O | 7 |
| Q | 8 |
| 8 | 9 |
| D | A |
| 2 | B |
| H | C |
| C | D |
| P | E |
| I | F |
Per capirci, se ad esempio abbiamo gli 8 caratteri AQCX3KVM, otterremo un match pari a 28D03456.
00401917 PUSH EAX
00401918 CALL Uedit32.0040191D ; << linea dopo
0040191D POP EAX ;
0040191E ADD EAX, 9
00401921 XCHG DWORD PTR SS:[ESP], EAX ;
00401924 JMP EDX ; << Match1 - 8 char iniziali
00401926 ADD ESP, 4
00401929 MOV DWORD PTR SS:[EBP-1C], EAX ; << salva primo match
0040192C ADD EBX, 8 ; << prossimi 8 char
0040192F PUSH EBX
00401930 PUSH EAX
00401931 CALL Uedit32.00401936 ; << linea dopo
00401936 POP EAX ;
00401937 ADD EAX, 9
0040193A XCHG DWORD PTR SS:[ESP], EAX ;
0040193D JMP EDX ; Secondo Match
0040193F ADD ESP, 4
00401942 MOV DWORD PTR SS:[EBP-10], EAX ; << salva secondo Match
00401945 ADD EBX, 8 ; << avanza
00401948 PUSH EBX
00401949 PUSH EAX
0040194A CALL Uedit32.0040194F ; << linea dopo
0040194F POP EAX ;
00401950 ADD EAX, 9
00401953 XCHG DWORD PTR SS:[ESP], EAX
00401956 JMP EDX ; << terzoMatch
00401958 ADD ESP, 4
0040195B MOV DWORD PTR SS:[EBP-C], EAX ; << salva terzo match
Dopo aver calcolato i primi 3 match, la routine combina il secondo e il terzo, prima di avanzare e procedere al calcolo del quarto match.
0040195F PUSH EDX ;
00401960 MOV EBX, EBP
00401962 SUB EBX, 10
00401965 PUSH EBX ; << secondoMatch
00401966 MOV EDX, DWORD PTR SS:[EBP-4]
00401969 SUB EDX, 17A
0040196F PUSH EAX
00401970 CALL Uedit32.00401975 ; << linea dopo
00401975 POP EAX ;
00401976 ADD EAX, 9
00401979 XCHG DWORD PTR SS:[ESP], EAX
0040197C JMP EDX ; << Combina Secondo e Terzo Match
0040197E ADD ESP, 4
00401981 POP EDX ;
00401982 POP EBX ;
00401983 ADD EBX, 8 ; << avanza nella stringa estratta
Ecco come sono rielaborati il secondo e il terzo match:
00401741 MOV DWORD PTR SS:[ESP], EBP ;
00401744 PUSH ESP
00401745 POP EBP ;
00401746 PUSH EAX
00401747 PUSH EBX ; <secondoMatch>
00401748 PUSH EDX ;
00401749 MOV EBX, DWORD PTR SS:[EBP+8] ; << ind. secondo match
0040174C MOV EAX, DWORD PTR DS:[EBX+4] ; << terzo match (valore)
0040174F MOV EDX, DWORD PTR DS:[EBX] ; << Secondo Match ( valore )
00401751 XOR EDX, EAX ; << SecondoMatch = SecondoMatch XOR TerzoMatch
00401753 BSWAP EDX ; << bswap risultato (in edx)
00401755 XOR EAX, EDX ; << TerzoMatch = TerzoMatch XOR ( valore_bswappato appena calcolato )
00401757 BSWAP EAX ; << bswap a sua volta
00401759 MOV DWORD PTR DS:[EBX+4], EAX ; << Salva valore di EAX nella posizione del terzoMatch
0040175C MOV DWORD PTR DS:[EBX], EDX ; << Salva EDX nella posizione del secondo Match
0040175E POP EDX ;
0040175F POP EBX ;
00401760 POP EAX ;
00401761 MOV EBP, DWORD PTR SS:[ESP] ;
00401764 ADD ESP, 8
00401767 JMP DWORD PTR SS:[ESP-4] ;
In pratica si XOR-ano i valori e si effettuano due BSWAP. Fatto questo si procede al calcolo e al salvataggio del quarto match.
00401987 PUSH EAX
00401988 CALL Uedit32.0040198D ; << linea successiva
0040198D POP EAX ;
0040198E ADD EAX, 9
00401991 XCHG DWORD PTR SS:[ESP], EAX
00401994 JMP EDX ; << Estrae quartoMatch
00401996 ADD ESP, 4
00401999 MOV DWORD PTR SS:[EBP-18], EAX ; << salva quartoMatch
A questo punto si eseguono tutta una serie di operazioni volte a verificare alcune condizioni.
Si parte controllando il primo e il quarto match:
0040199F SUB EBX, 8A0
004019A5 MOV EBX, DWORD PTR DS:[EBX] ; << BSWAP di 150000 (versione di Uedit)
004019A7 MOV EAX, DWORD PTR SS:[EBP-18] ; << EAX := QuartoMatch
004019AA BSWAP EBX ; << ottiene 150000 ...
004019AC NOT EBX ; << .... in EBX
004019AE PUSH EAX ; << push quartoMatch
004019AF PUSH EBX ; << push 150000
004019B0 MOV EBX, DWORD PTR SS:[EBP-18] ; << quartoMatch
004019B3 MOV EAX, DWORD PTR SS:[EBP-1C] ; << primo match
004019B6 ROR EAX, 0D ; << primoMatch = primoMatch ROR 0D
004019B9 XOR EBX, EAX ; << XOR quarto_match e (primoMatch ROR 0D)
004019BB ROL EAX, 0D ; << rotazione a sinistra ora (ripristina valore ?)
004019BE XOR EBX, EAX ; << nuovo XOR col valore 'non ribaltato'
004019C0 MOV DWORD PTR SS:[EBP-18], EBX ; << salva questo valore in posizione quartoMatch
004019C3 POP EBX ;
004019C4 POP EAX ;
004019C5 XOR EAX, EAX ; << EAX := 0
004019C7 MOV EAX, DWORD PTR SS:[EBP-18] ; << recupera il valore appena calcolato
004019CA AND EAX, F0000000 ; << Esegue AND (ultima cifra esadecimale)
004019CF ROL EAX, 4 ; << rotazione a sinistra di 4 (lo porta a destra a tutto )
004019D2 PUSH EBX ; << 150000
004019D3 PUSH EAX ; <<
004019D4 XCHG EAX, EBX ; << scambio EAX <--> EBX
004019D5 MOV EAX, DWORD PTR SS:[EBP-4]
004019D8 SUB EAX, 822
004019DD OR DWORD PTR DS:[EAX], EBX ; << OR della dword di 'controllo'
; << (su cui si decide la registrazione) deve essere = 1
; nella cifra esadecimale + bassa
La combinazione del primo e del quarto match deve essere tale da risultare, nella sua cifra esadecimale "più alta", pari ad 1 (viene controllato al ritorno di questa call).
Subito dopo ...
004019E2 AND EAX, 0FFFFFFF ; << tutto tranne prima cifra esadecimale
004019E7 MOV DWORD PTR SS:[EBP-18], EAX ; << e lo salva da dove l'ha preso (quarto match)
004019EA POP EAX ;
004019EB POP EBX ;
004019EC MOV EAX, DWORD PTR SS:[EBP-18] ; << riprende cio' che e' in quartoMatch
004019EF ROR EBX, 10 ; << ROR 150000, 10 (otteniamo 15)
004019F2 ROR EAX, 10 ; << ror EAX, 10
004019F5 SUB BX, AX ; << devono essere uguali le WORD + basse ( 00 15 ) !
004019F8 JE SHORT Uedit32.00401A0D ; v
004019FA MOV EBX, DWORD PTR SS:[EBP-4] ; << prepara valore di errore
004019FD SUB EBX, 822
00401A03 XOR EAX, EAX
00401A05 INC EAX
00401A06 SHL EAX, 1D
00401A09 OR DWORD PTR DS:[EBX], EAX
00401A0B JMP SHORT Uedit32.00401A61
... si verifica che la combinazione di primo e quarto match sia pari ( nella word più bassa ) alla versione del programma ( 15 in questo caso ). Inoltre:
00401A10 ROR EBX, 18
00401A13 SUB BL, AL ; << byte + bassi = ?
00401A15 JE SHORT Uedit32.00401A61 ; - lo faccio saltare
:
ci deve essere una corrispondenza anche tra le "parti alte" delle dword.
Infine si scomoda la FPU per l'ultimo check:
00401A64 SUB EBX, 8B4 ; << importante_401004
00401A6A WAIT
00401A6B FINIT
00401A6D FILD QWORD PTR DS:[EBX] ; << 00 C0 69 2A | C9 00 00 00
00401A6F FILD QWORD PTR SS:[EBP-10] ; << secondo e terzo match assieme come qword
; << (secondoMatch nella dword + bassa )
00401A72 MOV EBX, DWORD PTR SS:[EBP-4]
00401A75 SUB EBX, 8A8
00401A7B FILD QWORD PTR DS:[EBX] ; << currSysTime in FileTime
00401A7D FSUB ST(0), ST(1) ; << sottrae quest'ultima con secondo_terzo
00401A7F FDIV ST(0), ST(2) ; << divide per la prima (dovrebbe essere costante)
00401A81 FISTP DWORD PTR SS:[EBP-24] ; << salva il risultato (DWORD)
00401A84 MOV EAX, DWORD PTR SS:[EBP-24] ; << recupera questo valore
00401A87 TEST EAX, 80000000 ; << e lo controlla
00401A8C JE SHORT Uedit32.00401A90
00401A8E NEG EAX
00401A90 CMP EAX, 17C ; << EAX deve essere <= 17C
00401A95 JBE SHORT Uedit32.00401AAC ; DEVE SALTARE
00401A97 MOV EBX, DWORD PTR SS:[EBP-4]
00401A9A SUB EBX, 822
00401AA0 MOV EAX, 1FF
00401AA5 SHL EAX, 10
00401AA8 OR DWORD PTR DS:[EBX], EAX ;
00401AAA JMP SHORT Uedit32.00401ABA
00401AAC ROL EAX, 10 ; << ROL EAX,10 posizioni
00401AAF MOV EBX, DWORD PTR SS:[EBP-4]
00401AB2 SUB EBX, 822 ; << dword di controllo
00401AB8 OR DWORD PTR DS:[EBX], EAX ; << salva il valore nell'indicatore
00401ABA MOV EAX, DWORD PTR SS:[EBP-18] ; << valore in posizione quartoMatch
00401ABD AND EAX, 0F ; << ultima cifra esadecimale
00401AC0 TEST EAX, 8
00401AC5 JNZ SHORT Uedit32.00401AE6 ; << ** NON deve saltare
00401AC7 AND EAX, 7
00401ACA OR AL, 0
00401ACC JE SHORT Uedit32.00401AF7 ; << questo *deve* saltare
00401ACE OR EAX, 20
00401AD1 SHL EAX, 7
00401AD4 AND EAX, 1F80
00401AD9 MOV EBX, DWORD PTR SS:[EBP-4]
00401ADC SUB EBX, 822
00401AE2 OR DWORD PTR DS:[EBX], EAX ; << male
00401AE4 JMP SHORT Uedit32.00401AF7
00401AE6 MOV EBX, DWORD PTR SS:[EBP-4]
00401AE9 SUB EBX, 822
00401AEF XOR EAX, EAX
00401AF1 INC EAX
00401AF2 SHL EAX, 6
00401AF5 OR DWORD PTR DS:[EBX], EAX
00401AF7 POP EDI ;
00401AF8 POP ESI ;
00401AF9 POP EDX ;
00401AFA POP ECX ;
00401AFB POP EBX ;
00401AFC POP EAX ;
00401AFD ADD ESP, 24
00401B00 MOV EBP, DWORD PTR SS:[ESP] ;
00401B03 ADD ESP, 8
00401B06 JMP DWORD PTR SS:[ESP-4] ; << **** torna al chiamante
La sottrazione della data corrente con la combinazione di secondo e terzo match, e la successiva divisione per una costante, deve fornire un risultato minore di 17Ch. Questo valore contribuirà anche al valore dell'indicatore (dword di controllo [401096]), quindi va scelto con cura.
Si noti che in questa routine si modifica il valore della dword di controllo ([401096]) in diversi punti. Alcuni di essi sono codici di errore che saranno testati all'uscita, nella parte conclusiva della routine principale.
Routine di Registrazione – Parte 3: Siamo registrati ?.
Una volta tornati alla routine principale si analizza il valore della dword di controllo (il quale, se ben ricordiamo, è memorizzato in EBX) per verificare che tutto sia regolare. I test eseguiti sono illustrati di seguito.
Sostanzialmente si verifica che la cifra esadecimale più bassa (dopo lo shift right) della dword di controllo sia pari ad uno e che il valore non corrisponda ad uno dei codici di errore.
00440F20 CALL <Uedit32.divisione_qword> ; << seconda divisione quadword
(non ci interessa)
00440F25 MOV ESI, EBX ; << la call precedente NON modifica EBX
00440F27 SHR ESI, 10
00440F2A MOV ECX, EBX ; << nuova copia di EBX
00440F2C AND CL, 0F ; << ultimo char esadecimale
00440F2F AND ESI, 1FF ; << and esi, 1FF
00440F35 CMP CL, 1 ; << CL deve essere = 1
00440F38 JE SHORT Uedit32.00440F3F
00440F3A MOV BYTE PTR SS:[ESP+17], 0 ; << :(
00440F3F TEST EBX, 4000 ; << test
00440F45 JE SHORT Uedit32.00440F51
00440F47 TEST BL, 40
00440F4A JNZ SHORT Uedit32.00440F51
00440F4C MOV BYTE PTR SS:[ESP+17], 0 ; << :(
00440F51 TEST EBX, 20000000 ; << test EBX 20.000.000
00440F57 JE SHORT Uedit32.00440F5E
00440F59 MOV BYTE PTR SS:[ESP+17], 0 ; << :(
00440F5E PUSH ECX ; << push valore 'iniziale' della dword di controllo
:
:
00440FAC CMP BYTE PTR SS:[ESP+17], 0 ; << byte di registrazione
00440FB1 MOVZX EDX, AL
00440FB4 MOV DWORD PTR DS:[D05598], EDX
00440FBA JE SHORT Uedit32.00440FF2
00440FBC CMP ESI, 17C ; << *** DEVE SALTARE PER NON MOSTRARE LA NAG
; (dipende dal valore di "controllo")
00440FC2 JB SHORT Uedit32.00440FF2 ; v
00440FC4 TEST BL, 40
00440FC7 JNZ SHORT Uedit32.00440FF2
00440FC9 MOV EAX, DWORD PTR SS:[ESP+18]
00440FCD MOV ECX, DWORD PTR SS:[ESP+24]
00440FD1 MOV EDX, DWORD PTR SS:[ESP+28]
00440FD5 MOV DWORD PTR DS:[EAX], 1
00440FDB MOV EAX, DWORD PTR SS:[ESP+1C]
00440FDF MOV DWORD PTR DS:[EAX], ECX
00440FE1 MOV ECX, DWORD PTR SS:[ESP+2C]
00440FE5 MOV DWORD PTR DS:[EAX+4], EDX
00440FE8 MOV EDX, DWORD PTR SS:[ESP+30]
00440FEC MOV DWORD PTR DS:[EAX+8], ECX
00440FEF MOV DWORD PTR DS:[EAX+C], EDX
00440FF2 CMP DWORD PTR SS:[ESP+6C], 10 ; << lungh. del seriale == 10h ?
00440FF7 JB SHORT Uedit32.00441006
00440FF9 MOV EAX, DWORD PTR SS:[ESP+58] ; << stringa estratta da seriale
00440FFD PUSH EAX ; << push stringa estratta
00440FFE CALL <Uedit32.FreeString>
00441003 ADD ESP, 4
00441006 MOV AL, BYTE PTR SS:[ESP+17] ; << stato di registrazione
0044100A JMP SHORT Uedit32.0044100E
0044100C XOR AL, AL
0044100E MOV ECX, DWORD PTR SS:[ESP+2B0]
:
:
00441035 RETN
Si noti che lo stato di registrazione ( il byte in posizione [ESP+17] ) è di default pari ad 1 e viene posto a 0 se qualche test non è andato a buon fine. In linea 00441006 lo stato di registrazione viene memorizzato in AL, concludendo così la routine e, con essa, la nostra analisi.
Reversiamo il reversabile.
Per concludere, riassumo rapidamente l'idea risolutiva per ottenere un seriale corretto, rimandando al codice allegato tutti i dettagli del caso. La parte più complessa è stata la costruzione di match che potessero superare tutti i controlli. In pratica, ho scelto il primo match in maniera fissa ed ho calcolato gli altri a partire dalla data corrente e invertendo le operazioni:
{
int periodo2 = 0x10; // POSSO SCEGLIERE: 0 <= periodo2 <= 17C
int valore_minore_17C = periodo2;
// (data_corrente - secondo_terzo) / intervallo = valore_minore_17C
// -> l'unica incognita è secondo_terzo. ==>
// secondo_terzo = ( data_corrente - valore_minore_17C * intervallo )
__int64 intervallo = 0x000000C92A69C000; // costante
__int64 data_corrente = DataCorrenteInFileTime();
__int64 intermedio = ( intervallo * valore_minore_17C );
// e' una QWORD
// ( data_corrente - secondo_terzo ) \ intervallo = valore_minore_17C;
__int64 secondo_terzo = data_corrente - intermedio;
int match3_temp = secondo_terzo / 0x100000000; // Recupera DWORD alta
int match2_temp = secondo_terzo & 0xFFFFFFFF; // Recupera DWORD bassa
// Per la v.15
// 150000 = ( match4 XOR (match1 ROR 0D) ) XOR match1;
// -> SE SCELGO match1 = valore random in "X6A3KVMOQ8D2HCPI" ==> i.e match1 = 14232417 ==> 6KA30-AK6O0
// POSSO OTTENERE match4 (invertendo questa formula).
this->match1 = 0x14232417;
int x1 = ( match1 ^ 0x10150000 ); // Uso 10150000 invece di 00150000 perchè
// la cifra esadecimale + alta viene portata
// a diventare la cifra + bassa e viene posta
// in OR con [00401096] in linea 004019DD.
// In questo modo mi assicuro di
// avere ( CL = 1 ) all'uscita dal controllo
// - e quindi - ottenere il prog. registrato :)
int x2 = match1;
__asm ROR x2, 0x0D;
// Ricavo dunque match4
this->match4 = x1 ^ x2; // XOR
// Posti
int m2 = 0; // BSWAP(match2_temp);
int m3 = 0; // BSWAP(match3_temp);
/*
// Viene eseguito (con match2_temp e match3_temp che ora sono noti, per cui sono noti anche m2 ed m3):
// --------------------------------------
match2_temp = BSWAP( match2 XOR match3 ) ==> match2 XOR match3 = BSWAP(match2_temp);
match3_temp = BSWAP( match3 XOR match2_temp ) ==> match3 XOR match2_temp = BSWAP(match3_temp);
// Sistema :
1) match2 XOR match3 = m2; ==> match3 = m2 XOR match2;
2) match3 XOR match2_temp = m3;
2) ( m2 XOR match2 ) XOR match2_temp = m3; ==> m2 XOR match2 = m3 XOR match2_temp;
// Risultato ( ^ = XOR )
//----------------------------------------
match2 = ( m3 ^ match2_temp) ^ m2;
match3 = m2 ^ match2;
*/
// Modo sporco, ma veloce, per eseguire i BSWAP di match2_temp e match3_temp
__asm
{
PUSH EAX // Salva registro EAX
// Esegue primo BSWAP
MOV EAX, match2_temp
BSWAP EAX
MOV m2, EAX
// Esegue secondo BSWAP
MOV EAX, match3_temp
BSWAP EAX
MOV m3,EAX
POP EAX
}
// Ottiene i due match mancanti
this->match2 = ( m3 ^ match2_temp) ^ m2;
this->match3 = m2 ^ match2;
}
Una volta in possesso dei match, ho ricavato, simulando i BSWAP, gli indici nella stringa "X6A3KVMOQ8D2HCPI", e dunque i corrispondenti caratteri.
{
// Estrae gli indici da utilizzare per l'estrazione del seriale.
// Gli indici sono le posizioni 0-based nella stringa 'X6A3KVMOQ8D2HCPI'.
// Probabilmente si poteva semplificare la gestione dei BSWAP ... ma non ho trovato rapidamente il modo ;)
int i = 0, j = 0;
int indici[8];
// Recupera gli indici da match1 (ultimi 6)
getHexDigits( indici, this->match1, 2, 7 );
// Copia nell'array interno
for( j = 2; j < 8; j ++ )
this->indici[i++] = indici[j];
// Recupera gli indici da match2
getHexDigits( indici, this->match2, 0, 7 );
// Copia nell'array interno
for( j = 0; j < 8; j ++ )
this->indici[i++] = indici[j];
// Recupera gli indici da match3
getHexDigits( indici, this->match3, 0, 7 );
// Copia nell'array interno
for( j = 0; j < 8; j ++ )
this->indici[i++] = indici[j];
// Recupera gli indici da match4
getHexDigits( indici, this->match4, 0, 7 );
// Copia nell'array interno
for( j = 0; j < 8; j ++ )
this->indici[i++] = indici[j];
// Recupera gli indici da match1 ( primi 2 )
getHexDigits( indici, this->match1, 0, 1 );
// Copia nell'array interno
for( j = 0; j < 2; j ++ )
this->indici[i++] = indici[j];
}
In seguito ho riprodotto la generazione del vettore di valori sulla base del nome utente, con il conseguente recupero dei 32 valori di lookup (memorizzati nel vettore di interi 'codici'); ho eseguito la somma del resto e la moltiplicazione per 10 ... in modo da ottenere così le singole lettere del seriale. Ho, per concludere, aggiunto un carattere qualsiasi come quinto elemento di ogni sottogruppo.
{
int idx = 0;
int idx2 = 0;
int sz = 32; // lunghezza degli indici
int eax = 0;
for ( int i = 0; i < sz; i++ )
{
// Recupera i-esimo valore del vettore calcolato da [hash] userName
eax = this->codici[i];
// Chiama la routine per il calcolo del carattere 'successivo' del seriale:
char char_seriale = getCharSeriale( eax, idx2 );
// Memorizza risultato
serialBuffer[idx++] = char_seriale;
idx2++;
// Aggiunge un trattino ogni 4 ( con un carattere qualunque ... per ora infatti
// non e' controllato dall'algoritmo di verifica del programma).
if ( idx2 % 4 == 0 )
{
serialBuffer[idx++] = 'A';
serialBuffer[idx++] = '-';
}
} // fine for
// Conclude la stringa
serialBuffer[idx - 1] = '\0';
}
Una volta ottenuto il nostro bel seriale, possiamo verificare se abbiamo fatto un buon lavoro. Beh, ... la finestra di about sembra darci ragione :D
Una volta verificato che tutto funzioni a dovere, non dimentichiamo di cancellare il file uedit32.reg dalla cartella %userprofile%\Application Data\IDMComp\UltraEdit" per ripristinare lo stato trial del programma.
Note Finali
Mammamia. Non credevo che scrivere un tutorial potesse essere così complesso. Per fortuna sono giunto vivo alla fine e, spero, che lo siate anche voi.
Da tempo frequento la UIC ma non ho mai avuto il coraggio di contribuire dal momento che, spesso, i miei successi si limitavano a soluzioni banalissime per i più. Sono convinto invece, questa volta, di aver realizzato qualcosa degna di nota e, perchè no, in grado di dare una mano a qualche niubbo come me.
Mi aspetto commenti, critiche, segnalazioni su imprecisioni o cazzate ... insomma tutto ciò che possa migliorare queste pagine :D.
Ringraziamenti
Voglio ringraziare innanzitutto la UIC per essere un posto unico per imparare l'arte del reversing e per condividere qualunque tipo di emozione. Ringrazio yami per avermi aiutato a rendere leggibile questo documento, albesp77 per avermi dato la sua "opinione professionale" al riguardo, e tutti voi che leggerete questo tutorial.
Un ringraziamento speciale va alla mia prima e straordinaria insegnante di RCE, ovvero lena151 grazie alla quale (i suoi video tutorial sono eccezionali) sono entrato in questo mondo splendido e complicatissimo (almeno per quanto mi riguarda).
Ringrazio ancora tutti i grandi della scena che, con i loro innumerevoli tutorial, articoli e consigli, consentono anche a noi niubbi di fare dei passi avanti.
Pre-Disclaimer
L'autore di queste pagine declina ogni responsabilità relativa al loro contenuto e all'impiego delle informazioni in esso riportate. Il lettore si assume piena responsabilità per un loro eventuale uso inappropriato e/o per qualunque utilizzo intenda farne. Il tutorial è rilasciato "così com'è" senza garanzie di alcun tipo. Tutte le informazioni sono qui riportate solo per fini educativi e non vanno assolutamente adoperate per scopi illegali.
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 malevoli 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.
