2009-05-20:

CONFidence 2009 ESET crackme - rozwiązanie

re:easy:confidence:crackme:c++
W końcu jakiś techniczny post! A post poświęcony będzie crackme ESET'u, które to było do połamania na tegorocznej edycji CONFidence z numerkiem 2009. Crackme (przygotowanym specjalnie na confidence) pozwoliłem sobie udostępnić (@Marcin/Jakub w razie czego dajcie znać, to zdejmę ;>), tak aby osoby nieobecne na confi również mogły się pobawić:

UPDATE: Marcin podesłał mi nową binarkę bez pewnych ułatwień których tam nie miało być:
confidence_eset_crackme.zip (binarka bez ułatwień, crackme)
Ponieważ adresy w nowej się nie zgadzają z tym o czym pisze, to na samym dole postu jest do ściągnięcia również stara binarka, której adresy są zgodne z moim poniższym tutorialem :)

Drogi czytelniku, jeżeli chcesz spróbować własnymi siłami połamać powyższe crackme, TO NATYCHMIAST WYŁĄCZ TĄ STRONĘ!!! :) Poniżej zamieszczam rozwiązanie do ów crackme.



A

N

T

Y




S

P

O

I

L

E

R



Najpierw parę słów o rozwiązaniu: jak pisałem w poprzednim poście, miałem 3 podejścia do tego crackme - za dwoma pierwszymi razami okazywało się że "nie tędy droga". W rozwiązaniu które przedstawiam poniżej pominę etapy pomyłek, i przejdę od razu do prawidłowego rozwiązania.



Celem crackme jest uzyskanie/odzyskanie hasła, które wpisane w pole tekstowe spowoduje wyświetlenie czegoś w stylu "congratz!" (patrz screen powyżej). Zaczniemy jak zwykle od rekonesansu - czyli w ruch idą narzędzia typu PEiD (którego ofc nie miałem przy sobie na konferencji - wziąłem nieskonfigurowanego lapka na confi, prawie bez toolsów, a już na pewno bez moich kodów ;/) czy Ent (to miałem przy sobie). Poniżej wrzucam wykres z Enta (osoby zainteresowane ale niezorientowane wtf ten wykres przedstawia odsyłam do tego postu):



Jak widać, nie mamy do czynienia z żadnym nietrywialnym szyfrowaniem kodu - good news - więc, nie zwlekając dalej możemy wrzucić crackme w nasz ulubiony disassembler (czytaj: IDA Pro). Patrząc w listę Exportów dowiadujemy się że mamy trzy miejsca startowe - 2x TLS callback oraz EP. W tym miejscu na confi zrobiłem błąd i od razu poszedłem do EP ignorując TLSy - żeby trochę spotęgować napięcie i teraz tak zrobię :)

Zacznijmy od znalezienia jakiś procedurek obsługujących pobieranie tekstu z pola tekstowego - GetWindowTextA, GetDlgItemTextA, etc - czyli przeglądamy okienko Imports. Jak się okazuje w IAT występuje jedynie GetDlgItemTextA:

.text:00405B1A GetDlgItemTextA proc near               ; CODE XREF: DialogFunc+42
.text:00405B1A                 jmp     ds:__imp_GetDlgItemTextA
.text:00405B1A GetDlgItemTextA endp


Dodatkowo widać że GetDlgItemTextA jest użyty jedynie raz - w DialogFunc+42:

.text:00401175                 mov     ebx, offset String
.text:0040117A                 push    20
.text:0040117C                 push    0
.text:0040117E                 push    ebx
.text:0040117F                 call    memset
.text:00401184                 push    20              ; cchMax
.text:00401186                 push    ebx             ; lpString
.text:00401187                 push    67h             ; nIDDlgItem
.text:00401189                 push    [ebp+hDlg]      ; hDlg
.text:0040118C                 call    GetDlgItemTextA
.text:00401191                 test    eax, eax
.text:00401193                 jz      short loc_4011AE
.text:00401195                 push    ebx
.text:00401196                 call    sub_403055
.text:0040119B                 test    eax, eax
.text:0040119D                 jz      short loc_4011AE
.text:0040119F ; "Congratulations, your password is corre"...
.text:0040119F                 push    offset aCongratulation
.text:004011A4                 push    [ebp+hDlg]      ; hDlg
.text:004011A7                 call    sub_401132
.text:004011AC                 jmp     short locret_4011C9
.text:004011AE ; ---------------------------------------------------------------------------
.text:004011AE
.text:004011AE loc_4011AE:                             ; CODE XREF: DialogFunc+49
.text:004011AE                                         ; DialogFunc+53
.text:004011AE ; "Sorry, your password is wrong"
.text:004011AE                 push    offset aSorryYourPassw
.text:004011B3                 push    [ebp+hDlg]      ; hDlg
.text:004011B6                 call    sub_401132
.text:004011BB                 jmp     short locret_4011C9


Pod adresem 00401196 jest call sub_403055, do której jest przekazywany string, a następnie, w zależności od tego co zwróci sub_403055 wyświetlany jest napis "Contratulations..." albo "Sorry...". Tak więc sercem całości jest sub_403055.

Po zejściu do tej funkcji okazuje się ze jest ona długa. Bardzo długa. Baaaaaaaaardzo bardzo długa. A konkretniej, ma jakieś 3000 linii kodu asma, z czego większość to instrukcje typu sub, xor, lea, ror, rol, czy add. Pozostała część wygląda następująco:

.text:00403055 sub_403055      proc near               ; CODE XREF: DialogFunc+4C
.text:00403055
.text:00403055 arg_0           = dword ptr  8
.text:00403055
.text:00403055                 push    ebp
.text:00403056                 mov     ebp, esp
.text:00403058                 push    5
.text:0040305A                 pop     ecx
.text:0040305B                 mov     esi, [ebp+arg_0]
.text:0040305E                 mov     edi, offset go
.text:00403063                 pushf
.text:00403064                 xor     dword ptr ds:[esp], 100h
.text:0040306C                 popf

.text:0040306D                 nop
.text:0040306E
.text:0040306E loc_40306E:                             ; CODE XREF: sub_403055+2A94
.text:0040306E                 lodsd
.text:0040306F                 sub     eax, 8A14F2F5h
.text:00403074                 xor     eax, 7418FCC5h
.text:00403079                 lea     eax, [eax+3A61C552h]
.text:0040307F                 sub     eax, 0D101638Ch

...

.text:00405AE4                 ror     eax, 17h
.text:00405AE7                 stosd
.text:00405AE8                 dec     ecx
.text:00405AE9                 jnz     loc_40306E
.text:00405AEF                 nop
.text:00405AF0                 nop
.text:00405AF1                 nop
.text:00405AF2                 nop
.text:00405AF3                 mov     edi, offset go
.text:00405AF8                 mov     esi, offset dword_4070C2
.text:00405AFD                 push    5
.text:00405AFF                 pop     ecx
.text:00405B00                 repe cmpsd
.text:00405B02                 setz    al
.text:00405B05                 and     eax, 0FFh
.text:00405B0A                 leave
.text:00405B0B                 retn    4
.text:00405B0B sub_403055      endp


W skrócie, do ESI wrzucany jest adres stringu, do EDI wrzucany jest adres buforu na zakodowany string, a następnie po 4ry bajty (lodsd) string jest kodowany (max 5*4 czyli 20 bajtów), i zapisywany w buforze wyjściowym (stosd). Na samym końcu następuje porównanie zakodowanego stringu z oryginalnym zakodowanym hasłem znajdującym się pod adresem 4070C2:

0FBE0BB50h, 0D16C80CCh, 716786EDh, 3B77A739h, 493A8A5Ah

A następnie w zależności czy hasło jest OK czy nie, zwracana jest pewna wartość.

Wszystko wygląda prosto i klarownie na pierwszy rzut oka, zastanowić mogą tylko dwie rzeczy:

.text:00403063                 pushf
.text:00403064                 xor     dword ptr ds:[esp], 100h
.text:0040306C                 popf


oraz

.text:00405AEF                 nop
.text:00405AF0                 nop
.text:00405AF1                 nop
.text:00405AF2                 nop


Pierwszy zastanawiający kod to jest włączenie Trap Flag - czyli trybu krokowego procesora. Na początku uznałem że to jakiś anti-debug, i zignorowałem, ale jak się za chwilę okaże, nic bardziej mylnego!

Drugą sprawą są 4ry NOPy które ni stąd ni zowąd leżą sobie pod koniec funkcji - czyżby runtime coś było tam wrzucane? Jakiś dodatkowy kod?

I w tym momencie wrócimy do TLS callbacków, a konkretniej TlsCallback_0 pod adresem 40233A. Od razu rzucić się w oczy może debug string "loading imports" - jak się okazuje takich stringów jest więcej, i bardzo dobrze tłumaczą co się dzieje w kodzie. Callback jest krótki, i skupia się na wywołaniu paru funkcji, z których najciekawszą jest sub_40120E.

W tejże funkcji widzimy m.in. CreateProcsss z flagą DEBUG_PROCESS, a następnie pętlę debuggera która odbiera m.in. event EXCEPTION_SINGLE_STEP który generowany jest przez włączenie Trap Flag w kodzie który analizowaliśmy parę linii wyżej! No i układanka zaczyna pasować! Podsumujmy co do tej pory wiemy:

- gdy odpalamy crackme wykonanie nigdy nie dociera do wyświetlenia okna - zamiast tego proces odpala ponownie swojego exeka jako debugger!
- czyli mamy dwa procesy
- proces dziecko - który wyświetla okno, sprawdza hasło, i w pewnym momencie Trap Flag uaktywnia
- proces rodzica - który pozostaje w pętli debuggera i czeka aż się Trap Flag uaktywni

Oczywistym następstwem powyższego jest fakt iż procesu dziecka nie możemy debugować za pomocą debugger API (stealth debuggery typu Obsidian i debuggery ring 0 ofc dadzą radę, ale nie są potrzebne tak na prawdę).

Rzućmy okiem na obsługę eventu SINGLE_STEP:

.text:00401302                 mov     eax, [CONTEXT.EIP]
.text:00401308                 mov     edx, [eax]
.text:0040130A                 cmp     edx, 90909090h
.text:00401310                 jz      short koniec
.text:00401312                 or      [CONTEXT.EFLAGS], 100h
.text:0040131C                 cmp     dl, 35h         ; XOR EAX, ...
.text:0040131F                 jz      short action_xor
.text:00401321                 cmp     dl, 2Dh         ; SUB EAX, ...
.text:00401324                 jz      short action_sub
.text:00401326                 cmp     dl, 5           ; ADD EAX, ...
.text:00401329                 jz      short action_add
.text:0040132B                 jmp     short koniec
.text:0040132D ; ---------------------------------------------------------------------------
.text:0040132D
.text:0040132D action_xor:                             ; CODE XREF: sub_40120E+111
.text:0040132D                 sub     dword ptr [CONTEXT.EAX], 2
.text:00401334                 jmp     short koniec
.text:00401336 ; ---------------------------------------------------------------------------
.text:00401336
.text:00401336 action_sub:                             ; CODE XREF: sub_40120E+116
.text:00401336                 add     dword ptr [CONTEXT.EAX], 1
.text:0040133D                 jmp     short koniec
.text:0040133F ; ---------------------------------------------------------------------------
.text:0040133F
.text:0040133F action_add:                             ; CODE XREF: sub_40120E+11B
.text:0040133F                 xor     dword ptr [CONTEXT.EAX], 10101010h
.text:00401349
.text:00401349 koniec:                                 ; CODE XREF: sub_40120E+102
.text:00401349                                         ; sub_40120E+11D ...


Działanie tego mechanizmu jest następujące (jeżeli ktoś czytał mój art z Xploit 3/2008 o user opcodes to dostrzeże podobieństwo :>):
- spod EIP procesu dziecka pobierane są 4ry bajty instrukcji
- jeżeli te 4 bajty to 90909090 (czyli 4x NOP - brzmi znajomo ?) to nic więcej nie jest robione
- w innym wypadku flaga TF jest odnawiana (TF jest gaszone po pierwszym 'uruchomieniu')
- a następnie analizowany jest pierwszy bajt instrukcji spod EIP:
-- jeżeli jest to 35 (czyli XOR EAX, imm32), to dodatkowo od EAX odejmowane jest 2 (to się wykonuje PRZED prawdziwą instrukcją)
-- jeżeli jest to 2D (czyli SUB EAX, imm32), to dodatkowo do EAX dodawane jest 1
-- jeżeli jest to 05 (czyli ADD EAX, imm32), to dodatkowo EAX jest xorowane z 10101010h
- wykonanie procesu jest kontynuowane

Czyli - metoda szyfrująca hasło zawiera dodatkowe dodawania/odejmowania/xorowania których nie widać w listingu (bo wykonuje je zew debugger)! Sprryyytne :)

OK. Teraz mamy już wszystkie klocki potrzebne do rozwiązania zagadki! Metod na odzyskanie hasła jest kilka - np. można odwrócić listing, albo zrobić brute force - ja zdecydowałem się na to ostatnie.

W takim wypadku należy zacząć od skopiowania całej procedury zawierającej kodowanie hasła, a następnie kilkoma regexpami wkleić dodatkowe dec+dec, inc czy xor (najlepiej robić to właśnie w tej kolejności). Potem można usunąć niepotrzebne już włączanie TF, i skompilować.

Ostateczna postać procedury: esetcode.nasm

Teraz klepiemy w C/C++ prosty bruteforce który ma:
- wczytać do pamięci skompilowaną wersję procedury
- poprawić w niej adresy
- a następnie, korzystając z faktu że jest tylko 4GB kombinacji minus kombinacje zawierające przynajmniej jeden znak niedrukowalny, odpalać procedurę szyfrującą hasło

