13.07.2016 | 19:01

Adam Haertle

Jak napisać ciekawe CrackMe na CTF – instrukcja krok po kroku

Jeśli brakowało Wam ostatnio na naszej stronie tekstów o bardziej technicznym charakterze, to mamy nadzieję, że po lekturze poniższego artykułu będziecie zadowoleni. Autorem wpisu jest Bartosz Wójcik. Dziękujemy!

W dobie coraz popularniejszych konkursów CTF (ang. Capture The Flag) i dominacji na tym polu polskich ekip, takich jak m.in. Dragon Sector, pomyślałem, że ciekawym tematem byłoby zaprezentowanie, jak stworzyć proste CrackMe, wraz ze wskazaniem oryginalnych technik utrudniających jego złamanie i analizę.

crackme-z3s

Jeśli sam kiedykolwiek interesowałeś się inżynierią wsteczną (ang. reverse engineering), próbowałeś sił w konkursach CTF lub chciałeś sam stworzyć CrackMe i utrzeć nosa urwisom z innych zespołów CTF to jest to artykuł dla Ciebie.

Znasz temat? Nie czytaj dalej, tylko spróbuj zdobyć flagę!

Z góry powiem, że jeśli znasz temat i chcesz spróbować swoich sił w CrackMe, które jest tutaj opisane, przestań czytać dalszą część, w której znajdziesz szczegółowy opis mechanizmów zabezpieczeń, pobierz skompilowane CrackMe i spróbuj zdobyć flagę, a dopiero później wróć do artykułu żeby zweryfikować fakty. Poziom CrackMe, jeśli stosować skalę easy, medium i hard oceniam na medium.

Pobierz CrackMeZ3S.zip

Do uruchomienia CrackMe być może będziesz potrzebował bibliotek Visual C++ Redistributable Packages for Visual Studio 2013.

Jeśli pobrałeś CrackMe to nie bądź chytrym liskiem i nie czytaj dalej. Jeśli jednak chciałbyś się dowiedzieć, jak stworzyć CrackMe to zapraszam do dalszej lektury.

Co to jest CrackMe?

Jeśli jesteś czytelnikiem Z3S to zapewne wiesz co to HackMe, może to być specjalnie przygotowana strona internetowa, w której należy znaleźć lukę lub wykorzystać jakąś słabość, aby dostać się do ukrytej treści.

CrackMe to nic innego jak program komputerowy, stworzony tylko i wyłączanie w celu obejścia jego zabezpieczeń i uzyskania poprawnego hasła czy klucza seryjnego. CrackMe były popularne znacznie wcześniej, niż same konkursy CTF, koderzy mogli dzięki temu w kreatywny sposób rywalizować pomysłami na zabezpieczenia z crackerami, którzy próbowali je przełamać.

crackme-z3s-strona-crackmes-de

Do dzisiaj istnieje strona crackmes.de, gdzie znajduje się działające od 20 lat (sic!) archiwum z prawie 3000 (!) CrackMe oraz tutorialami do nich, strona jest aktywna i cały czas dodawane są nowe CrackMe.

Jakie są rodzaje CrackMe?

Jeśli wchodzić w szczegóły to programy CrackMe tradycyjnie dzielą się na kategorie w zależności od tego co jest celem ustalonym przez autora.

Można wyróżnić:

  • CrackMe – celem jest zwyczajowo wygenerowanie klucza seryjnego, pliku rejestracyjnego lub kombinacji „nazwa użytkownika” i pasującego do niego klucza. Wykluczone jest modyfikowanie pliku aplikacji, bo to nie jest cel i tradycyjne CrackMe nie są zabezpieczone przed modyfikacjami w binarce.
  • KeygenMe – jak nazwa wskazuje, należy stworzyć generator kluczy, różni się tym od zwykłego CrackMe, że zwykle wykorzystuje ciekawe algorytmy kryptograficzne i należy tutaj posiadać wiedzę właśnie na temat kryptografii oraz metod szyfrowania, aby utworzyć generator kluczy. Często wykorzystywane są liczby typu BIGNUM oraz algorytmy takie jak ECC, RSA czy DSA, które wymagają łamania wykorzystanych kluczy techniką brute-force.
  • ReverseMe – najbardziej skomplikowana forma CrackMe, której celem jest doprowadzenie np. do sytuacji w której ReverseMe wyświetli jakiś komunikat, np. „Dziękuję za rejestrację”. ReverseMe często są nie tylko bardzo dobrze zabezpieczone jeśli chodzi o same algorytmy rejestracyjne, ale także wykorzystują szereg technik utrudniających zmodyfikowanie pliku aplikacji, bo to jest najczęściej wymagane do osiągnięcia założonego celu (np. dopisanie jakiejś funkcjonalności).
  • UnpackMe – trochę inna forma CrackMe, w której otrzymujemy plik skompresowany, zabezpieczony lub poddany obfuskacji własnym lub komercyjnym oprogramowaniem np. exe-pakerem czy exe-protectorem i naszym celem jest rozpakowanie pliku, czyli przywróceniem go do stanu pierwotnego, najczęściej chodzi o odbudowę tabeli importów, przywrócenie oryginalnego kodu, odtworzenie struktur pliku wykonywalnego, tak żeby plik aplikacji działał bez warstwy zabezpieczenia. W przypadku własnych rozwiązań mogą to być fajne i ciekawe zadania, jednak jeśli zastosowane zostanie komercyjne zabezpieczenie jest to już zabawa dla prawdziwych hardcorów.

Cel naszego CrackMe

W wersjach na konkursy CTF celem jest zwykle zdobycie ukrytej „flagi”. Celem naszego CrackMe będzie odgadnięcie oraz ustawienie poprawnych „kluczy” dostępowych, po odgadnięciu których uzyskamy flagę. Nasze klucze będziemy pobierać z różnych źródeł dla urozmaicenia rozrywki osób próbujących je uzyskać.

Każdy klucz będzie posiadał dodatkową i prostą weryfikację, tak aby nie za bardzo komplikować całości.

System operacyjny i język programowania

Docelowe CrackMe zostanie stworzone na platformę Windows 10 (bez problemu działa także na starszych Windows np. 7) z wykorzystaniem języka C++ skompilowanego do natywnego kodu x86. Wykorzystamy kilka ciekawych mechanizmów i funkcji WinAPI, które być może nie są powszechnie znane. W CrackMe wykorzystamy kodowanie znaków UNICODE, co również może spowodować małe problemy w różnego rodzaju narzędziach do reversingu.

TLS Callbacks

Na dzień dobry wykorzystamy mało znany mechanizm TLS Callbacks. Związany jest on z funkcjonowaniem mechanizmu Thread Local Storage, który pozwala wielowątkowym aplikacjom na wykorzystywanie tych samych zmiennych globalnych, które jednak będą miały inne wartości dla każdego wątku programu. Przykładowo deklarujemy w C++ zmienną i oznaczamy ją specjalnym znacznikiem:

__declspec(thread) int wartosc;

W tym wypadku, każdy wątek w aplikacji będzie posiadał odrębną kopię tej wartości dla swojej dyspozycji.

W ramach tego mechanizmu istnieje coś takiego jak TLS Callbacks. Można to przyrównać do funkcji wejściowej dla bibliotek dynamicznych DLL – czyli DllMain(). System Windows wywoła funkcję zadeklarowaną jako TLS Callback, aby poinformować o przyłączeniu do procesu aplikacji, nowo załadowanych bibliotekach czy utworzonych wątkach, tak samo jak wielokrotnie wywołuje DllMain(), z tą małą różnicą, że w przypadku wykorzystania tego mechanizmu w aplikacji EXE, kod callbacka zostanie wywołany przed samym punktem wejściowym aplikacji (tzw. entrypoint).

Jest to bardzo istotny szczegół, ponieważ teoretycznie pozwala niezauważenie uruchomić dowolny kod, na który nikt nie zwróci uwagi bez wykorzystania odpowiednich opcji debuggera.

Mechanizm TLS Callbacks funkcjonuje od czasów Windows XP, jednak jego działanie nie jest w 100% takie samo dla różnych wersji Windows (nie wszystkie zdarzenia są obsługiwane). Jest on wykorzystywany przez niektóre systemy ochrony oprogramowania do uruchamiania kodu anty-debug przed uruchomieniem samego kodu aplikacji.

W naszym CrackMe wykorzystamy mechanizm TLS Callbacks również do sprawdzenia obecności debuggera.

///////////////////////////////////////////////////////////////////////////////
//
// mechanizm TLS callback pozwala na wykonanie kodu przed
// wejsciem programu do punktu wejsciowego (EP), mozna tutaj
// ukryc inicjalizacje kilku rzeczy
//
// szczegoly implementacji w C++
// http://stackoverflow.com/questions/14538159/about-tls-callback-in-windows
//
///////////////////////////////////////////////////////////////////////////////

void NTAPI TlsCallback(PVOID DllHandle, DWORD dwReason, PVOID)
{
	// powod wywolania callbacka - dolaczenie do procesu
	// czyli uruchomienie aplikacji, dokladnie tak samo
	// jak w przypadku funkcji DllMain() w bibliotekach DLL
	// jesli to inny powod - wyjdz
	if (dwReason != DLL_PROCESS_ATTACH)
	{
		return;
	}

	// sprawdz flagi sterty, w przypadku debuggowanej
	// aplikacji sa one inne niz w przypadku normalnie
	// uruchomionej aplikacji, w razie wykrycia debuggera
	// zablokuj dzialanie aplikacji w tym punkcie
	__asm
	{
		mov     eax, dword ptr fs:[30h]
		test	dword ptr [eax + 68h], HEAP_REALLOC_IN_PLACE_ONLY or HEAP_TAIL_CHECKING_ENABLED or HEAP_FREE_CHECKING_ENABLED
		je		_no_debugger

		_spij_slodko_aniolku:

		push	1000000
		call	Sleep

		jmp		_spij_slodko_aniolku

		_no_debugger:
	}
}

Jeśli uruchomimy CrackMe pod takim debuggerem jak np. OllyDbg v2 bez żadnych wtyczek ukrywających jego obecność, kod w TLS Callback wykryje jego działanie i zablokuje dalsze ładowanie aplikacji, wszystko będzie wyglądało jakby stanęło w miejscu.

Sprawdzanie kluczy

Metody sprawdzania kluczy będą działać w odrębnych wątkach. Wielowątkowość zawsze jest mniejszą lub większą przeszkodą w debugowaniu aplikacji. Kolejne funkcje weryfikujące klucze będą kaskadowo tworzyć wątki dla następnych metod sprawdzających.

//
// tablica z adresami kolejnych funkcji sprawdzajacych
// klucze dostepowe, wskazniki beda w niej zaszyfrowane
// i odszyfrowane jedynie w momencie uruchomienia kolejnego
//
// zapisujemy tutaj adresy przesuniete o 100 bajtow do przodu
// w kazdym deasemblerze spowoduje to maly "zamet", bo zostanie
// to odczytane jako wskaznik do funkcji, dla wiekszej rozrywki
// mozna tu zapisac wiecej tych elementow
//
#define ENCRYPTED_PTR(x, y) reinterpret_cast(reinterpret_cast(&x) + y) 

PVOID lpKeyProc[KEYS_COUNT] = {

	ENCRYPTED_PTR(Klucz0, 100),
	ENCRYPTED_PTR(Klucz1, 100),
	ENCRYPTED_PTR(Klucz2, 100),
	ENCRYPTED_PTR(Klucz3, 100),
	ENCRYPTED_PTR(Klucz4, 100),
	ENCRYPTED_PTR(Klucz5, 100),

};

SpeedStart('C');

//
// utworz 5 obiektow EVENT, ktore beda sluzyly
// jako znaczniki poprawnosci kluczy dostepowych
// dodatkowo zaszyfruj wskazniki do funkcji
// sprawdzajacych poprawnosci kluczy
//
for (int i = 0; i < KEYS_COUNT; i++)
{
	hEvents[i] = CreateEvent(nullptr, TRUE, FALSE, nullptr);
	lpKeyProc[i] = static_cast(EncodePointer(reinterpret_cast(reinterpret_cast(lpKeyProc[i]) - 100)));
}

//
// odpal pierwszy watek do falszywego sprawdzenia numeru seryjnego
// w nim zostana uruchomione kolejne watki, do kolejnych metod
// sprawdzajacych klucze dostepowe
//
hThreads[0] = CreateThread(nullptr, 0, static_cast(DecodePointer(lpKeyProc[0])), lpKeyProc, 0, &dwThreadIds[0]);

SpeedEnd('C');

// zaczekaj az wszystkie watki zostana zainicjalizowane (jakby ktos
// chcial cos pominac), watki sa odpalane kaskadowo, wiec ich
// wszystkie uchwyty nie beda w tym momencie ustawione, dlatego nie
// mozna od razu uzyc WaitForMultipleObjects()
for (int i = 0; i < _countof(hThreads); i++)
{
	while (hThreads[i] == nullptr)
	{
		OutputDebugString(_T("Co slychac doktorku?"));
	}
}

// zaczekaj az watki zakoncza prace
WaitForMultipleObjects(_countof(hThreads), hThreads, TRUE, INFINITE);

Po wykryciu poprawnego klucza dostępowego, wykorzystamy system zdarzeń (ang. event) do oznaczenia, które klucze dostępowe zostały poprawnie ustawione.

Fałszywy klucz

Zwykle CrackMe wymagają podania numeru seryjnego lub hasła i o tym też nie zapomnimy! W naszym CrackMe będziemy prosić o podanie hasła, sprawdzać skrupulatnie jego poprawność i zapisywać wynik sprawdzenia, jednak w decydującym punkcie CrackMe będzie to ignorowane.

Będzie to jedyny klucz, o jaki poprosi CrackMe z poziomu konsoli, czyli najbardziej będzie rzucał się w oczy. Jednak nasz klucz będzie tylko dodany dla zmyłki.

Dla wciągnięcia atakującego w rozgrywkę, wykorzystamy najpopularniejsze i przestarzałe hashowanie danych bazujące na algorytmie MD5. Wynik porównamy z zakodowanym na stałe hashem dla słowa „fake”.

Hash dla tego krótkiego słowa można będzie bez problemu znaleźć w tablicach z obliczonymi wartościami hash dla popularnych słów i kombinacji literowych tzw. rainbow tables lub wykorzystując łamacz haseł, np. John the Ripper czy hashcat.

///////////////////////////////////////////////////////////////////////////////
//
// Falszywy klucz - dla zmarnowania czasu ;)
//
///////////////////////////////////////////////////////////////////////////////

DWORD WINAPI Klucz0(LPTHREAD_START_ROUTINE lpKeyProc[])
{
	// odpal kolejny watek (kaskadowo)
	hThreads[1] = CreateThread(nullptr, 0, static_cast(DecodePointer(lpKeyProc[1])), lpKeyProc, 0, &dwThreadIds[1]);

	_tprintf(_T("Podaj tajne haslo: "));

	// odczytaj haslo jako ciag ANSI (zeby latwiej atakujacym bylo
	// znalezc niepoprawne haslo stosujac np. rainbow tables, juz
	// pojdziemy im na reke i nie bedziemy pobierac tego jako UNICODE
	gets_s(szPassword, sizeof(szPassword));

	// zacznij mierzyc czas tylko od tego momentu gets_s() by
	// to sztucznie wydluzyl
	SpeedStart('0');

	if (strlen(szPassword) > 0)
	{
		// encrypted with https://www.stringencrypt.com (v1.1.0) [C/C++]
		// szFakeHash = "144C9DEFAC04969C7BFAD8EFAA8EA194"
		unsigned char szFakeHash[33];

		szFakeHash[2] = 0xA8; szFakeHash[0] = 0xCD; szFakeHash[10] = 0xBC; szFakeHash[30] = 0x28;
		szFakeHash[16] = 0x0A; szFakeHash[13] = 0x0D; szFakeHash[29] = 0x76; szFakeHash[14] = 0x30;
		szFakeHash[12] = 0x01; szFakeHash[32] = 0xEC; szFakeHash[3] = 0xCE; szFakeHash[31] = 0x3B;
		szFakeHash[15] = 0x48; szFakeHash[1] = 0x33; szFakeHash[25] = 0x27; szFakeHash[27] = 0xD9;
		szFakeHash[9] = 0x5F; szFakeHash[17] = 0x93; szFakeHash[24] = 0x8B; szFakeHash[7] = 0x9C;
		szFakeHash[26] = 0x5A; szFakeHash[23] = 0x24; szFakeHash[18] = 0x66; szFakeHash[19] = 0x06;
		szFakeHash[5] = 0xC1; szFakeHash[28] = 0x69; szFakeHash[21] = 0xF8; szFakeHash[20] = 0x9D;
		szFakeHash[4] = 0xFC; szFakeHash[22] = 0x44; szFakeHash[6] = 0xFF; szFakeHash[11] = 0x42;
		szFakeHash[8] = 0x83;

		for (unsigned int GpjcO = 0, qeVjl; GpjcO < 33; GpjcO++) { qeVjl = szFakeHash[GpjcO]; qeVjl = (((qeVjl & 0xFF) >> 2) | (qeVjl << 6)) & 0xFF; qeVjl += GpjcO; qeVjl = (((qeVjl & 0xFF) >> 5) | (qeVjl << 3)) & 0xFF;
			qeVjl ^= 0xF7;
			qeVjl = ~qeVjl;
			qeVjl ^= GpjcO;
			qeVjl--;
			qeVjl = ~qeVjl;
			qeVjl -= 0xDF;
			qeVjl = ((qeVjl << 6) | ((qeVjl & 0xFF) >> 2)) & 0xFF;
			qeVjl--;
			qeVjl ^= 0x76;
			qeVjl += 0xF0;
			qeVjl -= GpjcO;
			qeVjl ^= GpjcO;
			qeVjl = ~qeVjl;
			qeVjl += GpjcO;
			qeVjl = (((qeVjl & 0xFF) >> 2) | (qeVjl << 6)) & 0xFF;
			qeVjl += 0x2C;
			qeVjl = ((qeVjl << 4) | ((qeVjl & 0xFF) >> 4)) & 0xFF;
			qeVjl -= 0xFF;
			qeVjl = ((qeVjl << 1) | ((qeVjl & 0xFF) >> 7)) & 0xFF;
			qeVjl = ~qeVjl;
			qeVjl++;
			qeVjl = (((qeVjl & 0xFF) >> 4) | (qeVjl << 4)) & 0xFF; qeVjl -= 0xEF; qeVjl = (((qeVjl & 0xFF) >> 2) | (qeVjl << 6)) & 0xFF; qeVjl -= 0xF7; qeVjl = (((qeVjl & 0xFF) >> 3) | (qeVjl << 5)) & 0xFF; qeVjl -= 0x48; qeVjl = ~qeVjl; qeVjl -= GpjcO; qeVjl ^= GpjcO; qeVjl += 0xE6; qeVjl ^= 0xB4; qeVjl -= 0x9D; qeVjl = ~qeVjl; qeVjl--; qeVjl ^= GpjcO; qeVjl += 0x17; qeVjl ^= 0x55; qeVjl += GpjcO; qeVjl += 0xB3; qeVjl = (((qeVjl & 0xFF) >> 3) | (qeVjl << 5)) & 0xFF;
			qeVjl -= 0xCE;
			qeVjl = ~qeVjl;
			qeVjl += 0x9B;
			qeVjl ^= 0x71;
			qeVjl--;
			qeVjl = ((qeVjl << 7) | ((qeVjl & 0xFF) >> 1)) & 0xFF;
			szFakeHash[GpjcO] = qeVjl;
		}

		// sprawdzamy hash ze slowa "fake" (https://www.pelock.com/products/hash-calculator)
		if (CheckMD5(szPassword, strlen(szPassword), reinterpret_cast(szFakeHash)) == TRUE)
		{
			SetEvent(hEvents[0]);
		}
	}

	SpeedEnd('0');

	return 0;
}

