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ę).
By the way...
If you'd like to learn SSH in depth, in the second half of January'25 we're running a 6h course - you can find the details at hexarcana.ch/workshops/ssh-course
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:
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
To przez ewolucję! -> http://www.kaila.pl/humor/program.htm ;D
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 :)
Sure ;> Prosta sprawa - w URL w komentarzach wpisać np. javascript:alert(666), i potem czekać aż ktoś kliknie ;>
2-3 minuty z tego co pamiętam ;)
Add a comment: