integrato ;)
EffeTech HTTP sniffer reverse engineering 1°parte
Analisi di algoritmi, keygenning e cracking

 
Introduzione e disclaimer
 
Questo articolo non è una guida completa al reverse engineering, è impossibile, sarà esclusivamente uno studio dell'algoritmo di validazione del codice seriale di un determinato programma, per capirne le vulnerabilità e come è possibile aggirarle.
Sono necessari alcuni requisiti preliminari per poter capire questo articolo come la conoscenza del linguaggio assembler, l'utilizzo di un debugger, una minima conscenza delle API di windows e la capacità di programmazione in linguaggi di alto livello.
Ricordo comunque che violare la licenza di un programma è reato, per continuare a utilizzare software al di fuori del periodo di trial concesso occorre acquistare regolarmente una licenza.

 
Software reversing 1 passo: Comprensione
 
Generalmente nello sviluppo completo di una applicazione si usano quasi interamente linguaggi di programmazione ad alto livello, di conseguenza quindi dovrebbe essere ovvio capire che dopo la compilazione nessuno che non disponga del codice sorgente di tale programma sarà mai più in grado di leggerne il codice ad alto livello. Quello che tutti possono fare però è, a patto di una buona conoscenza di tale linguaggio, leggerne il codice assembly istruzione per istruzione e cercare di "risalire" manualmente ad un codice di più alto livello meglio comprensibile.
Ovviamente questo procedimento è tutt'altro che facile e immediato, in quanto a volte anche solo una istruzione di alto livello viene codificata in linguaggio macchina con decine di istruzioni assembly, e da questo si intuisce anche perchè sarebbe praticamente impossibile reversare un'intera applicazione.
Quello che verrà trattato in questo articolo è l'analisi di un algoritmo di protezione di un particolare programma, capendone il funzionamento, riportandolo a codice di alto livello e studiandone le vulnerabilità. Vi pare chiaro che se dopo aver letto questo primo passo vi siete accorti che non sapete minimamente cosa sia il linguaggio assembler, un algoritmo di protezione, o non avete ancora idea di come poter leggere il codice di una applicazione compilata potete già cambiare pagina.

 
Software reversing 2 passo: Analisi target
 
Il programma che verrà analizzato in questo articolo è EffeTech HTTP sniffer v 4.0.0.0, un packet sniffer locale per le richieste HTTP che ha un periodo di trial di 15 giorni. Il programma è disponibile per il download sul o tra le.
Dopo aver installato il programma e aperto la prima cosa che compare all'avvio è la segunte schermata:

Cioè la richiesta dell'introduzione di un nome utente e un codice seriale per la registrazione del prodotto al di fuori del periodo di prova. La protezione che andremo a studiare in questo articolo sarà dunque questa: l'algoritmo di validazione della combinazione user/serial per la registrazione del programma (in licenza Non-Commercial e Commercial).

 
Software reversing 3 passo: Partiamo col debugger e posizioniamoci
 