Należy pamiętać, że wydłużanie pracy przy analizie kodu jest jedną z najlepszych metod, skutecznie zniechęcającą do analizy we wszelkiego rodzaju zabezpieczeniach i nie należy ignorować tego typu zabezpieczeń ani ich znaczenia. Osoba przeprowadzająca analizę może się po prostu znudzić lub sfrustrować znajdując takie „udogodnienia”, a dla nas będzie to tylko na rękę.

crackme-z3s-mr-burns-deception

Jako ciekawostkę mogę powiedzieć, że często popularne systemy zabezpieczeń do gier nie są projektowane jako „niełamalne”, ale służą jedynie wydłużeniu czasu, w jakim producenci gier będą w stanie sprzedaż maksymalną liczbę kopii w okresie premiery gry. Złamanie zabezpieczenia w takim wypadku i nagłówki prasowe mówiące o tym, że „poległo” jakieś zabezpieczenie to tylko iluzoryczne zwycięstwo crackerów i piratów, którzy kolejny raz pełni zadowolenia i ze stałą pieśnią na ustach „wszystko jest do złamania” mogą się poklepać po plecach, nawet nie zdając sobie sprawy, kto tak naprawdę jest tutaj wygranym.

Klucz 1 – zmienna środowiskowa

Jednym z kluczy będzie zmienna środowiskowa, która musi być poprawnie ustawiona np. poprzez systemowy edytor zmiennych środowiskowych. Dla utrudnienia sprawdzimy standardową zmienną środowiskową Windows – „PROCESSOR_ARCHITECTURE” jednak z drobną literówką, zamiast dwóch „S” będziemy szukać zmiennej „PROCESOR_ARCHITECTURE”.

Sprawdzana wartość będzie taka, jaka jest ustawiona w 64 bitowych systemach z dodatkową spacją na końcu, czyli „AMD64 ”.

Ktoś, kto sprawdzi sobie zmienne środowiskowe np. wykonując komendę konsolową „set”, na pewno zauważy tą wartość, pytanie czy zauważy literówkę oraz dodatkową spację? W Windows 10 można szybko zmienną ustawić z linii komend:

set PROCESOR_ARCHITECTURE=AMD64 <- spacja na końcu

lub przez edytor zmiennych środowiskowych, uruchamiając go przez kombinację klawiszy WIN+R i wpisując „sysdm.cpl”.

Klucz 2 – ukryty klucz ADS

System plików NTFS umożliwia zapisywanie dodatkowych „streamów” w plikach, nazywa się to Alternate Data Stream i można dzięki temu ukryć dodatkowe dane w plikach, tak, że nie będą one widoczne z poziomu Eksploratora Windows. Przykładowo jest to wykorzystywane przez przeglądarki, gdy pobieramy jakieś pliki z Internetu, to na dysku oprócz pobranego pliku, jest tworzony dla niego dodatkowy stream „plik.zip:ZoneIdentifier” z identyfikatorem strefy, określającej gdzie taki plik może być używany. Stąd te wszystkie denerwujące komunikaty ostrzegawcze, gdy próbujesz otworzyć cokolwiek pobranego z Internetu.

crackme-z3s-ads-identyfikator-strefy

Mechanizm ADS jest także wykorzystywany przez malware do ukrywania danych, jednak to ciekawa metoda do wykorzystania w CrackMe i posłużymy się nią do ukrycia kolejnego klucza.

Będziemy go poszukiwać w samym pliku CrackMe, dokładnie w streamie „CrackMeZ3S.exe:Z3S.txt”.

///////////////////////////////////////////////////////////////////////////////
//
// Klucz 2 - sprawdzanie ADS
//
///////////////////////////////////////////////////////////////////////////////

DWORD WINAPI Klucz2(LPTHREAD_START_ROUTINE lpKeyProc[])
{
	SpeedStart('2');

	// odpal kolejny watek (kaskadowo)
	hThreads[3] = CreateThread(nullptr, 0, static_cast(DecodePointer(lpKeyProc[3])), lpKeyProc, 0, &dwThreadIds[3]);

	TCHAR wszPath[512] = { 0 };

	// pobierz sciezke do pliku CrackMe
	GetModuleFileName(GetModuleHandle(nullptr), wszPath, sizeof(wszPath));

	// dodaj sciezke do ADS
	_tcscat_s(wszPath, _countof(wszPath), _T(":Z3S.txt"));

	// otworz plik "CrackMeZ3S.exe:Z3S.txt"
	HANDLE hFile = CreateFile(wszPath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);

	SpeedEnd('2');

	// czy udalo sie otworzyc plik?
	if (hFile == INVALID_HANDLE_VALUE)
	{
		return 0;
	}

	// pobierz rozmiar tego pliku (sprawdz czy nie jest wiekszy
	// niz bufor, gdzie chcemy go odczytac)
	DWORD dwFileSize = GetFileSize(hFile, nullptr);

	if (dwFileSize > sizeof(szADS))
	{
		CloseHandle(hFile);
		return 0;
	}

	DWORD dwReadBytes = 0;

	// odczytaj zawartosc ukrytego pliku
	if (ReadFile(hFile, &szADS, dwFileSize, &dwReadBytes, nullptr) == FALSE || dwReadBytes != dwFileSize)
	{
		CloseHandle(hFile);
		return 0;
	}

	// zamknij uchwyt
	CloseHandle(hFile);

	char szTemp[sizeof(szADS)];

	strcpy_s(szTemp, _countof(szTemp), szADS);

	// odwroc kolejnosc znakow
	_strrev(szTemp);

	if (strcmp(szTemp, "\n\r70.6102") == 0)
	{
		// ustaw flage oznaczajaca, ze istnieje ADS dla pliku CrackMe
		SetEvent(hEvents[2]);
	}

	return 0;
}