Ponieważ hasło szyfrowane jest DWORDami które są dodatkowo niezależne od siebie, tak więc możemy w jednej pętli szukać prawidłowego rozwiązania dla wszystkich pięciu DWORDów z hasła. Taki brute force wygląda następująco (kod pisany na kolanie na konkursie, więc nie spodziewajcie sie miss code 2009 ;p):

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char buf[1024 * 1024];

DWORD a[5];
DWORD b[5];

int
main(void)
{
 FILE *f;
 f = fopen("code", "rb");
 fread(buf, 1, sizeof(buf), f);
 fclose(f);

 int (__stdcall *func)(const char *a) = (typeof(func))&buf;

#define P1 0xd
#define P2 0x34B8
#define P3 0x34BD

 DWORD v12 = (DWORD)a;
 DWORD v3 = (DWORD)b;

 *(DWORD*)(P1 + buf) = v12;
 *(DWORD*)(P2 + buf) = v12;
 *(DWORD*)(P3 + buf) = v3;

 puts("done patching"); fflush(stdout);

 static unsigned char brute[40];

 DWORD myin;

 for(myin = 0; myin != 0xffffffff; myin++)
 {
   *(DWORD*)brute = myin;

       if((myin % 0x01000000) == 0)
     putchar('.');

   if(brute[0] < ' ' || brute[0] > '~') continue;
   if(brute[1] < ' ' || brute[1] > '~') continue;
   if(brute[2] < ' ' || brute[2] > '~') continue;
   if(brute[3] < ' ' || brute[3] > '~') continue;

   func((const char*)brute);

   if(a[0] == 0x0FBE0BB50 || a[0] == 0x0D16C80CC || a[0] == 0x716786ED || a[0] == 0x3B77A739 || a[0] == 0x493A8A5A)
   {
     char asdf[8];
     *(DWORD*)(asdf) = myin;
     asdf[4] = 0;
     printf("%.8x (%s) == %.8x\n", myin, asdf, a[0]);
   }
 }

 return 0;
}


Kompilujemy (g++, z uwagi na użycie typeof()), odpalamy, i... (ilość kropek może nie odpowiadać rzeczywistości ;p)

done patching
.................................20276e69 (in' ) == 716786ed
20756f59 (You ) == fbe0bb50
........................................6b6c6174 (talk) == d16c80cc
..6d206f74 (to m) == 3b77a739
......................................................


Jak widać nie znalazło ostatniego ciągu (\0 by trzeba tam w brute dorzucić), ale to mało istotne. Układając to co mamy możemy się domyślić co jest ostatnie: You talkin' to m ->  You talkin' to me?, i gotowę! :)

No i chyba tyle :)

P.S. Oryginalna binarka z CONFidence: confiesetcrackme.zip

Comments:

2009-05-20 16:20:05 = Malcom
{
Ciekawe, ciekawe... :>

BTW, Czemu ludzie od security i nie tylko pisza taki paskudny kod?
Ani to C ani C++, tylko miksy, C z klasami lub inne potworki ;p
}
2009-05-20 17:59:23 = Gynvael Coldwind
{
@Malcom
To przez ewolucję! -> http://www.kaila.pl/humor/program.htm ;D
}
2009-05-23 08:40:33 = Gynvael Coldwind
{
@ged_
Gratz za XSS'a (mimo że trzeba kliknąć) ;> To pierwsza rzecz którą ktoś znalazł na moim blogu ;> Zaraz to spatchuje ;>

@flame
Pozwoliłem sobie ukryć flamewar jaki wywiązał się między bw a innymi czytelnikami ;>
Natomiast dla celów kronikarskich zaznaczę o co chodziło: bw stwierdził że crackme było zbyt proste i że powinno być trudniejsze, na co marcin odparł że tak miało być jak było (i że crackme było przewidziane na 30 minut), a potem dyskusja zeszła na to co powinno być dozwolone podczas crackme, a czego używania można zabronić, co skończyło się ogólnymi wjazdami personalno/firmowymi :)
}
2009-05-25 16:48:45 = Leming
{
Uchylisz rąbka tajemnicy dot. tego XSS'a ;> ?
}
2009-05-25 17:27:39 = Gynvael Coldwind
{
@Leming
Sure ;> Prosta sprawa - w URL w komentarzach wpisać np. javascript:alert(666), i potem czekać aż ktoś kliknie ;>
}
2009-06-19 20:08:58 = no comment
{
http://www.secnews.pl/2009/06/19/eset-crackme/
}
2010-03-16 06:14:08 = timon
{
Jak dlugo mielil sie ten brute force? Bo tak na moje 'oko' to chyba pare godzin :) Ale moze mi sie tylko wydaje ;)
}
2010-03-16 11:07:32 = Gynvael Coldwind
{
@timon
2-3 minuty z tego co pamiętam ;)
}

Add a comment:

Nick:
URL (optional):
Math captcha: 10 ∗ 4 + 4 =