Il metodo più veloce per localizzare la parte di algoritmo che ci interessa all'interno delle migliaia di istruzioni in codice assembly del programma la maggior parte delle volte è tramite l'utilizzo di un debugger. Ognuno può utilizzare il debugger che preferisce, io personalmente come la maggior parte delle persone utilizzo il comodo e pratico . Con un minimo di conoscenza della programmazione a oggetti di windows e delle api la prima cosa che si pensa è che il programma leggerà i dati da quelle due textbox tramite funzioni come GetDlgItemText,GetWindowText o magari addirittura SendMessage con parametro WM_GETTEXT (volendo si potrebbe leggere la tabella delle funzioni importate, ma si fa prima a "manoni"). Il primo passo da fare quindi è lanciare il debugger e attaccare il processo EHSniffer.exe o lanciare l'applicazione da dentro il debugger (come si preferisce) e posizionare breakpoint sulla prima di queste API (ollydbg: Alt+F1 e scrivere bp GetDlgItemTextA, A per la versione Ansi). Il debugger non fermerà l'esecuzione del processo dopo la pressione del tasto Register, quindi riproviamo posizionando un breakpoint su GetWindowText (bp GetWiundowTextA) e noteremo che debugger fermerà l'esecuzione del processo all'interno della user32.dll la libreria con questa api. Usciamo da tale libreria con Ctrl+F9 e ci troveremo all'indirizzo 0x004438C7 subito dopo la sua chiamata.
E' plausibile pensare che ci troviamo non troppo lontani da un prossimo utilizzo dei dati che il programma sta leggendo, comuqnue ricordo che una delle parti più difficili del reversing, anche se leggendo un articolo già fatto non si nota, è trovare la posizione esatta dell'inizio e della fine dell'algoritmo da studiare, senza perdere ore di lavoro addentrandosi in sotto-sotto-...-sotto routine inutili per i nostri scopi e questo lo si impara solo provando, riprovando e facendo esperienza.
Comunque quello che ci troveremo davanti se abbiamo fatto tutto bene è il seguente spezzone di codice:
  1. 004438AB PUSH ESI ; /hWnd
  2. 004438AC CALL DWORD PTR DS:[<&USER32.GetWindowTextLengthA>] ; \GetWindowTextLengthA
  3. 004438B2 LEA ECX,DWORD PTR DS:[EAX+1]
  4. 004438B5 PUSH ECX
  5. 004438B6 MOV ECX,DWORD PTR SS:[EBP+10]
  6. 004438B9 PUSH EAX
  7. 004438BA CALL EHSniffe.saveStrLen ; Salvo in memoria la lng delle str
  8. 004438BF PUSH EAX ; |Buffer
  9. 004438C0 PUSH ESI ; |hWnd
  10. 004438C1 CALL DWORD PTR DS:[<&USER32.GetWindowTextA>] ; \GetWindowTextA
  11. 004438C7 MOV ECX,DWORD PTR SS:[EBP+10]
Ci troviamo comunque ancora all'interno di varie sotto-call del programma, quello che dobbiamo fare e arrivare al punto giusto del codice. Continuando il debug passo passo si uscirà da questa call (che chiamerò readRoutine) che non fa altro che chiamare le API per la lettura della lunghezza dell'input, salvarla in memoria, leggere la stringa e salvarla in memoria il tutto due volte (per entrambe le textbox). Alla fine ci troveremo all'indirizzo 0x00415308.
  1. 004152DB PUSH EAX ; /Arg3
  2. 004152DC PUSH 2 ; |Arg2 = 00000002
  3. 004152DE PUSH EDI ; |Arg1
  4. 004152DF CALL <EHSniffe.doesntCare> ; \doesntCare
  5. 004152E4 LEA ECX,DWORD PTR DS:[ESI+C0]
  6. 004152EA PUSH ECX ; /Arg3
  7. 004152EB PUSH 40C ; |Arg2 = 0000040C
  8. 004152F0 PUSH EDI ; |Arg1
  9. 004152F1 CALL <EHSniffe.readRoutine> ; \readRoutine
  10. 004152F6 LEA EDX,DWORD PTR DS:[ESI+C4]
  11. 004152FC PUSH EDX ; /Arg3
  12. 004152FD PUSH 40D ; |Arg2 = 0000040D
  13. 00415302 PUSH EDI ; |Arg1
  14. 00415303 CALL <EHSniffe.readRoutine> ; \readRoutine
  15. 00415308 ADD ESI,0C8
  16. 0041530E PUSH ESI ; /Arg3
  17. 0041530F PUSH 420 ; |Arg2 = 00000420
  18. 00415314 PUSH EDI ; |Arg1
  19. 00415315 CALL <EHSniffe.doesntCare> ; \doesntCare
  20. 0041531A POP EDI
  21. 0041531B POP ESI
  22. 0041531C RETN 4
Evitando di debuggare due routine inutili ed uscendo da due sotto-routine in cui siamo innestati ci troveremo finalmente all'inizio del codice che ci interessa, ovvere all'indirizzo 0x0041548E. Adesso inizia il compito più complesso: dobbiamo analizzare e comprendere nei minimi dettagli il codice che abbiamo davanti utilizzando solo il debbugger.

 
Software reversing 4 passo: Analisi algoritmo
 
Solo l'analisi del codice assembly richiede parecchie ore di debug e di varie prove che ognuno dovrebbe provare a fare, ma ovviamente riporto il codice già commentato e spiegato alla fine di questa analisi.
Dunque il primo controllo che l'algoritmo di validazione del seriale fa è il seguente:
  1. 00415489 CALL EHSniffe.readTextBox ; Sbuchiamo da qui dopo le varie readRoutine
  2. 0041548E MOV EAX,DWORD PTR SS:[EBP+C4] ; EAX = serial
  3. 00415494 CMP DWORD PTR DS:[EAX-C],12 ; if (strlen(serial) == 0x12) ?
  4. 00415498 LEA EDI,DWORD PTR SS:[EBP+C4] ; EDI = serial
  5. 0041549E JNZ <EHSniffe.WrongSerial> ; strlen(serial) != 0x12 -> errore
Dunque subito capiamo che il programma si aspetta di leggere un seriale di 0x12 (18) caratteri se no subito verrà stampato il messaggio di errore "Wrong user or Serial Number!". Tradotto in un linguaggio ad alto livello (C) questo controllo diventa:
  1. if (strlen(serial) != 18)
  2. printf("Serial errato\n");