Oczekiwaną wartość klucza można ustawić z okna konsoli, wykorzystując komendę:

echo 2016.07> CrackMeZ3S.exe

i sprawdzić czy udało się utworzyć stream komendą:

dir /r

Należy zwrócić uwagę na brak spacji w komendzie „echo” przed znakiem przekierowania „>”, łatwo się pomylić, dodatkowa spacja doda błędne dane, a oczekiwanym kluczem jest tylko i wyłącznie „2016.07”.

crackme-z3s-ustawianie-alternate-data-stream-ntfs

Klucz 3 – schowek systemowy

Kolejny klucz będzie poszukiwany w schowku systemowym. CrackMe będzie oczekiwało w nim skopiowanej, konkretnej wartości tekstowej. I nie mówię tutaj o numerze konta bankowego ;)

///////////////////////////////////////////////////////////////////////////////
//
// Klucz 3 - sprawdzanie systemowego schowka
//
///////////////////////////////////////////////////////////////////////////////

DWORD WINAPI Klucz3(LPTHREAD_START_ROUTINE lpKeyProc[])
{
	SpeedStart('3');

	// odpal kolejny watek (kaskadowo)
	hThreads[4] = CreateThread(nullptr, 0, static_cast(DecodePointer(lpKeyProc[4])), lpKeyProc, 0, &dwThreadIds[4]);

	// otworz schowek
	if (OpenClipboard(nullptr) == TRUE)
	{
		// pobierz uchwyt danych w formacie CF_TEXT
		HANDLE hData = GetClipboardData(CF_TEXT);

		// czy jakies dane sa skopiowane?
		if (hData != nullptr)
		{
			// zablokuj pamiec
			char *pszText = static_cast(GlobalLock(hData));

			if (pszText != nullptr)
			{
				// hehe ;)
				if (strcmp(pszText, "Boom Boom - Lip Lock - Song") == 0)
				{
					// kopiuj zawartosc schowka do zmiennej globalnej
					strcpy_s(szClipboard, sizeof(szClipboard), pszText);

					// ustaw flage dla tego klucza
					SetEvent(hEvents[3]);
				}
			}

			GlobalUnlock(hData);
			CloseClipboard();
		}
	}

	SpeedEnd('3');

	return 0;
}

Tutaj raczej nie trzeba tłumaczyć co trzeba zrobić, aby ustawić ten klucz, CTRL-C i po sprawie.

Klucz 4 – sprawdzanie trybu zgodności

System Windows umożliwia ustawienie odpowiedniego trybu zgodności z wcześniejszymi wersjami Windows dla aplikacji, które niepoprawnie zachowują się w nowszych wersjach systemu operacyjnego. Wykorzystamy to jako kolejny klucz, sprawdzając czy aplikacja uruchomiona jest w trybie Windows Vista.

///////////////////////////////////////////////////////////////////////////////
//
// Klucz 4 - sprawdzanie trybu zgodnosci
//
///////////////////////////////////////////////////////////////////////////////

DWORD WINAPI Klucz4(LPTHREAD_START_ROUTINE lpKeyProc[])
{
	SpeedStart('4');

	// odpal kolejny watek (kaskadowo)
	hThreads[5] = CreateThread(nullptr, 0, static_cast(DecodePointer(lpKeyProc[5])), lpKeyProc, 0, &dwThreadIds[5]);

	osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);

	// funkcja GetVersionEx() jest juz oznaczona jako przestarzala,
	// jednak dla naszego CrackMe spelni swoja role
	#pragma warning(disable : 4996)
	GetVersionEx(&osvi);

	// numeracja bedzie pasowac do Windows Vista oraz Windows Server 2008
	// https://msdn.microsoft.com/pl-pl/library/windows/desktop/ms724833(v=vs.85).aspx
	if (osvi.dwMajorVersion == 6 && osvi.dwMinorVersion == 0)
	{
		// ustaw flage oznaczajaca, ze tryb zgodnosci jest poprawnie ustawiony
		SetEvent(hEvents[4]);
	}

	SpeedEnd('4');

	return 0;
}

Sprawdzenie jest o tyle niepozorne, że wygląda jak standardowy kod weryfikujący wersję systemu Windows. Aby ten klucz dostępu przeszedł weryfikację, wystarczy zmienić tryb zgodności we właściwościach pliku „CrackMeZ3S.exe” lub uruchomić je na Windows Vista (używa ktoś?).

crackme-z3s-klucz-4-tryb-zgodnosci

Jeśli ktoś będzie je próbował łamać na czymś starszym niż samo Windows Vista, będzie niestety musiał spatchować kod lub ustawić hooka na funkcję GetVersionEx() i emulować oczekiwane wartości wersji systemu Windows, tak aby wskazywały właśnie na Windows Vista.

Klucz 5 – kliknięcie CTRL-C

W naszym CrackMe podepniemy się pod funkcję obsługującą sygnały wysyłane do aplikacji okienkowej, w tym kombinację klawiszy CTRL-C, czyli zwyczajowo zamykających aplikację. Sprawdzimy czy użytkownik nacisnął kombinację CTRL-C w trakcie działania CrackMe.

///////////////////////////////////////////////////////////////////////////////
//
// handler dla skrotu CTRL-C
//
///////////////////////////////////////////////////////////////////////////////

