Autorem tekstu jest Przemysław Saleta. Omawianie CrackMe można ściągnąć tutaj.
Wstępna analiza CrackMe
Wstępnie spróbujmy ustalić, z czym mamy do czynienia – skanujemy „crackme.exe” przy pomocy PEiD. „PeCompact 2.xx –> BitSum Technologies *” – aplikacja jest spakowana, prawdopodobnie plugin PEiD Generic Unpacker będzie w stanie rozpakować program automatycznie. Detekcja OEP się chyba powiodła – 56EB00 – odpakowanie niestety nie. Możemy skorzystać z dedykowanego unpackera bądź też zrobić to ręcznie, wybierzemy drugą opcję.
Ładujemy program w Interactive Disassemblera, z zaznaczoną opcją ładowania zasobów. Oczywiście możemy też posłużyć się innym debuggerem, ale IDA i tak zapewne będzie podstawowym narzędziem w dalszej analizie. Prolog unpackera wygląda następująco:
.text:00401000 mov eax, offset _handler
.text:00401005 push eax
.text:00401006 push large dword ptr fs:0
.text:0040100D mov large fs:0, esp
.text:00401014 xor eax, eax
.text:00401016 mov [eax], ecx
.text:00401016 ; ---------------------------------------------------------------------------
.text:00401018 aPecompact2 db 'PECompact2',0
.text:00401023 ; ---------------------------------------------------------------------------
Jak widać ustawiany jest handler SEH, do którego przekazywane jest sterowanie poprzez generację wyjątku ACCESS_VIOLATION:
.rsrc:005A02F4 ; int __cdecl handler(EXCEPTION_RECORD *exceptionRecord)
.rsrc:005A02F4 _handler proc near ; DATA XREF: .text:start
.rsrc:005A02F4
.rsrc:005A02F4 exceptionRecord = dword ptr 4
.rsrc:005A02F4
.rsrc:005A02F4 mov eax, 0F059F079h
.rsrc:005A02F9 lea ecx, [eax+1000129Eh]
.rsrc:005A02FF mov [ecx+1], eax
.rsrc:005A0302 mov edx, [esp+exceptionRecord]
.rsrc:005A0306 mov edx, [edx+EXCEPTION_RECORD.ExceptionAddress]
.rsrc:005A0309 mov byte ptr [edx], 0E9h
.rsrc:005A030C add edx, 5
.rsrc:005A030F sub ecx, edx
.rsrc:005A0311 mov [edx-4], ecx
.rsrc:005A0314 xor eax, eax
.rsrc:005A0316 retn
.rsrc:005A0316 _handler endp
Na pierwszy rzut oka widać, iż modyfikowany jest kod, który spowodował rzucenie wyjątku. Zastawiamy breakpoint na koniec handlera (ustawiamy kursor na linii 0x5A0316 i wciskamy F2), następnie uruchamiamy program (F9). Po chwili IDA zgłasza wspomniany wyjątek, wznawiamy wykonywanie (F9) i potwierdzamy przekazanie wyjątku do programu. Widzimy, że kod uległ niewielkiej zmianie:
.text:00401016 jmp sub_5A0317
.text:00401016 ; ---------------------------------------------------------------------------
.text:0040101B aOmpact2 db 'ompact2',0
.text:00401023 ; ---------------------------------------------------------------------------
Funkcja sub_5A0317 jest względnie niewielka, na końcu widać dosyć typowy epilog unpackera:
.rsrc:005A03AE mov eax, esi
.rsrc:005A03B0 pop edx
.rsrc:005A03B1 pop esi
.rsrc:005A03B2 pop edi
.rsrc:005A03B3 pop ecx
.rsrc:005A03B4 pop ebx
.rsrc:005A03B5 pop ebp
.rsrc:005A03B6 jmp eax
Ustawmy kursor na adresie 0x5A03B6 i skorzystajmy z opcji Run to cursor (F4) aby przejść do skoku do OEP. PEiD się mylił, OEP to 0x4F7496.
Zrzucenie pamięci i odbudowa importów
Pozostało zrzucić proces z pamięci i odbudować importy, w tym celu skorzystamy z narzędzi LordPE i ImpREC (Import REConstructor). Wybieramy w LordPE odpowiedni proces, z menu kontekstowego zaś ’Dump full…’, zapisujemy jako „unpacked_crackme.exe”. W ImpREC również wybieramy proces z listy, następnie wpisujemy w OEP wartość 0xF7696 (RVA), klikamy [AutoSearch], potem [Get Imports]. Teraz wystarczy skorzystać z opcji [Fix dump]. Zamykamy pozostawioną sesję debuggera.
Analiza rozpakowanego pliku
Ładujemy nowo powstały plik („unpacked_crackme_.exe„) do Interactive Disassemblera. EP dosyć typowy dla aplikacji tworzonych przy użyciu Visual Studio, aby ułatwić sobie późniejszą analizę w oknie Signatures (Shift+F5) dodajemy sygnatury ’Microsoft VisualC 2-9/net runtime’ i 'MFC 3.1/4.0/4.2/8.0/9.0 32bit’.
Na początek podejmujemy próbę programu uruchomienia pod debuggerem w celu ogólnego zapoznania się z zachowaniem aplikacji (korzystając z debuggera wbudowanego w Interactive Disassemblera). Można bezpiecznie przyjąć założenie, że program nie podejmie działań destruktywnych w momencie wykrycia ingerencji, jest to w końcu wyłącznie crackme.
W chwilę po uruchomieniu zostaje zgłoszony wyjątek Privileged instruction (1):
.text:00401DE1 mov eax, 'VMXh'
.text:00401DE6 mov ebx, 0
.text:00401DEB mov ecx, 10
.text:00401DF0 mov edx, 'VX'
.text:00401DF5 in eax, dx ; (1)
.text:00401DF6 cmp ebx, 'VMXh'
.text:00401DFC setz [ebp+rc]
Funkcja ta to nic innego jak powszechnie znana i stosowana IsInsideVMWare [1]. W tym konkretnym crackme dodano wywołanie ExitProcess, które ma natychmiast zamknąć aplikację w wypadku wykrycia pracy wewnątrz VMWare. Interesującym (i zapewne wartym prześledzenia) może być kontekst wywołania tej funkcji, w tym celu zaznaczamy ostatnią instrukcję właściwego epilogu (0x401E38) i korzystamy z opcji Run to cursor (F4), potwierdzając przekazanie wyjątku do programu (wyjątek jest generowany wyłącznie kiedy program pracuje poza VMWare).
Analogicznie można też posłużyć się callstackiem (Ctrl+Alt+S) i przejść do następnej instrukcji po wywołaniu IsInsideVMWare – 0x40146B. Po wyjściu z funkcji widzimy ciąg wywołań FindWindowW połączonych z warunkowym wywołaniem ExitProcess w wypadku znalezienia okna OllyDbg (także chronionego pluginem Phant0m), WinDBG… i Interactive Disassemblera. Za nimi znajduje się kolejny kod odpowiedzialny za detekcję debuggera:
.text:004014BF push offset LibFileName ; "ntdll.dll"
.text:004014C4 mov [ebp+debugPort], 0
.text:004014CB call LoadLibraryW
.text:004014D1 push offset ProcName ; "NtQueryInformationProcess"
.text:004014D6 push eax ; hModule
.text:004014D7 call GetProcAddress
.text:004014DD push 0 ; ReturnLength
.text:004014DF mov edi, eax
.text:004014E1 push 4 ; ProcessInformationLength
.text:004014E3 lea eax, [ebp+debugPort]
.text:004014E6 push eax ; ProcessInformation
.text:004014E7 push ProcessDebugPort ; ProcessInformationClass
.text:004014E9 call GetCurrentProcess
.text:004014EF push eax ; ProcessHandle
.text:004014F0 call edi ; NtQueryInformationProcess
.text:004014F2 test eax, eax
.text:004014F4 jnz short loc_401502
.text:004014F6 cmp [ebp+debugPort], eax
.text:004014F9 jz short loc_401502
.text:004014FB push eax ; uExitCode
.text:004014FC call ExitProcess
Powyższy kod ma za zadanie pobrać uchwyt portu debuggera dla bieżącego procesu [2] i zamknąć aplikację jeżeli takowy istnieje. Jak łatwo się domyślić, kiedy nie stwierdzono obecności debuggera wywołanie ExitProcess jest pomijane, program przechodzi pod 0x401502, który to adres jest początkiem epilogu funkcji. Aby szybko i bezpiecznie ominąć wszystkie testy ustawiamy kursor na linii 0x401502 i korzystamy z dostępnej w menu kontekstowym opcji Set IP (Ctrl+N). Cały kod odpowiedzialny za wykrywanie debuggera nie posiada efektów ubocznych (poza ew. zamknięciem procesu…), więc zmiana EIP nie wpłynie negatywnie na działanie aplikacji. Pozwalamy na dalsze, swobodne, wykonywanie aplikacji – Run (F9).
Pojawiło się okno crackme. Wpisujemy w aktywne pole tekstowe, dla przykładu, „Jakub Dębski na premiera” i klikamy sąsiedni przycisk „CHECK”, aby zobaczyć reakcję aplikacji.
Niespodzianką jest ponowne rzucenie wyjątku wewnątrz IsInsideVMWare – postępujemy tak jak poprzednio. Kolejna niespodzianka to widziana wcześniej drabinka testów opartych o FindWindowW, najprawdopodobniej razem z wywołaniem IsInsideVMWare stanowią funkcję inline. I tym razem za nimi znajduje się antydebug:
.text:004017A5 mov [ebp+beeingDebugged], 0
.text:004017A9 mov eax, large fs:30h
.text:004017AF mov al, [eax+_PEB.BeingDebugged]
.text:004017B2 mov [ebp+beeingDebugged], al
.text:004017B5 cmp [ebp+beeingDebugged], 0
.text:004017B9 jz short loc_4017C3
.text:004017BB push 0 ; uExitCode
.text:004017BD call ExitProcess
Analogicznie do poprzedniego razu zmieniamy IP na 0x4017C3. Śledząc wykonywanie (F8) widzimy pobranie tekstu z kontrolki (0x4017FD, z użyciem GetWindowTextW) i jego konwersję z UNICODE na ANSI (0x0x40181C). Następnie tekst ten jest porównywany (przy użyciu inline’owanego strcmp) z „Let’s break to the other side.” – jest to pierwsze hasło. Aby pominąć konieczność ponownego wpisywania, tym razem prawidłowego, hasła wystarczy (będąc ’w’ strcmp) zmienić EIP na 0x40184C – adres epilogu strcmp ’zwracającego’ 0, oznaczające równość porównywanych c-stringów, lub też przejść za końcowy test wartości strcmp (0x401859). Następnie możemy zaobserwować, że wysyłany jest komunikat BM_SETCHECK (zaznaczający checkboksa) oraz zmieniana jest aktywność kontrolek – odblokowywany jest drugi etap zadania. Na tym kończy się metoda obsługująca pierwszy button „CHECK”, puszczamy więc program wolno (F9).
Z drugim polem postępujemy podobnie jak poprzednio, ponownie trafiamy do IsInsideVMWare, postępujemy tak jak poprzednio… Po wyjściu znajdujemy się wewnątrz sub_401900 (1):
.text:0040192D call IsInsideVMWare
.text:00401932 mov esi, FindWindowW ; (1)
.text:00401938 xor ebx, ebx ; (2)
.text:0040193A push ebx ; lpWindowName
.text:0040193B push offset aOllydbg ; "OLLYDBG"
.text:00401940 call esi ; FindWindowW
Należy tutaj zwrócić uwagę na instrukcję oznaczoną na listingu jako (2), jest to typowa dla kompilatora CL optymalizacja, wartość zerowa z ebx będzie wykorzystywana przez (niemal) całą resztę funkcji. Przed pominięciem ciągu testów instrukcja musi zostać wykonana, bądź też rejestr wyzerowany ręcznie.
W tym miejscu można już wysnuć wniosek, że przed każdym ważnym wykonywana będzie inline’owana funkcja. W rzeczywistości jest to nie tyle zabezpieczenie co hint ułatwiający analizę (oby zamierzony…). Najlepiej świadczy o tym rozpoczynanie testów od ’głośnej’ funkcji IsInsideVMWare.
Za pomijanymi FindWindowW napotykamy na:
.text:00401983 mov [ebp+NtGlobalFlags], ebx
.text:00401986 mov eax, large fs:30h
.text:0040198C mov eax, [eax+_PEB.NtGlobalFlag]
.text:0040198F mov [ebp+NtGlobalFlags], eax
.text:00401992 test byte ptr [ebp+NtGlobalFlags], 70h
.text:00401996 jz short loc_40199F
.text:00401998 push ebx ; uExitCode
.text:00401999 call ExitProcess
Jest to kolejny trick anty-debug, opierający się na obecności flag:
#define FLG_HEAP_ENABLE_TAIL_CHECK 0x00000010
#define FLG_HEAP_ENABLE_FREE_CHECK 0x00000020
#define FLG_HEAP_VALIDATE_PARAMETERS 0x00000040
Flagi te ustawiane są najczęściej z powodu obecności debuggera ring3. Docelowo zmieniamy EIP na 0x40199F. Do 0x4019F9 kod jest analogiczny do poprzedniego etapu.
.text:00401A19 push eax ; strlen(pass)
.text:00401A1A mov ecx, esi
.text:00401A1C lea eax, [ebp+pass]
.text:00401A1F call sub_401260 ; (1)
.text:00401A24 mov ecx, esi
.text:00401A26 mov eax, offset aRxzlcnl0agluzy ; "RXZlcnl0aGluZyBpcyBqdXN0IHRoZSBiZWdpbm5"...
.text:00401A2B jmp short loc_401A30 ; inline strcmp
Pod adresem 0x401A10 napotykamy inline’owaną funkcję strlen, której wynik razem ze stringiem przekazywany jest do funkcji sub_401260 (1). Na pierwszy rzut oka widać, iż jest to funkcja kodująca przy użyciu base64 (po zajrzeniu do jej wnętrza w oczy rzucają się charakterystyczne 3 odwołania do tablicy kodowej). Zakodowane hasło porównywane jest (0x401A30, inline strcmp) ze stringiem znajdującym się pod 0x544A80. Korzystając z IDAPython:
Python>print('"%s"' % GetString(0x544A80, -1, 0).decode('base64'))
"Everything is just the beginning, don't you think?"
Odczytaliśmy w ten sposób drugie z trzech haseł.
Tak jak w wypadku poprzedniej funkcji po dojściu do początku inline’owanego strcmp (0x401A30) zmieniamy EIP na 0x401A4C – ominiemy konieczność wpisania prawidłowego hasła. Śledząc dalej można zaobserwować, analogiczne do poprzednich, zmiany stanu kontrolek, następnie pobierany i testowany jest stan obu checkboksów (0x401AA8 i 0x401ABF). Jest to o tyle zabawne, że pierwszy został zaznaczony w poprzedniej funkcji, drugi dosłownie kilka instrukcji wcześniej (0x401A7C – zaznaczenie, 0x401ABF – sprawdzenie).
Następnie ukrywane jest główne okno (0x401AD3) i wyświetlany komunikat „Stage 3 started!” (0x401AD8). Ostatnią interesującą operacją jest zaalokowanie obiektu klasy CWinThread (0x401ADF) z funkcją pod adresem 0x401B70 jako argumentem i uruchomienie jej w nowym wątku (0x401B10). W takim układzie powinniśmy przejść do debugowania nowego wątku – zastawiamy breakpoint na jego entry point (przechodzimy pod 0x401B70 i wciskamy F2) i puszczamy program (F9). Warto zaznaczyc, że opcja Run to cursor nie działa (prawidłowo) pomiędzy wątkami.
W nowym wątku (znowu…) wita nas widziana poprzednio inline’owana funkcja odpowiedzialna za większość tricków antydebug. Trochę za wiele tych podpowiedzi… Po dojściu do wywołania IsInsideVMWare (0x401BA6) zmieniamy EIP na 0x401BFC (pamiętając o wyzerowaniu ebx) – dodatkowych tricków w asortymencie chyba brakło, nic poza inline’owanymi:
.text:00401BFC call $LN12_7 ; (1)
.text:00401C01 lea ecx, [esp+5Ch+socket]
.text:00401C05 call CSocket::CSocket(void)
.text:00401C0A push ebx ; lpszSocketAddress
.text:00401C0B push 111111b ; lEvent
.text:00401C0D push SOCK_DGRAM ; nSocketType
.text:00401C0F push 696 ; nSocketPort
.text:00401C14 lea ecx, [esp+6Ch+socket]
.text:00401C18 mov [esp+6Ch+var_4], ebx
.text:00401C1C call sub_41173B ; (2)
Pierwsze napotkane wywołanie funkcji (1) może rzucić wyjątkiem, jest to inicjalizacja WinSock, w takim wypadku wracamy do poprzedniego widoku
(powyższego, wciskając Esc), ustawiamy kursor na następnej instrukcji i używamy Run to cursor. Chociaż IDA nie rozpoznała metody wywoływanej na rzecz gniazda sieciowego (2) to mając pewne doświadczenie z MFC można łatwo się domyślić z czym mamy do czynienia – dla części klas właściwa inicjalizacja (ze względu na możliwość niepowodzenia) została wydzielona do oddzielnej (jawnie wywoływanej) metody Create, sockety także takową posiadają [3]. Jak widzimy, argument lpszSocketAddress przyjmuje wartość NULL, socket będzie służył wyłącznie odbieraniu pakietów UDP na porcie 696. Po prześledzeniu kilku kolejnych instrukcji dochodzimy do miejsca (1):
.text:00401C27 push ebx ; nFlags
.text:00401C28 lea eax, [esp+60h+sockAddrLen]
.text:00401C2C push eax ; lpSockAddrLen
.text:00401C2D mov eax, [esp+64h+socket]
.text:00401C31 lea ecx, [esp+64h+sockAddr]
.text:00401C35 push ecx ; lpSockAddr
.text:00401C36 push 5 ; nBufLen
.text:00401C38 lea edx, [esp+6Ch+srcBuf]
.text:00401C3C push edx ; lpBuf
.text:00401C3D mov edx, [eax+38h]
.text:00401C40 lea ecx, [esp+70h+socket]
.text:00401C44 mov dword ptr [esp+70h+sockAddrLen], size sockaddr
.text:00401C4C call edx ; (1)
IDA podpowiada iż jest to metoda CSocket::ReceiveFromHelper, w praktyce inline wywołania drugiej wersji przeciążonej metody RecieveFrom [4]. Rzut oka na resztę funkcji utwierdza nas w przekonaniu, iż program odbiera komendy po UDP i reaguje na nie w następujący sposób:
- „calc” – uruchomienie Kalkulatora;
- „help” – uruchomienie Pomocy Windows;
- „exit” – zamknięcie procesu;
- „more” – odpalenie funkcji sub_401D90 w nowym wątku.
Nas powinien interesować jedynie ostatni przypadek – możemy w tym miejscu postąpić dwojako:
- wysłać odpowiedni pakiet, chociażby korzystając z IDAPython:
Python>import socket
Python>socket.socket(type = socket.SOCK_DGRAM).sendto('more', ('localhost', 696))
- po prostu zmienić EIP na początek bloku obsługującego wiadomość ’more’ – 0x401CD9.
Niezależnie od przyjętej metody dochodzimy do uruchomienia nowego wątku, powinniśmy zastawić breakpoint na początek odpalanej funkcji (0x401D90) i puścić resztę wolno (F9).
Cała funkcja sprowadza się do wywołania sub_402080 i zamknięcia procesu, przejdźmy więc do pierwszej funkcji.
Ukryte zasoby
Po wstępnym przejrzeniu funkcji można wysnuć wniosek, iż wypakowuje ona plik wykonywalny (jako plik tymczasowy) z zasobów (zasób „video”, typu RC_DATA) następnie zaś uruchamia z flagą CREATE_SUSPENDED i zastępuje główny moduł aplikacji innym (przy pomocy własnego, uproszczonego loadera PE), także pochodzącym z zasobów („music”). W praktyce na tym można zakończyć analizę crackme.exe, warto jednak dokładniej zapoznać się z mechaniką loadera:
- zasób „video” wypakowywany jest do pliku tymczasowego (0x40209A-0x40212C);
- tworzony jest z niego nowy, wstrzymany, proces (0x40212C-0x402197);
- następuje załadowanie zasobu „music”;
- alokowana jest pamięć, wedle rozmiaru z nagłówka (OptionalHeader.SizeOfImage);
- przekopiowane zostają nagłówki (0x4021D1);
- następnie, zgodnie z nagłówkami sekcji, rozmieszczane są ich dane (0x4021D6-0x402239);
- alokowana jest pamięć, w której umieszczana jest instrukcja ’retn 4′ uruchamiana następnie jako nowy wątek w zdalnym procesie (0x402239-0x4022B1) ma to na celu wymuszenie prawidłowej inicjalizacji procesu (pierwotny główny wątek nie został nigdy uruchomiony – CREATE_SUSPENDED);
- przetwarzany jest katalog importów (0x4022B1-0x4023E4);
- przy pomocy ntdll!ZwUnmapViewOfSection usuwany jest obraz oryginalnego głównego modułu zdalnej aplikacji, następnie pod odpowiednim adresem umieszczany jest budowany właśnie przez loader (0x4023E4-0x402458);
- dostosowywane są atrybuty stron pamięci dla nagłówka i kolejncyh sekcji pliku wykonywalnego, zgodnie z ich charakterystykami (0x402458-0x40252C);
- uruchamiany jest nowy wątek, entry point nowego modułu, loader czeka na jego zakończenie (0x40252C-0x402561);
- pamięć używana przez loader do zbudowania modułu zostaje zwolniona (0x402569);
- plik tymczasowy jest usuwany (0x40257F);
We wspomnianej implementacji loadera nie obeszło się bez kilku nieco niepewnych rozwiązań.
Funkcja remoteLoadLibrary (0x401F80) tworzy payload (ignorujący przekazany argument) mający wywołać LoadLibrary w zdalnym procesie, jest to całkowicie zbędne ponieważ prototyp LoadLibrary jest względnie kompatybilny z prototypem funkcji nowego wątku (funkcja w konwencji stdcall przyjmująca wskaźnik). Argumentem dla nowego wątku powinien być adres nazwy biblioteki, zaś wywoływaną funkcją bezpośrednio LoadLibrary. Co więcej wynikiem działania wątku – dostępnym z użyciem GetExitCodeThread – jest uchwyt-adres załadowanego modułu (tak w wypadku użycia payloadu jak i wywołania bezpośredniego), który jednak nie jest zwracany do wywołującego, co stwarza konieczność późniejszego jego pobrania. Payload:
; odłożenie adresu nazwy biblioteki i 'skok' do wywołania LoadLibrary
;
call _callLoadLib
; ---------------------------------------------------------------------------
libName db 'LIB_NAME.DLL',0
db 243 dup(0)
; ---------------------------------------------------------------------------
_callLoadLib:
call LoadLibraryA
retn 4
Kolejnym niedopatrzeniem jest ładowanie biblioteki poprzez LoadLibrary w celu rozwiązania importów. Powoduje to załadowanie biblioteki w kontekście procesu loadera, wraz z zależnościami i wywołaniem DllMain. Może to być potencjalnie niebezpieczne, poza tym biblioteka może być zależna od głównego modułu aplikacji (jak chociażby pluginy wielu programów). Lepszym rozwiązaniem jest użycie LoadLibraryEx z flagą DONT_RESOLVE_DLL_REFERENCES.
Być może nieco pochopnym jest też umieszczanie nowego modułu bez sprawdzania konfliktu adresów – w tym konkretnym wypadku adres bazowy obu modułów jest taki sam. Podobnym niedopatrzeniem jest też oczekiwanie na zakończenie zdalnego wątku odpowiadającego entry pointowi modułu, jego zakończenie nie musi oznaczać zakończenia funkcjonowania programu, w efekcie czego usunięcie pliku tymczasowego mogłoby się nie powieść. W takich przypadkach zawsze należy oczekiwać na zasygnalizowanie uchwytu procesu.
Na koniec warto zauważyć, że argumenty remoteGetModuleHandle (0x401E40) są przekazywane z użyciem rejestrów, w tym także edi (zarezerwowanego przy standardowych konwencjach wywołania), co jest zapewne efektem użycia przełącznika /Og dla kompilatora CL – globalnej optymalizacji.
W tym momencie analiza crackme.exe jest zakończona, przejdźmy więc do dwóch pozostałych plików wykonywalnych. ’Host’ dla wstrzykiwanego modułu również może być interesujacy – z jakiegoś powodu nie wykorzystano istniejącej aplikacji (przeglądarki internetowej), jak miało to miejsce w crackme z roku 2008. Najpierw należy je wydobyć, w tym celu najlepiej posłużyć się jakimś edytorem zasobów lub PE, użyty wcześniej LordPE wystarczy.
Dla ułatwienia zapisujemy oba zasoby z sekcji RC_DATA pod nazwami plików odpowiadającymi nazwom zasobów – „video.exe” i „music.exe”. Ewentualnie można posłużyć się też pluginem PE Extract dla PEiD, który automatycznie wyszuka osadzone pliki PE i zapisze obok pliku źródłowego.
Najpierw host – „video.exe”. PEiD podaje, iż jest to ’Microsoft Visual Basic 5.0 / 6.0′. Faktycznie w importach figuruje „MSVBVM60.DLL”, co oznacza aplikację VB6 skompilowaną do formy P-Code. Spróbujmy uruchomić program. Pojawia się okno zatytułowane „Public key”, wyświetlające wartość „5518f65d” i nic więcej. Może być to kolejny hint do późniejszego wykorzystania. Na wszelki wypadek wypadałoby sprawdzić, czy program nie robi czegoś więcej – wrzucamy program w jakiś dezasembler P-Code, dla przykładu P32Dasm lub ew. VB Decompiler Lite (faktycznym dekompilatorem jest jedynie wersja płatna). Cóż, pusto, „video.exe” zawiera wyłącznie formę.
Pozostał ostatni moduł, „music.exe”. PEiD stwierdza, iż jest to ’MEW 11 SE 1.2 -> NorthFox/HCC’, może tym razem PEiD Generic Unpacker sobie poradzi. Odpalamy plugin, wykrywanie EIP, klikamy [Unpack], potwierdzamy odbudowanie importów – tym razem chyba się udało. Uruchamiamy „music.exe.unpacked_.exe”, faktycznie działa.
Szyfrowanie asymetryczne RSA
Otwarty dialog pozwala na podanie danych oraz pary kluczy – publicznego i prywatnego, zapewne mamy tu do czynienia z jakimś algorytmem asymetrycznym (najprawdopodobniej RSA, ze względu na jego powszechność). Dla testu klikamy [Decrypt], otrzymujemy komunikat „Bad Private Key :(„. Ładujemy program w P32Dasm, zaglądamy do Strings (F8), klikamy na wzmiankowany string – znaleźliśmy się w Command1.Click():
00003060: 04 FLdRfVar var_8C
00003063: 05 ImpAdLdRf
00003066: 24 NewIfNullPr
00003069: 0F VCallAd
0000306C: 19 FStAdFunc var_88
0000306F: 08 FLdPr var_88
00003072: 0D VCallHresult TextBox.Get_Text()
00003077: 6C ILdRf var_8C
0000307A: 1B LitStr: "2309cd31"
0000307D: FB30 EqStr =
0000307F: 2F FFree1Str var_8C
00003082: 1A FFree1Ad var_88
00003085: 1C BranchF 00003117
...
00003117: loc_00003085
00003117: 27 LitVar_Missing
0000311A: 27 LitVar_Missing
0000311D: 27 LitVar_Missing
00003120: F5 LitI4: 0 0x0
00003125: 3A LitVarStr: "Bad Private Key :("
0000312A: 4E FStVarCopyObj var_D0
0000312D: 04 FLdRfVar var_D0
00003130: 0A ImpAdCallFPR4 MsgBox()
Wiemy już jaką wartość powinien mieć klucz prywatny, wprowadzamy go w odpowiednie pole i ponownie klikamy [Decrypt] – „Run-time error ’13’: Type mismatch”. Cóż, ktoś tu zapomniał o walidacji argumentów, widocznie wszystkie pola powinny być wypełnione hexstringami…
W tym momencie należy wrócić na chwilę do poprzedniego programu – zawierał klucz publiczny. Twórcy malware’u generalnie nie są zbyt kreatywni, możliwe iż klucze powiedzą nam coś o użytym algorytmie – wrzucamy parę kluczy w dowolną wyszukiwarkę internetową. Istotnie, jedynym sensownym wynikiem wyszukiwania jest moduł dla VB służący implementacji RSA [5], zaś klucze pochodzą z jego przykładów:
' Do a mini-RSA encryption with 32-bit key:
' Public key (n, e) = (0x5518f65d, 0x11)
' Private key d = 0x2309cd31
' Message m = 0x35b9a3cb
' Encrypt c = m^e mod n = 35b9a3cb^11 mod 5518f65d = 528C41E5
' Decrypt m' = c^e mod n = 528C41E5^2309cd31 mod 5518f65d = 35B9A3CB
strResult = mpModExp("35b9a3cb", "11", "5518f65d")
Debug.Print strResult & " (expected 528C41E5)"
strResult = mpModExp("528C41E5", "2309cd31", "5518f65d")
Debug.Print strResult & " (expected 35B9A3CB)"
Zdecydowanie ciekawym jest fragment licencji biblioteki:
' You are free to use this code as part of your own applications
' provided you keep this copyright notice intact and acknowledge
' its authorship with the words:
' "Contains cryptography software by David Ireland of
' DI Management Services Pty Ltd ."
Cóż, autor crackme chyba jej nie czytał. W każdym bądź razie wypadałoby się jeszcze upewnić, czy faktycznie jest to ta właśnie biblioteka, niezmieniona – wprowadzamy do odpowiednich pól dane z adekwatnego przykładu, klikamy [Decrypt], otrzymujemy wynik zgodny z dokumentacją. Skorzystanie z wyszukiwarki internetowej oszczędziło nam sporo czasu – nie musimy przeprowadzać analizy i testów większej części aplikacji. Zobaczmy teraz co dzieje się z wynikiem szyfrowania:
000030CD: 3E FLdZeroAd var_A4
000030D0: FDC7 PopTmpLdAdStr var_A8
000030D4: 3E FLdZeroAd var_98
000030D7: FDC7 PopTmpLdAdStr var_9C
000030DB: 3E FLdZeroAd var_8C
000030DE: FDC7 PopTmpLdAdStr var_90
000030E2: 0B ImpAdCallI2 Module2 2.1
000030E7: 23 FStStrNoPop var_AC
000030EA: 05 ImpAdLdRf
000030ED: 24 NewIfNullPr
000030F0: 0F VCallAd
000030F3: 19 FStAdFunc var_B0
000030F6: 08 FLdPr var_B0
000030F9: 0D VCallHresult TextBox.Set_Text()
000030FE: 32 FFreeStr var_90 var_9C var_A8 var_AC
00003109: 29 FFreeAd: var_88 var_94 var_A0 var_B0
00003114: 1E Branch 00003140
...
00003140: loc_00003114
00003140: 04 FLdRfVar var_8C
00003143: 05 ImpAdLdRf
00003146: 24 NewIfNullPr
00003149: 0F VCallAd
0000314C: 19 FStAdFunc var_88
0000314F: 08 FLdPr var_88
00003152: 0D VCallHresult TextBox.Get_Text()
00003157: 6C ILdRf var_8C
0000315A: 4A FnLenStr Len()
0000315B: F5 LitI4: 4 0x4
00003160: C7 EqI4 =
00003161: 2F FFree1Str var_8C
00003164: 1A FFree1Ad var_88
00003167: 1C BranchF 00003195
0000316A: 05 ImpAdLdRf
0000316D: 56 NewIfNullAd
00003170: FD9C FStAdNoPop
00003174: 05 ImpAdLdRf
00003177: 24 NewIfNullPr
0000317A: 0D VCallHresult Global._Load(object As IDispatch)
0000317F: 1A FFree1Ad var_88
00003182: 27 LitVar_Missing
00003185: 25 PopAdLdVar
00003186: 27 LitVar_Missing
00003189: 25 PopAdLdVar
0000318A: 05 ImpAdLdRf
0000318D: 24 NewIfNullPr
00003190: 0D VCallHresult Form._Show(Modal As Variant, OwnerForm As Variant)
00003195: loc_00003167
00003195: 13 ExitProcHresult
Jeżeli długość tekstu ustawionego jako wynik operacji jest równa cztery to ładowane i otwierane jest nowe okno. Korzystając z Pythona (IDA powinna jeszcze gdzieś w tle się znajdować) szyfrujemy, dla przykładu ’3537′:
Python>print '%x' % ((0x3537 ** 0x11) % 0x5518f65d)
1df20b9b
Wprowadzamy otrzymany hexstring w pierwsze pole tekstowe, wciskamy [Decrypt]. Otworzyło się okno ’Final’ zawierające pole na klucz, przycisk „Try the key” i pole, w którym powinno zostać wyświetlone ostatnie hasło. Z braku lepszej alternatywy ponownie zaglądamy do diassemblera, poprzez okno Procedures przechodzimy do Form2 -> Command1.Click(). Widzimy tam wywołanie funkcji oznaczonej jako „Module1 1.3”, z dwoma argumentami – pobranym z edita kluczem i stringiem:
„112D2330206235622B2A66272B2166362A65322A2065332C2C3323303620686C6B”
Funkcja ta składa się z prostej pętli liczącej od 1 do połowy długości hexstringa (tablice w VB indeksowane są od 1, nie od 0), odkodowującej kolejne wartości bajtów (po dwa znaki) i wykonujące na nich operację xor z kolejnymi znakami cyklicznie zapętlonego klucza. Należy zwrócić uwagę na fragment:
00002E66: 6C ILdRf var_8C
00002E69: 80 ILdI4 param_C
00002E6C: 4A FnLenStr Len()
00002E6D: C2 ModI4 Mod
00002E6E: F5 LitI4: 1 0x1
00002E73: AA AddI4 +
00002E74: 80 ILdI4 param_C
00002E77: 0B ImpAdCallI2 Mid$()
W pierwszej iteracji pętli wartość zmiennej sterującej (var_8C) wynosi 1, więc pierwszym branym pod uwagę znakiem klucza nie jest pierwszy lecz drugi (1 % len + 1 = 2).
Rozpiszmy w takim razie hexstring na kolejne bajty:
11 2D 23 30 20 62 35 62 2B 2A 66 27 2B 21 66 36 2A 65 32 2A 20 65 33 2C 2C 33 23 30 36 20 68 6C 6B
Możliwe podejścia są dwa – bruteforce, bądź kryptoanaliza. Ręczne odszyfrowanie nie powinno być specjalnie trudne, przyjmując odpowiednie założenia na bazie poprzednich haseł:
- zdanie zaczyna się wielką literą;
- jest zakończone znakiem interpunkcyjnym;
- to prawidłowy tekst (prawdopodobnie) w języku angielskim, zawiera spacje.
Potencjalnie można przyjąć też dodatkowe założenia, odszyfrowany klucz z poprzedniego etapu może służyć do odszyfrowania hasła, musi więc mieć formę czteroznakowego hexstringa. Jest to jednak dosyć ryzykowne, na razie spróbujmy obejść się bez tego.
Hasło powinno zaczynać się wielką literą – zaszyfrowany znak ma niską wartość, bit różniący małe i wielkie litery jest wyzerowany, można więc wysnuć wniosek, że pierwszy znak klucza również jest wielką literą.
Tekst składa się głównie z niewielkich wartości, zapewne małych liter, przedzielonych wartościami około 0x65 – znakami interpunkcyjnymi.
Przyglądając się wartościom znaków można zauważyć, że są one niewielkie – znaki klucza są podobne do siebie, do tego są to wielkie litery (analogiczna prawidłowość jak w przypadku pierwszej litery).
Ostatnie trzy znaki są chyba znakami interpunkcyjnymi – wielokropek? Spróbujmy pogrupować tekst wedle podobieństwa występujących w nim znaków interpunkcyjnych (potencjalnych spacji – najczęściej występujące wartości to 62, 65 i 66), z jak najkrótszym cyklem klucza:
11 2D 23 30 20 62 35 62 2B 2A 66 27 2B 21 66 36 2A 65 32 2A 20 65 33 2C 2C 33 23 30 36 20 68 6C 6B
Sprawdźmy teraz nasze założenia – w trzeciej kolumnie mamy potencjalną kropkę i spację:
0x66 ^ 0x20 (' ') = 0x46 ('F')
0x68 ^ 0x2e ('.') = 0x46 ('F')
Wygląda dobrze. Dla drugiej kolumny wyliczamy klucz adekwatny dla spacji:
0x65 ^ 0x20 (' ') = 0x45 ('E')
W wypadku ostatniej kolumny możemy podejrzewać, że 0x62 również jest spacją:
0x62 ^ 0x20 (' ') = 0x4B ('B')
0x6C ^ 0x2e ('.') = 0x4B ('B')
Pozostała pierwsza, tutaj możemy polegać wyłącznie na kropce:
0x6B ^ 0x2e ('.') = 0x45 ('E')
Fin
Składając razem (i pamiętając o przesunięciu klucza przy deszyfrowaniu) otrzymujemy ’BEEF’, przetestujmy klucz. Wygląda na to, że jest on prawidłowy, otrzymaliśmy ostatnie, trzecie hasło:
„There’s no end to the universe…”
Warto zauważyć, iż klucz faktycznie składa się z czterech cyfr szesnastkowych, zaś wyjście w oknie z RSA zawiera wielkie litery. Najciekawszym jest jednak fakt, iż przykład poprzedzający zacytowany wcześniej używa „beef” jako wiadomości, ta zaś po zaszyfrowaniu i odszyfrowaniu via „music.exe” przyjmie formę właśnie „BEEF”. Trudno uznać to za przypadek…
Dodatkowe materiały
[1] – http://www.codeproject.com/KB/system/VmDetect.aspx
[2] – https://msdn.microsoft.com/en-us/library/ms684280(VS.85).aspx
[3] – https://msdn.microsoft.com/en-us/library/xz019029(v=VS.80).aspx
[4] – https://msdn.microsoft.com/en-us/library/2yay34ef(v=VS.80).aspx
[5] – http://www.di-mgt.com.au/src/basModExp.bas.html