Ora, introdotto un seriale di 18 caratteri, proseguendo con l'analisi del codice ci troveremo di fronte al primo ciclo:
  1. 004154A4 MOV EBX,DWORD PTR SS:[ESP+10]
  2. 004154A8 MOV ESI,DWORD PTR SS:[ESP+10]
  3. 004154AC MOV EAX,DWORD PTR SS:[ESP+10] ; EAX = ESI = EBX = 'x' (non importa)
  4. 004154B0 XOR EDX,EDX ; EDX = 0 - Contatore ciclo
  5. 004154B2 TEST EDX,EDX ; ! Inizio ciclo !
  6. 004154B4 JL EHSniffe.004155A6 ; Salta se minore (la prima iter non salta)
  7. 004154BA MOV ECX,DWORD PTR DS:[EDI] ; ECX = serial
  8. 004154BC CMP EDX,DWORD PTR DS:[ECX-C] ; if (EDX > strlen(serial))
  9. 004154BF JG EHSniffe.inputError ; Si? Errore (Si introducono più di 0x12 caratteri)
  10. 004154C5 TEST EDX,EDX ; ---------------------------.
  11. 004154C7 MOV CL,BYTE PTR DS:[ECX+EDX] ; CL = serial[EDX] |
  12. 004154CA JNZ SHORT EHSniffe._JUMP_01 ; Salta sempre tranne alla 1 iter (EDX != 0)
  13. 004154CC MOVSX EBX,CL ; Arrivo qui solo a EDX == 0, EBX = serial[0]
  14. 004154CF JMP SHORT <EHSniffe.IncCont&LOOP> ; Ricomincio il ciclo
  15. _JUMP_01 CMP EDX,1 ; if (EDX == 1) (siamo a serial[1])
  16. 004154D4 JNZ SHORT EHSniffe._JUMP_02 ; EDX != 1, prossimo controllo
  17. 004154D6 MOVSX EAX,CL ; EAX = serial[1]
  18. 004154D9 MOV ESI,EAX ; ESI = serial[1]
  19. 004154DB JMP SHORT <EHSniffe.IncCont&LOOP> ; Ricomincio il ciclo
  20. _JUMP_02 CMP EDX,3 ; if (EDX == 3) (siamo a serial[3])
  21. 004154E0 JE SHORT EHSniffe._JUMP_03 ; EDX == 3, prossimo controllo
  22. 004154E2 CMP EDX,6 ; if (EDX == 6) (siamo a serial[6])
  23. 004154E5 JNZ SHORT EHSniffe._JUMP_04 ; EDX != 6, prossimo controllo
  24. 004154E7 MOVSX EAX,CL ; EAX = serial[6]
  25. 004154EA MOV ESI,EAX ; ESI = serial[6]
  26. 004154EC JMP SHORT <EHSniffe.IncCont&LOOP> ; Ricomincio il ciclo
  27. _JUMP_04 CMP EDX,0A ; if (EDX == 0x0A) (siamo a serial[10])
  28. 004154F1 JNZ SHORT EHSniffe._JUMP_05 ; EDX != 0x0A, prossimo controllo
  29. 004154F3 MOVSX EAX,CL ; EAX = serial[10]
  30. 004154F6 MOV DWORD PTR SS:[ESP+10],EAX ; Salvo nelloo stack serial[10]
  31. 004154FA JMP SHORT <EHSniffe.IncCont&LOOP> ; Ricomincio il ciclo
  32. _JUMP_05 CMP EDX,0E ; if (EDX == 0x0E) (siamo a serial[14]))
  33. 004154FF JNZ SHORT EHSniffe._JUMP_06 ; EDX != 0x0E, prossimo controllo
  34. 00415501 SUB EBX,50 ; EBX -= 0x50 (serial[0] - 0x50)
  35. 00415504 JMP SHORT EHSniffe._JUMP_07 ; Forzo il salto
  36. _JUMP_06 CMP EDX,12 ; if (EDX == 0x12) controllo INUTILE
  37. 00415509 JE SHORT EHSniffe._JUMP_07 ; EDX == 0x12, forzo il salto
  38. 0041550B CMP EDX,8 ; if (EDX == 8) (siamo a serial[8]))
  39. 0041550E JNZ SHORT EHSniffe._JUMP_08 ; EDX != 0x0A, prossimo controllo
  40. _JUMP_03 MOVSX EAX,CL ; Arrivo qui solo con EDX == 3 o EDX == 8
  41. 00415513 MOV ESI,EAX ; ESI = serial[ESI]
  42. 00415515 JMP SHORT <EHSniffe.IncCont&LOOP> ; Ricomincio il ciclo
  43. _JUMP_08 CMP EDX,0F ; if (EDX == 0x0F) (siamo a serial[15]))
  44. 0041551A JNZ SHORT <EHSniffe.IncCont&LOOP> ; EDX != 0x0E, ricomincio il ciclo
  45. _JUMP_07 MOVSX EAX,CL ; Arrivo qui solo con EDX == 0x0E o EDX == 0x12
  46. 0041551F <>INC EDX ; Incremento contatore
  47. 00415520 CMP EDX,12 ; Finita la stringa?
  48. 00415523 JL SHORT EHSniffe.004154B2 ; No, rinizio ciclo
A parte uno strano e inutile controllo che non si avvererà mai a metà ciclo (EDX == 0x12) il funzionamento generale di questa parte di codice è abbastanza facile. All'uscita dal ciclo avremo salvati questi valori:

Stack [ESP+10] = serial[10]
EAX = serial[15]
ESI = serial[8]
EBX = serial[0] - 0x50

Grazie ai quali possiamo capire il secondo controllo che il programma fa:
  1. 00415525 SUB EAX,DWORD PTR SS:[ESP+10] ; EAX = serial[15] - serial[11]
  2. 00415529 SUB EAX,ESI ; EAX = EAX - serial[8]
  3. 0041552B ADD EAX,EBX ; EAX = EAX + [serial[0] - 0x50]
  4. 0041552D JNZ <EHSniffe.WrongSerial> ; if (EAX != 0) -> errore