BOOL CtrlHandler(DWORD fdwCtrlType)
{
	switch (fdwCtrlType)
	{
	case CTRL_C_EVENT:

		// ustaw flage oznaczajaca, ze uzytkownik kliknal CTRL-C
		SetEvent(hEvents[5]);

		return TRUE;
	}

	return FALSE;
}

///////////////////////////////////////////////////////////////////////////////
//
// Klucz 5 - sprawdz czy uzytkownik nacisnal CTRL-C
//
///////////////////////////////////////////////////////////////////////////////

DWORD WINAPI Klucz5(LPTHREAD_START_ROUTINE lpKeyProc[])
{
	SpeedStart('5');

	// ustaw obsluge CTRL-C
	SetConsoleCtrlHandler(reinterpret_cast(CtrlHandler), TRUE);

	SpeedEnd('5');

	return 0;
}

Naciśnięcie CTRL-C jednocześnie ustawi flagę tego klucza dostępowego oraz zakończy działanie CrackMe.

Wszystkie klucze ustawione – co dalej?

Jeśli wszystkie wątki z kodem sprawdzającym klucze dostępowe zakończą działanie i wszystkie klucze zostaną wykryte, a użytkownik kliknie na końcu CTRL-C, to z pojedynczych liter kluczy dostępowych zostanie zbudowana flaga.

///////////////////////////////////////////////////////////////////////////////
//
// Sprawdzanie poprawnosci wszystkich kluczy oraz generowanie flagi
// z pojedynczych znakow kluczy dostepowych
//
// Poprawna flaga to:
//
// "PELock v2.0"
//  01234567890
//
///////////////////////////////////////////////////////////////////////////////

DWORD WINAPI Check(DWORD Param)
{
	SpeedStart('C');

	// Klucz 0 - falszywy klucz
	if (WaitForSingleObject(hEvents[0], 1) == WAIT_OBJECT_0)
	{
		// falszywe nadpisanie danych, znaki z tego hasla
		// i tak nie beda uzyte (zapisujemy je poza wlasciwym
		// buforem)
		wszFlag[16] = TCHAR(szPassword[4]);
		wszFlag[12] = TCHAR(szPassword[1]);

		#ifdef _DEBUG
		_tprintf(_T("[i] klucz 0 - OK\n"));
		#endif
	}

	// Klucz 1 - zmienna srodowiskowa
	if (WaitForSingleObject(hEvents[1], 1) == WAIT_OBJECT_0)
	{
		// "PELock[ ]v2.0" - "AMD64[ ]"
		wszFlag[6] = wszEnvrionmentVariable[5];

		#ifdef _DEBUG
		_tprintf(_T("[i] klucz 1 - OK\n"));
		#endif
	}

	// Klucz 2 - ADS
	if (WaitForSingleObject(hEvents[2], 1) == WAIT_OBJECT_0)
	{
		// "PELock v[2].[0]" - "[2][0]16.07"
		wszFlag[8] = TCHAR(szADS[0]);
		wszFlag[10] = TCHAR(szADS[1]);
		wszFlag[9] = TCHAR(szADS[4]);

		#ifdef _DEBUG
		_tprintf(_T("[i] klucz 2 - OK\n"));
		#endif
	}

	// Klucz 3 - zawartosc schowka
	if (WaitForSingleObject(hEvents[3], 1) == WAIT_OBJECT_0)
	{
		// "Boom Boom - Lip Lock - Song"
		wszFlag[4] = TCHAR(szClipboard[18]);
		wszFlag[3] = TCHAR(szClipboard[17]);
		wszFlag[2] = TCHAR(szClipboard[16]);
		wszFlag[5] = TCHAR(szClipboard[19]);

		#ifdef _DEBUG
		_tprintf(_T("[i] klucz 3 - OK\n"));
		#endif
	}

	// Klucz 4 - klikniecie CTRL-C
	if (WaitForSingleObject(hEvents[4], 1) == WAIT_OBJECT_0)
	{
		// brakujaca literka
		wszFlag[7] = TCHAR('v');

		#ifdef _DEBUG
		_tprintf(_T("[i] klucz 4 - OK\n"));
		#endif
	}

	// Klucz 5 - wersja systemu pasujaca do Windows Vista
	if (WaitForSingleObject(hEvents[5], 1) == WAIT_OBJECT_0)
	{
		// literka 'P' = 0x4A + 6
		wszFlag[0] = TCHAR(0x4A + osvi.dwMajorVersion);

		// literka 'E' = 0x45 - 0
		wszFlag[1] = TCHAR(0x45 - osvi.dwMinorVersion);

		#ifdef _DEBUG
		_tprintf(_T("[i] klucz 5 - OK\n"));
		#endif
	}

	SpeedEnd('C');

	return 0;
}

Przed wyświetleniem tryumfalnego komunikatu z flagą, zostanie ona dodatkowo zweryfikowana przez sprawdzenie jej skrótu kryptograficznego z dodatkową „solą”:

//
// oblicz MD5 z ciagu tekstowego flagi oraz soli
// (aby zapobiec atakom brute-force)
// ma to na celu unikniecie sytuacji, w ktorej
// ktos chcialby ominac czesc zabezpieczen (np.
// recznie ustawiajac EVENTy)
//
TCHAR wszFlagSalty[128];

_stprintf_s(wszFlagSalty, _T("#flag4poprawna %s \n123458s3cr3t _+=-=-="), wszFlag);

// hash liczymy z ciagu TCHAR, a wynik otrzymamy w postaci ciagu ANSI
BOOL bValidFlag = CheckMD5(wszFlagSalty, _tcslen(wszFlagSalty) * sizeof(TCHAR), "4ED28DA4AAE4F2D58BF52EB0FE09F40B");

SpeedEnd('V');

