- Messaggi
- 2,332
- Reazioni
- 1,928
- Punteggio
- 134
Crackme v4. Marquire
autore Marquire
Siamo giunti all'ultimo mese dell'anno, e quasi un anno fa ho pubblicato il primo CrackMe qui sul forum. Da allora ne sono seguiti altri tre, e questo è il quinto.
Chi fosse interessato, può trovare i precedenti all'interno del topic che riepiloga le Guide degli Utenti.
Ho deciso di utilizzare qualche immagine in più rispetto alle altre volte, specie nelle prime fasi; spero sia più semplice da seguire così da comprendere meglio alcuni passaggi che faccio.
Questa volta inizio con una premessa: ho risolto il CrackMe sfruttando un "bug", ma nulla a che fare con il software, bensì con la costruzione del testo:
This is the successor of CrackMe_V3_Marquire with an increased 'security' for the hidden key.
The goal would be to find the right key.
Non c'è specificato un formato della chiave, e debuggandolo/reversandolo mi sono reso conto che in realtà... the right key, non è solo una. Quindi ho sollevato la questione e mi è stato risposto che la soluzione che proponevo sarebbe stata comunque accettata e ritenuta corretta; tuttavia la premessa per dirvi che è una soluzione "not intended".
E' stato davvero interessante da reversare, quindi non mi perdo in ulteriori inutili chiacchiere... e iniziamo!
1. Analisi statica di base
Da un primo controllo utilizzando EXEInfo PE, non risulta compresso, e si vedono anche alcuni dei possibili compilatori:Anche dalle funzioni utilizzate non sembra esserci nulla di particolarmente "allarmante", quindi decido di continuare, e iniziare subito ad eseguirlo.
2. La prima esecuzione
Lanciando il programma la schermata che ci si trova davanti è quella mostrata di seguito:A questo punto digito una password a caso, premo Invio e... il programma si chiude! E' una brutta notizia, in quanto a volte, specie su applicazioni semplici, si riesce a risalire alla parte più interessante proprio grazie a questo tipo di messaggi.
A questo punto decido di proseguire con il debugger, per capire cosa accade...
3. Esecuzione con debugger
Aperto il programma in x32dbg, sapendo le librerie che importa e conoscendo come avviene l'input, la prima cosa che faccio è andare nella finestra che riguarda i Simboli, dove si trovano le DLL, e selezionare MSVCRT per vedere le relative funzioni importate:la prima funzione che cerco è "getch", che infatti compare in elenco:
doppio click su di essa, e ci si trova nel corpo della funzione, in MSVCRT.DLL. Ho settato due breackpoint (abbreviato come sempre in bp, da ora in avanti) ad inizio e a fine funzione:
Ne ho settati due per il seguente motivo: con il primo verifico se ho settato il bp corretto, in quanto deve essere raggiunto prima della richiesta di input da tastiera; con il secondo si può vedere l'indirizzo di ritorno, ovvero il chiamante della funzione (in realtà una volta raggiunta la RET è sufficiente "steppare" oltre, premendo F8, e in automatico ci si ritrova sulla funzione chiamante, e non è l'unico modo per arrivarci).
Premendo F9 qualche volta, ci si ritrova al primo bp; premendo di nuovo viene richiesto l'input. Anche qui, premendo F9 si raggiunge il bp successivo, quello sulla RET:
in basso a dx è possibile vedere sullo stack l'indirizzo di ritorno al quale, premendo F8, si salta (0x402186).
A questo punto ci troviamo nella funzione che richiede l'input. Setto subito un bp all'ingresso della funzione, sulla PUSH:
Da questo momento riporterò spesso il codice, poichè penso sia più chiaro rispetto alle immagini (così posso riprendere le singole parti nel proseguo dell'articolo).
Iniziamo con la prima parte visibile in immagine:
Codice:
00402130 | 55 | push ebp |
00402131 | 89E5 | mov ebp,esp |
00402133 | 57 | push edi | edi:EntryPoint
00402134 | 56 | push esi |
00402135 | 53 | push ebx |
00402136 | 31F6 | xor esi,esi |
00402138 | 83E4 F0 | and esp,FFFFFFF0 |
0040213B | 81EC A0010000 | sub esp,1A0 |
00402141 | 8D5C24 1C | lea ebx,dword ptr ss:[esp+1C] |
00402145 | E8 C6FCFFFF | call crackme_v4_marquire.401E10 |
0040214A | 891C24 | mov dword ptr ss:[esp],ebx |
0040214D | C74424 1C 03000000 | mov dword ptr ss:[esp+1C],3 |
00402155 | E8 46F2FFFF | call crackme_v4_marquire.4013A0 |
0040215A | EB 25 | jmp crackme_v4_marquire.402181 |
0040215C | 8D7426 00 | lea esi,dword ptr ds:[esi] |
00402160 | 8D56 03 | lea edx,dword ptr ds:[esi+3] |
00402163 | 3215 00004000 | xor dl,byte ptr ds:[400000] |
00402169 | 83C6 01 | add esi,1 |
0040216C | 31D0 | xor eax,edx |
0040216E | 888434 A1000000 | mov byte ptr ss:[esp+esi+A1],al |
00402175 | C70424 2A000000 | mov dword ptr ss:[esp],2A | 2A:'*'
0040217C | E8 07FFFFFF | call <JMP.&putchar> |
00402181 | E8 0AFFFFFF | call <JMP.&_getch> |
00402186 | 3C 0D | cmp al,D | D:'\r'
00402188 | 888434 A2000000 | mov byte ptr ss:[esp+esi+A2],al |
0040218F | 75 CF | jne crackme_v4_marquire.402160 |
Nota: mi sembra superfluo dirlo, ma è solo per chiarezza: la prima colonna è l'indirizzo virtuale, la seconda è il codice macchina dell'istruzione, la terza è il codice che più ci interessa (la quarta sono stringhe/commenti).
Ho riportato apposta gli indirizzi (come faccio sempre), così si possono seguire i salti guardando l'assembly. Quello che è un ciclo a tuttii gli effetti lo si vede subito, guardando il JNE a fine blocco. A prima vista come ciclo pare un while, in quanto il JMP all'indirizzo 0x0040215 salta direttamente alla getch(), e successivamente avviene il controllo sul dato inserito (con CMP).
La prima cosa che si comprende, è che la lettura dell'input (che si trova in AL quando si torna da getch()) termina quando viene inserito il byte 0xD, che è l'Invio.
L'input viene memorizzato in un buffer puntato da [ESP+ESI+A2]; già da come è composto sappiamo che A2 è un offset fisso, e che la base è ESP; in parole povere ESI è l'indice usato per scorrere il buffer (quello che si fa con un for scorrere un array, insomma) e la base di questo buffer è l'indirizzo EBP+A2.
A sostegno di questa tesi all'indirizzo 0x00402136 si vede
XOR ESI, ESI
, che provoca l'azzeramento del registro. E' già un ciclo interessante, poichè guardando l'istruzione sopra alla CALL verso putchar() si vede che viene passato 0x2A, che è l'asterisco, come si vede dal commento. Questo fa si che alla pressione di un tasto, sulla console si veda un asterisco, e non il carattere inserito in input. A questo punto, proseguiamo entrando nel vivo...4. L'analisi ha inizio
L'analisi ha inizio con questo frammento di codice, ripreso dal precedente blocco:
Codice:
00402160 | 8D56 03 | lea edx,dword ptr ds:[esi+3] |
00402163 | 3215 00004000 | xor dl,byte ptr ds:[400000] |
00402169 | 83C6 01 | add esi,1 |
0040216C | 31D0 | xor eax,edx |
0040216E | 888434 A1000000 | mov byte ptr ss:[esp+esi+A1],al |
00402175 | C70424 2A000000 | mov dword ptr ss:[esp],2A | 2A:'*'
0040217C | E8 07FFFFFF | call <JMP.&putchar> |
00402181 | E8 0AFFFFFF | call <JMP.&_getch> |
00402186 | 3C 0D | cmp al,D | D:'\r'
00402188 | 888434 A2000000 | mov byte ptr ss:[esp+esi+A2],al |
0040218F | 75 CF | jne crackme_v4_marquire.402160 |
il JMP causa il salto all'istruzione 0x00402181. Come primo carattere ho inserito 'a', quindi il loop si ripete, e salta all'istruzione LEA visibile qui sopra.
Ciò che è stato fatto dal compilatore è un'ottimizzazione: LEA sta per "load effective address" e ciò che fa è prelevare il contenuto alla locazione [ESI+3]. Considerando che ESI è inizialmente uguale a 0, l'istruzione fa EDX = 0 + 3.
Notate nulla di "strano" ora? Se conoscete un pò il PE header sapete che l'immagine viene caricata in memoria e che gli indirizzi virtuali delle varie sezionni sono determinabili anche dal PE header (lasciamo perdere le protezioni, cose come ASLR, che provocano la randomizzazione e quindi ogni volta che carichiamo il programma in memoria avrà una posizione diversa (semplifico)).
Questo è il PE header di questo file:
La cosa interessante, ciò che ho definito "strano", che ha catturato la mia attenzione, è l'indirizzo 0x400000. Se guardate lo screen noterete che si tratta del VA di ImageBase. Questo indirizzo è, come dice il nome stesso, la base del nostro PE in memoria; in poche parole si tratta della locazione a cui si trova il primo byte del PE header, 0x4D ('M', che è l'header del DOS, MZ (iniziali del suo inventore)).
Viene fatto quindi lo XOR tra il valore di EDX (3, visto prima) e 0x4D, ottenendo 0x4E. Viene incrementato ESI di 1 unità e viene fatto un altro XOR tra EAX e EDX. EAX ha come byte più basso il registro AL, quindi di fatto lo XOR riguarda il valore ottenuto prima e presente in EDX, con il carattere letto in input. Il valore ottenuto (presente in AL quindi) viene memorizzato alla locazione precedente.
A prima vista la locazione può sembrare diversa, in quanto l'indirizzo precedente era [ESP+ESI+A2] mentre quello attuale è [ESP+ESI+A1]: in realtà l'indirizzo è il medesimo, in quanto nel primo accesso che ho riportato viene usato come displacement la costante A2, più grande di una unità rispetto all'accesso successivo (costante A1); ESI infatti viene incrementato di una unità prima dell'accesso [EBP+ESI+A1], quindi di fatto "compensa" l'unità espressa dalla costante sotto.
La parte sotto al JNE è... lunga. Utilizzo uno spoiler:
Codice:
00402191 | C68434 A2000000 00 | mov byte ptr ss:[esp+esi+A2],0 |
00402199 | A1 96004000 | mov eax,dword ptr ds:[400096] |
0040219E | 8D7424 24 | lea esi,dword ptr ss:[esp+24] |
004021A2 | 0FB6C0 | movzx eax,al |
004021A5 | 83C0 0F | add eax,F |
004021A8 | 894424 20 | mov dword ptr ss:[esp+20],eax |
004021AC | 8B15 76004000 | mov edx,dword ptr ds:[400076] | 00400076:"\r\n$"
004021B2 | 0FB6D2 | movzx edx,dl |
004021B5 | 83C2 09 | add edx,9 |
004021B8 | 895424 24 | mov dword ptr ss:[esp+24],edx |
004021BC | 8B15 77004000 | mov edx,dword ptr ds:[400077] | 00400077:"\n$"
004021C2 | 81E2 FF000000 | and edx,FF |
004021C8 | 895424 28 | mov dword ptr ss:[esp+28],edx |
004021CC | 8B15 9A004000 | mov edx,dword ptr ds:[40009A] |
004021D2 | 81E2 FF000000 | and edx,FF |
004021D8 | 895424 2C | mov dword ptr ss:[esp+2C],edx | [esp+2C]:"««««««««"
004021DC | 8B15 41004000 | mov edx,dword ptr ds:[400041] |
004021E2 | 0FB6D2 | movzx edx,dl |
004021E5 | 83EA 01 | sub edx,1 |
004021E8 | 895424 30 | mov dword ptr ss:[esp+30],edx | [esp+30]:"««««««««"
004021EC | 8B15 77004000 | mov edx,dword ptr ds:[400077] | 00400077:"\n$"
004021F2 | 0FB6D2 | movzx edx,dl |
004021F5 | 83CA 10 | or edx,10 |
004021F8 | 895424 34 | mov dword ptr ss:[esp+34],edx |
004021FC | 8B15 47004000 | mov edx,dword ptr ds:[400047] |
00402202 | 0FB6D2 | movzx edx,dl |
00402205 | C1FA 04 | sar edx,4 |
00402208 | 895424 38 | mov dword ptr ss:[esp+38],edx |
0040220C | 8B15 69004000 | mov edx,dword ptr ds:[400069] | 00400069:"in DOS mode.\r\r\n$"
00402212 | 0FB6D2 | movzx edx,dl |
00402215 | C1FA 04 | sar edx,4 |
00402218 | 895424 3C | mov dword ptr ss:[esp+3C],edx |
0040221C | 8B15 60004000 | mov edx,dword ptr ds:[400060] | 00400060:"t be run in DOS mode.\r\r\n$"
00402222 | 0FB6D2 | movzx edx,dl |
00402225 | 83EA 60 | sub edx,60 |
00402228 | 895424 40 | mov dword ptr ss:[esp+40],edx |
0040222C | 8B15 C8004000 | mov edx,dword ptr ds:[4000C8] |
00402232 | 0FB6D2 | movzx edx,dl |
00402235 | 83C2 01 | add edx,1 |
00402238 | 895424 44 | mov dword ptr ss:[esp+44],edx |
0040223C | 8B15 86004000 | mov edx,dword ptr ds:[400086] |
00402242 | 0FB6D2 | movzx edx,dl |
00402245 | 83EA 03 | sub edx,3 |
00402248 | 895424 48 | mov dword ptr ss:[esp+48],edx |
0040224C | 8B15 4A004000 | mov edx,dword ptr ds:[40004A] |
00402252 | 0FB6D2 | movzx edx,dl |
00402255 | 83C2 10 | add edx,10 |
00402258 | 895424 4C | mov dword ptr ss:[esp+4C],edx | [esp+4C]:"««««««««"
0040225C | 8B15 00004000 | mov edx,dword ptr ds:[400000] |
00402262 | 0FB6D2 | movzx edx,dl |
00402265 | 83CA 30 | or edx,30 |
00402268 | 895424 50 | mov dword ptr ss:[esp+50],edx | [esp+50]:"««««««««"
0040226C | 8B15 34004000 | mov edx,dword ptr ds:[400034] |
00402272 | 81E2 FF000000 | and edx,FF |
00402278 | 895424 54 | mov dword ptr ss:[esp+54],edx |
0040227C | 8B15 06004000 | mov edx,dword ptr ds:[400006] |
00402282 | 0FB6D2 | movzx edx,dl |
00402285 | 83C2 19 | add edx,19 |
00402288 | 895424 58 | mov dword ptr ss:[esp+58],edx |
0040228C | 8B15 5A004000 | mov edx,dword ptr ds:[40005A] | 0040005A:" cannot be run in DOS mode.\r\r\n$"
00402292 | 81E2 FF000000 | and edx,FF |
00402298 | 895424 5C | mov dword ptr ss:[esp+5C],edx |
0040229C | 8B15 96004000 | mov edx,dword ptr ds:[400096] |
004022A2 | 81E2 FF000000 | and edx,FF |
004022A8 | 895424 60 | mov dword ptr ss:[esp+60],edx |
004022AC | 8B15 31004000 | mov edx,dword ptr ds:[400031] |
004022B2 | 0FB6D2 | movzx edx,dl |
004022B5 | 83C2 11 | add edx,11 |
004022B8 | 895424 64 | mov dword ptr ss:[esp+64],edx |
004022BC | 8B15 76004000 | mov edx,dword ptr ds:[400076] | 00400076:"\r\n$"
004022C2 | 0FB6D2 | movzx edx,dl |
004022C5 | 83CA 10 | or edx,10 |
004022C8 | 895424 68 | mov dword ptr ss:[esp+68],edx |
004022CC | 8B15 48004000 | mov edx,dword ptr ds:[400048] |
004022D2 | 0FB6D2 | movzx edx,dl |
004022D5 | C1FA 04 | sar edx,4 |
004022D8 | 895424 6C | mov dword ptr ss:[esp+6C],edx |
004022DC | 8B15 68004000 | mov edx,dword ptr ds:[400068] | 00400068:" in DOS mode.\r\r\n$"
004022E2 | 0FB6D2 | movzx edx,dl |
004022E5 | C1FA 04 | sar edx,4 |
004022E8 | 895424 70 | mov dword ptr ss:[esp+70],edx |
004022EC | 8B15 61004000 | mov edx,dword ptr ds:[400061] | 00400061:" be run in DOS mode.\r\r\n$"
004022F2 | 0FB6D2 | movzx edx,dl |
004022F5 | 83EA 60 | sub edx,60 |
004022F8 | 895424 74 | mov dword ptr ss:[esp+74],edx | [esp+74]:&L"¤±¤±"
004022FC | 8B15 C3004000 | mov edx,dword ptr ds:[4000C3] |
00402302 | 0FB6D2 | movzx edx,dl |
00402305 | 83C2 01 | add edx,1 |
00402308 | 895424 78 | mov dword ptr ss:[esp+78],edx |
0040230C | 8B15 81004000 | mov edx,dword ptr ds:[400081] |
00402312 | 0FB6D2 | movzx edx,dl |
00402315 | 83EA 03 | sub edx,3 |
00402318 | 895424 7C | mov dword ptr ss:[esp+7C],edx | [esp+7C]:&"ˆþ`"
0040231C | 8B15 41004000 | mov edx,dword ptr ds:[400041] |
00402322 | 0FB6D2 | movzx edx,dl |
00402325 | 83C2 10 | add edx,10 |
00402328 | 899424 80000000 | mov dword ptr ss:[esp+80],edx | [esp+80]:&"ˆþ`"
0040232F | 8B15 01004000 | mov edx,dword ptr ds:[400001] |
00402335 | 0FB6D2 | movzx edx,dl |
00402338 | 83CA 30 | or edx,30 |
0040233B | 899424 84000000 | mov dword ptr ss:[esp+84],edx |
00402342 | 8B15 71004000 | mov edx,dword ptr ds:[400071] | 00400071:"ode.\r\r\n$"
00402348 | 0FB6D2 | movzx edx,dl |
0040234B | 83CA 10 | or edx,10 |
0040234E | 899424 88000000 | mov dword ptr ss:[esp+88],edx |
00402355 | 8B15 41004000 | mov edx,dword ptr ds:[400041] |
0040235B | 0FB6D2 | movzx edx,dl |
0040235E | C1FA 04 | sar edx,4 |
00402361 | 899424 8C000000 | mov dword ptr ss:[esp+8C],edx | [esp+8C]:"‹Eà‹Mðd‰\r"
00402368 | 8B15 61004000 | mov edx,dword ptr ds:[400061] | 00400061:" be run in DOS mode.\r\r\n$"
0040236E | 0FB6D2 | movzx edx,dl |
00402371 | C1FA 04 | sar edx,4 |
00402374 | 899424 90000000 | mov dword ptr ss:[esp+90],edx |
0040237B | 8B15 61004000 | mov edx,dword ptr ds:[400061] | 00400061:" be run in DOS mode.\r\r\n$"
00402381 | 0FB6D2 | movzx edx,dl |
00402384 | 83EA 60 | sub edx,60 |
00402387 | 899424 94000000 | mov dword ptr ss:[esp+94],edx |
0040238E | 8B15 C1004000 | mov edx,dword ptr ds:[4000C1] |
00402394 | 0FB6D2 | movzx edx,dl |
00402397 | 83C2 01 | add edx,1 |
0040239A | 899424 98000000 | mov dword ptr ss:[esp+98],edx |
004023A1 | 8B15 81004000 | mov edx,dword ptr ds:[400081] |
004023A7 | 0FB6D2 | movzx edx,dl |
004023AA | 83EA 03 | sub edx,3 |
004023AD | 899424 9C000000 | mov dword ptr ss:[esp+9C],edx |
004023B4 | 31D2 | xor edx,edx |
004023B6 | 31C9 | xor ecx,ecx |
Per ovvie ragioni non riporterò ogni singolo passaggio, altrimenti l'articolo assumerebbe più le sembianze di un capitolo di un libro, ma riporterò solo un paio di passi (poi si tratta solo di eseguire le operazioni che si vedono):
Codice:
00402191 | C68434 A2000000 00 | mov byte ptr ss:[esp+esi+A2],0 |
00402199 | A1 96004000 | mov eax,dword ptr ds:[400096] |
0040219E | 8D7424 24 | lea esi,dword ptr ss:[esp+24] |
004021A2 | 0FB6C0 | movzx eax,al |
004021A5 | 83C0 0F | add eax,F |
004021A8 | 894424 20 | mov dword ptr ss:[esp+20],eax |
La prima istruzione non fa altro che inserire il terminatore dopo la stringa inserita. Lo spiazzamento 0x400096 è sempre parte del PE header e la logica è di fatto la medesima in ogni singolo passo che segue.
4.1 L'inizio del keygen
Ogni singolo spiazzamento che vedete nel codice assembly sopra (insomma, tutto ciò che inizia per 0x40...) fa parte del PE header. Ho raccolto tutti questi offset e li ho inseriti in un array; da lì ho iniziato il codice che riguarda il keygen (man mano che raccoglievo pezzi, cercavo mettere assieme il puzzle):
C:
const int offsets[] =
{
0x96,0x76,0x77,0x9A,0x41,0x77,0x47,0x69,0x60,0xC8,0x86,0x4A,0x00,0x34,0x06,0x5A,
0x96,0x31,0x76,0x48,0x68,0x61,0xC3,0x81,0x41,0x01,0x71,0x41,0x61,0x61,0xC1,0x81
};
faccio solo notare che il primo valore è 0x96, che corrisponde al primo displacement... a seguire gli altri. Qusto valore è l'offset fisico, cioè la posizione in byte leggendo il file byte per byte sul disco.
Fatto ciò ho pensato, per non impazzire, di usare questo array per creare un buffer con i valori che trovo a questi offset; si tratta in pratica dei valori che vengono letti quando avviene
mov eax, dword ptr ds:[400096]
e le altre istruzioni analoghe.La lettura in questo modo risulta comoda e "pulita":
C:
FILE *hfile = fopen("CrackMe_V4_Marquire.exe", "rb");
int buff_index = 0;
for(int i=0; i<32; i++)
{
fseek(hfile, offsets[i], SEEK_SET);
fread(&buff[buff_index++], 1, 1, hfile);
}
fclose(hfile);
Tutto il codice assembly visto sopra l'ho tradotto "manualmente" senza l'utilizzo di disassembler (anche perchè il migliore in circolazione è a pagamento, a meno di averlo crackato, e lo si trova con IDA Pro... e poi è un buon esercizio):
C:
for(int i=0; i<32; i++)
{
switch(i)
{
case 0: buff[i] += 0x0f; break;
case 1: buff[i] += 9; break;
case 2: buff[i] &= 0xFF; break;
case 3: buff[i] &= 0xFF; break;
case 4: buff[i] -= 1; break;
case 5: buff[i] |= 0x10; break;
case 6: buff[i] >>= 4; break;
case 7: buff[i] >>= 4; break;
case 8: buff[i] -= 0x60; break;
case 9: buff[i]++; break;
case 10: buff[i] -= 3; break;
case 11: buff[i] += 0x10; break;
case 12: buff[i] |= 0x30; break;
case 13: buff[i] &= 0xFF; break;
case 14: buff[i] += 0x19; break;
case 15: buff[i] &= 0xFF; break;
case 16: buff[i] &= 0xFF; break;
case 17: buff[i] += 0x11; break;
case 18: buff[i] |= 0x10; break;
case 19: buff[i] >>= 4; break;
case 20: buff[i] >>= 4; break;
case 21: buff[i] -= 0x60; break;
case 22: buff[i]++; break;
case 23: buff[i] -= 3; break;
case 24: buff[i] += 0x10; break;
case 25: buff[i] |= 0x30; break;
case 26: buff[i] |= 0x10; break;
case 27: buff[i] >>= 4; break;
case 28: buff[i] >>= 4; break;
case 29: buff[i] -= 0x60; break;
case 30: buff[i]++; break;
case 31: buff[i] -= 3; break;
}
}
se lo confrontate con il codice assembly, vedrete che ogni singolo "blocco" che esegue operazioni e che le salva nel buffer di output, coincide con le operazioni fatte qui sopra.
5. Il seriale "hashato"
Proseguendo con il codice assembly, si vede quanto di seguito riportato (riprendo gli ultimi due xor del precedente listato):
Codice:
004023B4 | 31D2 | xor edx,edx |
004023B6 | 31C9 | xor ecx,ecx |
004023B8 | EB 0B | jmp crackme_v4_marquire.4023C5 |
004023BA | 8DB6 00000000 | lea esi,dword ptr ds:[esi] |
004023C0 | 8B06 | mov eax,dword ptr ds:[esi] |
004023C2 | 83C6 04 | add esi,4 |
004023C5 | 0FAFC2 | imul eax,edx |
004023C8 | 81C2 FFFF0000 | add edx,FFFF |
004023CE | 01C1 | add ecx,eax |
004023D0 | 81FA F3FF0C00 | cmp edx,CFFF3 |
004023D6 | 75 E8 | jne crackme_v4_marquire.4023C0 |
questo algoritmo è un algoritmo di hash, se così vogliamo definirlo, che genera un codice in base all'input che ottiene; l'input è il buffer precedente.
In C il codice è questo:
C:
int gen_serial(int *buff)
{
int edx = 0;
int ecx = 0;
int index = 0;
while(edx != 0xCFFF3)
{
int eax = buff[index];
eax *= edx;
edx += 0xFFFF;
ecx += eax;
index++;
}
return ecx;
}
nel codice ho cercato di usare i nomi dei registri, così dovrebbe risultare più semplice vedere le analogie. Inizio con il dare un assaggio di questi primi step, eseguendo il codice visto sino ad ora:
Codice:
How many key would you like to generate? 1
Scanning PE header...
Buffer Content:
1E 16 A 2 1E 1A C 6 14 5 5 11 7D 0 19 20 F 11 1D 2 2 FFFFFFC0 1 42 2F 7A 7F 1 2 FFFFFFC0 1 42
ECX number (serial, 'hashed' buffer content): 931F6CE
il valore che torna da questa funzione è visibile qui sopra.
6. Il calcolo del seriale
Successivamente al codice assembly del secondo paragrafo, si trova questo codice:
Codice:
004023D8 | 8D8424 A2000000 | lea eax,dword ptr ss:[esp+A2] |
004023DF | 31D2 | xor edx,edx |
004023E1 | 31FF | xor edi,edi | edi:EntryPoint
004023E3 | 0FBE30 | movsx esi,byte ptr ds:[eax] |
004023E6 | 83C0 01 | add eax,1 |
004023E9 | 0FAFF2 | imul esi,edx |
004023EC | 81C2 FFFF0000 | add edx,FFFF |
004023F2 | 01F7 | add edi,esi | edi:EntryPoint
004023F4 | 81FA F3FF0C00 | cmp edx,CFFF3 |
004023FA | 75 E7 | jne crackme_v4_marquire.4023E3 |
Il codice in C è:
C:
int calc_serial(char *pwd_buff)
{
int edx = 0;
int edi = 0;
int index = 0;
while(edx != 0xCFFF3)
{
int esi = (int) pwd_buff[index++];
esi *= edx;
edx += 0xFFFF;
edi += esi;
}
return edi;
}
A questo punto i due valori calcolati vengono confrontati: se "l'hash" corrisponde la key inserita è corretta.
Codice:
004023FC | 39F9 | cmp ecx,edi |
004023FE | 891C24 | mov dword ptr ss:[esp],ebx |
00402401 | 74 0F | je crackme_v4_marquire.402412 |
00402403 | E8 C8F3FFFF | call crackme_v4_marquire.4017D0 |
00402408 | 8D65 F4 | lea esp,dword ptr ss:[ebp-C] |
0040240B | 31C0 | xor eax,eax |
0040240D | 5B | pop ebx |
0040240E | 5E | pop esi |
0040240F | 5F | pop edi |
00402410 | 5D | pop ebp |
00402411 | C3 | ret |
00402412 | E8 29F3FFFF | call crackme_v4_marquire.401740 |
00402417 | EB EF | jmp crackme_v4_marquire.402408 |
Le due CALL che si vedono nel codice qui sopra contengono la generazione del messaggio di insuccesso (key errata) e successo (key corretta). Proseguendo con la key casuale inserita prima, si ottiene infatti:
Codice:
## The goal of this crackme is to find the key! ##
Enter the key : *****
->wrong!
7. Il keygen completo
Ebbene siamo giunti al codice completo:
C:
/*
0040215C | 8D7426 00 | lea esi,dword ptr ds:[esi]
00402160 | 8D56 03 | lea edx,dword ptr ds:[esi+3]
00402163 | 3215 00004000 | xor dl,byte ptr ds:[400000]
00402169 | 83C6 01 | add esi,1
0040216C | 31D0 | xor eax,edx
0040216E | 888434 A1000000 | mov byte ptr ss:[esp+esi+A1],al
00402175 | C70424 2A000000 | mov dword ptr ss:[esp],2A
0040217C | E8 07FFFFFF | call <JMP.&putchar>
00402181 | E8 0AFFFFFF | call <JMP.&_getch>
00402186 | 3C 0D | cmp al,D
00402188 | 888434 A2000000 | mov byte ptr ss:[esp+esi+A2],al
0040218F | 75 CF | jne crackme_v4_marquire.402160
ESI = 0
ds:[400000] = 'M'
*/
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#define BUFF_SIZE 0x20
#define STR_LEN 14
void init_buffer(int *buff)
{
// Offset inside PE header
const int offsets[] =
{
0x96,0x76,0x77,0x9A,0x41,0x77,0x47,0x69,0x60,0xC8,0x86,0x4A,0x00,0x34,0x06,0x5A,
0x96,0x31,0x76,0x48,0x68,0x61,0xC3,0x81,0x41,0x01,0x71,0x41,0x61,0x61,0xC1,0x81
};
FILE *hfile = fopen("CrackMe_V4_Marquire.exe", "rb");
int buff_index = 0;
for(int i=0; i<32; i++)
{
fseek(hfile, offsets[i], SEEK_SET);
fread(&buff[buff_index++], 1, 1, hfile);
}
fclose(hfile);
for(int i=0; i<32; i++)
{
switch(i)
{
case 0: buff[i] += 0x0f; break;
case 1: buff[i] += 9; break;
case 2: buff[i] &= 0xFF; break;
case 3: buff[i] &= 0xFF; break;
case 4: buff[i] -= 1; break;
case 5: buff[i] |= 0x10; break;
case 6: buff[i] >>= 4; break;
case 7: buff[i] >>= 4; break;
case 8: buff[i] -= 0x60; break;
case 9: buff[i]++; break;
case 10: buff[i] -= 3; break;
case 11: buff[i] += 0x10; break;
case 12: buff[i] |= 0x30; break;
case 13: buff[i] &= 0xFF; break;
case 14: buff[i] += 0x19; break;
case 15: buff[i] &= 0xFF; break;
case 16: buff[i] &= 0xFF; break;
case 17: buff[i] += 0x11; break;
case 18: buff[i] |= 0x10; break;
case 19: buff[i] >>= 4; break;
case 20: buff[i] >>= 4; break;
case 21: buff[i] -= 0x60; break;
case 22: buff[i]++; break;
case 23: buff[i] -= 3; break;
case 24: buff[i] += 0x10; break;
case 25: buff[i] |= 0x30; break;
case 26: buff[i] |= 0x10; break;
case 27: buff[i] >>= 4; break;
case 28: buff[i] >>= 4; break;
case 29: buff[i] -= 0x60; break;
case 30: buff[i]++; break;
case 31: buff[i] -= 3; break;
}
}
}
void read_buffer(int *buff)
{
for(int i=0; i<BUFF_SIZE; i++)
{
printf("%X ", buff[i]);
}
}
/*
004023B4 | 31D2 | xor edx,edx
004023B6 | 31C9 | xor ecx,ecx
004023B8 | EB 0B | jmp crackme_v4_marquire.4023C5
004023BA | 8DB6 00000000 | lea esi,dword ptr ds:[esi]
004023C0 | 8B06 | mov eax,dword ptr ds:[esi]
004023C2 | 83C6 04 | add esi,4
004023C5 | 0FAFC2 | imul eax,edx
004023C8 | 81C2 FFFF0000 | add edx,FFFF
004023CE | 01C1 | add ecx,eax
004023D0 | 81FA F3FF0C00 | cmp edx,CFFF3
004023D6 | 75 E8 | jne crackme_v4_marquire.4023C0
*/
int gen_serial(int *buff)
{
int edx = 0;
int ecx = 0;
int index = 0;
while(edx != 0xCFFF3)
{
int eax = buff[index];
eax *= edx;
edx += 0xFFFF;
ecx += eax;
index++;
}
return ecx;
}
/*
004023D8 | 8D8424 A2000000 | lea eax,dword ptr ss:[esp+A2]
004023DF | 31D2 | xor edx,edx
004023E1 | 31FF | xor edi,edi
004023E3 | 0FBE30 | movsx esi,byte ptr ds:[eax]
004023E6 | 83C0 01 | add eax,1
004023E9 | 0FAFF2 | imul esi,edx
004023EC | 81C2 FFFF0000 | add edx,FFFF
004023F2 | 01F7 | add edi,esi
004023F4 | 81FA F3FF0C00 | cmp edx,CFFF3
004023FA | 75 E7 | jne crackme_v4_marquire.4023E3
*/
int calc_serial(char *pwd_buff)
{
int edx = 0;
int edi = 0;
int index = 0;
while(edx != 0xCFFF3)
{
int esi = (int) pwd_buff[index++];
esi *= edx;
edx += 0xFFFF;
edi += esi;
}
return edi;
}
void print_char_bytes(char *buff, int size)
{
for(int i=0; i<size; i++)
{
printf("%X ", buff[i]);
}
}
void gen_random_string(char *esp_esi, char *str)
{
int esi = 0;
const char *charset = "?_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
while(esi < STR_LEN)
{
//char ch = (char) (rand() % 96) + 33;
char ch = charset[(rand() % strlen(charset))];
str[esi] = ch;
int edx = esi + 3;
edx ^= 0x4D;
int eax = ch ^ edx;
esp_esi[esi++] = (char)eax;
}
}
int main()
{
srand(time(NULL));
int times = 0;
char esp_esi[STR_LEN+1] = {'\0'}; // "hashed" string
char str[STR_LEN+1] = {'\0'}; // generated string
int buff[BUFF_SIZE] = {0}; // buffer filled with some header byte
printf("How many key would you like to generate? ");
scanf("%d", ×);
printf("\nScanning PE header...\n");
init_buffer(buff);
printf("\nBuffer Content:\n");
read_buffer(buff);
int ecx = gen_serial(buff);
printf("\nECX number (serial, 'hashed' buffer content): %X\n", ecx);
FILE *hsolution = fopen("solution.txt","w");
while(times)
{
// hashed string (esp_esi) and ASCII generated string (str)
gen_random_string(esp_esi, str);
int edi = calc_serial(esp_esi);
if(edi == ecx)
{
str[strlen(str)-1] = '\n';
//printf("\nString serial: %X\n", edi);
//printf("String chars: %s\n", str);
fwrite(str, strlen(str),1,hsolution);
times--;
}
}
fclose(hsolution);
return 0;
}
Ho voluto generalizzare e andare di forza bruta, come dicevo a inizio articolo, quindi genero tutte le key richieste e le scrivo su un file txt. Le key generate sono tutte quelle che come "hash" hanno il medesimo valore di quello calcolato; questo lo si vede nel codice del main, all'interno del while.
La generazione è tutta opera di get_random_string():
C:
void gen_random_string(char *esp_esi, char *str)
{
int esi = 0;
const char *charset = "?_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
while(esi < STR_LEN)
{
//char ch = (char) (rand() % 96) + 33;
char ch = charset[(rand() % strlen(charset))];
str[esi] = ch;
int edx = esi + 3;
edx ^= 0x4D;
int eax = ch ^ edx;
esp_esi[esi++] = (char)eax;
}
}
questa è la parte di "forza bruta" utilizzando un alfabeto ristretto (per evitare caratteri 'strani').
E' anche piuttosto veloce, per un centinaio di keys l'attesa non supera il secondo.