Il che tradotto in linguaggio C diventa banalmente:
  1. if ((serial[15] - serial[10] - serial[8]) + (serial[0] - 0x50) != 0)
  2. printf("Serial errato\n");
Una piccola nota: Come si vede tutto il codice esaminato era sviluppabile evitando un ciclo in quanto i vari offset controllati (0x1 0x06 0x0E...) sono statici e non variano in base a dei calcoli. E' probabile quindi che questo spezzone di codice era stato complicato apposta per confondere chiunque avesse cercato di reversare questo algoritmo.
Fermiamoci un attimo a fare il punto della situazione: per adesso il nome inserito non è stato minimante utilizzato dal codice, ma per noi è meglio cosi, ora per poter continuare a debuggare senza visualizzare il messaggio di errore occorre costruirsi un seriale "provvisorio" che sia conforme anche a questo secondo controllo. Il primo che ho calcolato è stato: Pxxxxxxx9xAxxxxzxx, dove si può verificare che:

serial[0] = 'P' = 0x50;
serial[8] = '9' = 0x39;
serial[10] = 'A' = 0x41;
serial[15] = 'z' = 0x7A;
(serial[15] - serial[10] - serial[8]) + (serial[0] - 0x50) = (0x7A - 0x41 - 0x39) + (0x50 - 0x50) = 0

Eseguendo da capo l'applicazione e inserendo questo codice verrà stampato lo stesso il messaggio di errore, significa quindi che questo non è il solo controllo effettuato (ci stupiremmo del contrario). Proseguiamo dunque con l'analisi del codice dal punto in cui ci eravamo fermati:
  1. 00415533 PUSH EDI ; Passo il seriale
  2. 00415534 MOV ECX,EHSniffe.004736C8
  3. 00415539 CALL <EHSniffe.CheckSerial> ; call CheckSerial
  4. 0041553E TEST EAX,EAX ; EAX torna dalla CheckSerial
  5. 00415540 JE <EHSniffe.WrongSerial> ; if (EAX == 0) -> errore
  6. 00415546 MOV ECX,DWORD PTR DS:[EDI] ; Se no registro il programma
  7. 00415548 PUSH ECX ; /Arg3
  8. 00415549 PUSH EHSniffe.0045C378 ; |Arg2 = 0045C378 ASCII "Reg"
  9. 0041554E PUSH EHSniffe.0045C378 ; |Arg1 = 0045C378 ASCII "Reg"
  10. 00415553 MOV ECX,EHSniffe.004735F8 ; |
  11. 00415558 CALL EHSniffe.00443966 ; \EHSniffe.00443966
  12. 0041555D MOV EDX,DWORD PTR SS:[EBP+C0]
  13. 00415563 LEA ESI,DWORD PTR SS:[EBP+C0]
  14. 00415569 PUSH EDX ; /Arg3
  15. 0041556A PUSH EHSniffe.0045C370 ; |Arg2 = 0045C370 ASCII "Name"
  16. 0041556F PUSH EHSniffe.0045C378 ; |Arg1 = 0045C378 ASCII "Reg"
  17. 00415574 MOV ECX,EHSniffe.004735F8 ; |
  18. 00415579 CALL EHSniffe.00443966 ; \EHSniffe.00443966
Come si nota viene pushato il seriale, viene chiamata una routine e al ritorno dalla chiamata si controlla il valore assunto da EAX, se EAX == 0 viene chiamata la solita funzione di errore, se no si prosegue con alcune istruzioni che fanno pensare a una registrazione del seriale nel registro di sistema (proprio così infatti).
Appurato ciò abbiamo due strade davanti a noi adesso: la via più semplice, cambiare con un editor esadecimale all'indirizzo 0x00415540 il JE in un JNE in modo da rendere validi tutti i seriali che rispecchino solo fino alla 2 caratteristica, o andare avanti a reversare e vedere cosa altro troveremo.


Script Execution Time: 0.127275 seconds - Visite: 640648
Copyright © 2007-2017 Suondmao v0.1.5-1