if (bValidFlag == TRUE)
{

Robimy tak, aby upewnić się, że dane kluczy były poprawne i nikt nie zmodyfikował np. w debuggerze samego kodu weryfikującego, żeby tylko dojść do tego fragmentu kodu.

Antydebugging

Czym by było prawdziwe CrackMe bez metodu utrudniających debuggowanie? W naszym CrackMe też nie zabraknie tego typu atrakcji. Można wykorzystać popularne techniki bazujące na sprawdzaniu obecności debuggerów bazujących na funkcjach WinAPI, takie jak np. IsDebuggerPresent(), jednak ich popularność i powszechna wiedza o nich, z góry stawiają je na przegranej pozycji. Poza tym jak widzę po raz setny IsDebuggerPresent() to chce mi się płakać.

crackme-z3s-dawson-crying

Wykrywanie śledzenia kodu w debuggerze

Dodamy do kodu CrackMe metodę wykrywająca popularne narzędzia wykorzystywane do analizy oprogramowania w czasie ich działania, czyli debuggery. Debuggery pozwalają na śledzenie skompilowanej aplikacji bez dostępu do jej kodów źródłowych. Debugger wyświetla kod skompilowanej aplikacji w formie instrukcji assemblera, umożliwiając śledzenie aplikacji instrukcja po instrukcji, pozwala zatrzymywać działanie aplikacji w wybranych punktach programu (stawia się tzw. pułapki czyli z ang. breakpoints) lub przy próbie wywołania jakiejś funkcji systemowej, np. gdy aplikacja będzie chciała wyświetlić okienko z komunikatem „Twój klucz jest nieprawidłowy” przykładowo wykorzystując funkcję MessageBox().

Wykorzystamy prosty fakt, że debuggowany program, wykonywany krok po kroku w oknie debuggera, niezależnie jaki to jest debugger, jest znacznie wolniejszy, gdyż cały mechanizm debuggowania spowalnia wykonywanie wszystkich instrukcji.

Skąd to spowolnienie? Spójrz jak wygląda standardowa pętla debuggera oparta o funkcje WinAPI a zrozumiesz ile tam się dzieje, dodatkowo czas wydłuża sam użytkownik debuggera, który tu wykona kilka instrukcji, sprawdzi wartości rejestrów, tam spojrzy na dokumentację i tak czas się wydłuża.

Będziemy pobierali czas pomiędzy wykonywaniem oznaczonych bloków kodu i o ile ktoś nie uruchomi tego CrackMe pod emulatorem PC na Commodore 64 albo Atari to nie ma bata, żeby wykonanie kilku prostych instrukcji zajęło przykładowo 5 sekund, a ktoś kto będzie ten kod śledził w oknie debuggera, na pewno spędzi przy tym więcej czasu.

///////////////////////////////////////////////////////////////////////////////
//
// pobieranie poczatkowego czasu, funkcja koniecznie musi byc inline
// zeby zapobiec jej prostemu spatchowaniu w 1 punkcie
//
///////////////////////////////////////////////////////////////////////////////

void __forceinline SpeedStart(int iSpeedStructIndex)
{
	QueryPerformanceFrequency(&Speed[iSpeedStructIndex].Frequency);
	QueryPerformanceCounter(&Speed[iSpeedStructIndex].StartingTime);
}

///////////////////////////////////////////////////////////////////////////////
//
// pobieranie koncowego czasu i sprawdzanie czy przekroczyl ustalony limit
//
///////////////////////////////////////////////////////////////////////////////

void __forceinline SpeedEnd(int iSpeedStructIndex, int iMaxTimeInSeconds = 5)
{
	QueryPerformanceCounter(&Speed[iSpeedStructIndex].EndingTime);
	Speed[iSpeedStructIndex].ElapsedMicroseconds.QuadPart = Speed[iSpeedStructIndex].EndingTime.QuadPart - Speed[iSpeedStructIndex].StartingTime.QuadPart;
	
	//Speed[iSpeedStructIndex].ElapsedMicroseconds.QuadPart *= 1000000;
	Speed[iSpeedStructIndex].ElapsedMicroseconds.QuadPart /= Speed[iSpeedStructIndex].Frequency.QuadPart;

	// sprawdz czy przekroczony zostal ustalony limit czasowy
	if (Speed[iSpeedStructIndex].ElapsedMicroseconds.QuadPart > iMaxTimeInSeconds)
	{
		#ifdef _DEBUG
		_tprintf(_T("[!] przekroczono ustalony limit %i sekund dla indeksu %c, czas wykonywania %llu"), iMaxTimeInSeconds, iSpeedStructIndex, Speed[iSpeedStructIndex].ElapsedMicroseconds.QuadPart);
		#endif

		// w razie przekroczenia ustalonego limitu czasowego
		// nie wyswietlany zadnych komunikatow ostrzegawczych
		// ale uszkadzamy wewnetrze struktury CrackMe, co sprawi,
		// ze CrackMe nie bedzie funkcjonowac prawidlowo lub po
		// prostu sie zawiesi w ktoryms momencie

		// losujemy czy uszkodzic jakas strukture czy nie
		#define LOTTO_CRASH ((rand() & 6) == 0)

		// losowo wymaz uchwyt watku
		if (LOTTO_CRASH) hThreads[rand() % _countof(hThreads)] = nullptr;

		// losowo zamaz uchwyt eventa
		if (LOTTO_CRASH) hEvents[rand() % _countof(hEvents)] = reinterpret_cast(rand());

		// losowo resetuj event (znacznik odnalezienia klucza dostepowego)
		if (LOTTO_CRASH) ResetEvent(hEvents[rand() % _countof(hEvents)]);

		// losowo wypelnij bufory tekstowe
		if (LOTTO_CRASH) memset(wszEnvrionmentVariable, _countof(wszEnvrionmentVariable) * sizeof(TCHAR), rand());
		if (LOTTO_CRASH) memset(szADS, sizeof(szADS), rand());
		if (LOTTO_CRASH) memset(szClipboard, sizeof(szClipboard), rand());
		if (LOTTO_CRASH) memset(szPassword, sizeof(szPassword), rand());
		if (LOTTO_CRASH) memset(wszFlag, _countof(wszFlag) * sizeof(TCHAR), rand());

		// evil asm trick ;), wskaznik stosu jest ZAWSZE wyrownany do 4,
		// tutaj sprawimy, ze bedzie niewyrownany i na 200% aplikacja
		// sie zawiesi
		if (LOTTO_CRASH) __asm inc esp
	}
}

Po detekcji wydłużonych czasów wykonywania, nie wyświetlimy żadnych komunikatów o debuggerze, bo to najgorsze co można zrobić, gdyż w ten sposób dajemy osobie analizującej zabezpieczenie jasną wskazówkę, gdzie tkwi problem. W naszym CrackMe losowo uszkodzimy wewnętrzne bufory danych oraz znaczniki rejestracji dla poszczególnych kluczy dostępowych. Dzięki temu, nawet jeśli ustawione zostaną poprawne klucze dostępowe, to w obecności debuggera finalnie nie uda się wygenerować poprawnej flagi.

Tego typu zabezpieczenia można ominąć stosując wtyczki do debuggerów lub stosując hooki na funkcje odczytujące czas, które będą zwracać do aplikacji fałszywe wyniki.

Opcje kompilatora i linkera

Mimo tego, że CrackMe jest napisane w C++, a nie w assemblerze - można dodatkowo urozmaicić rozrywkę stosując odpowiednie opcje kompilatora i linkera. W naszym CrackMe zastosujemy randomizację bazy poprzez mechanizm ASLR, który sprawi, że nasz plik wykonywalny będzie posiadał domyślnie relokacje i za każdym razem gdy zostanie uruchomiony, system Windows wgra go pod inny adres bazowy w pamięci.

crackme-z3s-opcje-kompilatora

Będzie można zapomnieć o stawianiu pułapek w debuggerze na stałe adresy w pamięci wirtualnej, ponieważ za każdym uruchomieniem kod znajdzie się w innym rejonie pamięci, tak samo adresy funkcji uzyskane przy deasemblacji będą bezużyteczne, innymi słowy trzeba będzie się trochę pomęczyć.

Sprytni reverserzy mogliby jednak usunąć informacje o relokacjach i sprawić, że obraz pliku EXE będzie ładowany cały czas pod domyślny adres bazowy, dlatego dodatkowo ustawimy go na 0. Jest to rzadko stosowana opcja, jednak w przypadku ASLR nawet Microsoft rekomenduje ustawianie adresu bazowego na 0, chociaż sam kompilator nie stosuje domyślnie tej opcji.

Jak można i to zabezpieczenie ominąć, aby ułatwić sobie życie, nie tylko w przypadku tego CrackMe, ale w analizie innych aplikacji? Należałoby przerelokować plik EXE pod dowolny adres bazowy np. domyślnie stosowany 0x400000 i usunąć informacje o relokacjach lub flagę w strukturze nagłówka pliku PE (Portable Executable) oznaczającą, że ten mechanizm jest wykorzystywany.

Koniec

Jak widzicie, sposobów na oryginalne metody weryfikacji jest całkiem sporo, wiele z nich kryje się w rzadko używanych mechanizmach systemu Windows oraz starych funkcjach WinAPI. Z powodzeniem mogą być wykorzystane jako elementy układanek we wszelkiego rodzaju CrackMe. Napiszcie w komentarzach z jakich interesujących technik i nietrywialnych metod sami byście skorzystali lub mieliście okazję je widzieć w innych CrackMe. Do artykułu załączam źródło i skompilowany kod CrackMe. Hasło do źródeł to „CrackMeZ3S”.

Pobierz CrackMeZ3S.zip

O Autorze
Jestem autorem systemu ochrony oprogramowania przed złamaniem oraz inżynierią wsteczną – PELock. Moja firma świadczy usługi z zakresu inżynierii wstecznej oprogramowania, takie jak np. odzyskiwanie utraconych kodów źródłowych czy lokalizacja oprogramowania i gier komputerowych bez dostępu do kodów źródłowych. W wolnym czasie prowadzę bloga SecurityNews, czasami zdarzy mi się napisać artykuł do prasy komputerowej (hi Magazyn Programista), z takich dziedzin jak reverse engineering, analiza malware oraz programowanie niskopoziomowe.
Powrót

Komentarze

  • 2016.07.13 20:03 Korsarz

    Artykuł a’la Sekurak.

    Odpowiedz
    • 2016.07.14 22:27 Michał Sajdak

      nie wiadomo czy to zaleta tekstu czy obelga ;-)

      Odpowiedz
  • 2016.07.13 21:16 j

    Super artykuł! miła odmiana w standardowej Z3S

    Odpowiedz
  • 2016.07.14 11:14 Krzysiek

    Jestem pod wrażeniem wiedzy autora. Wielkie dzięki za wspaniały art.

    Odpowiedz
    • 2016.07.15 18:58 tomek

      wlasnie mi sie przypomniala pasta o wedkarzu #pdk ;)

      Odpowiedz
  • 2016.07.14 14:57 V0ID

    Ale gratka! Autor nie tylko pokazuje kod ale i tłumaczy wykorzystywane mechanizmy. Dużo ciekawych rzeczy wyniosłem z tego artykułu, oby więcej takich!

    Odpowiedz
  • 2016.07.14 16:26 AW

    „The archive is password protected”

    Super!
    To teges – może od razu skasujcie i artykuł, będzie jeszcze większe szczekuryty. :>>>

    Odpowiedz
    • 2016.07.14 17:01 Bartosz Wójcik

      To zabezpieczenie przed nieprzeczytaniem artykułu do końca ;)

      Odpowiedz
    • 2016.07.14 22:33 mr_ty

      gdybym juz decydowal sie pobrac jakies archiwum opisywane w artykule to zamiast zaczynac od komentarza zaczalbym od ctrl+f has* ;)

      Odpowiedz
  • 2016.07.15 16:30 Darek

    Super. Bardzo ciekawy, konkretny artykuł. Oby więcej takich.

    Odpowiedz
  • 2016.07.16 12:25 Adam

    „emulatorem PC na Commodore 64 albo Atari”
    To ja poproszę taki emulator i7 na natywnym C64, najlepiej w wersji GNU (i free)…

    Odpowiedz
    • 2016.07.17 05:34 SasQ

      Chcesz do tego frytki? ;J

      Odpowiedz
  • 2016.07.21 10:35 tomek

    Mam nadzieje że będzie dostępne video do pobrania bo chciałbym obejrzeć na żywo ale nie uda mi się wrócić z pracy :(

    Odpowiedz
  • 2016.07.22 09:28 Robert

    >_spij_slodko_aniolku
    ktoś tu dużo na Wykopie siedzi

    Odpowiedz

Zostaw odpowiedź

Jeśli chcesz zwrócić uwagę na literówkę lub inny błąd techniczny, zapraszamy do formularza kontaktowego. Reagujemy równie szybko.

Jak napisać ciekawe CrackMe na CTF – instrukcja krok po kroku

Komentarze