Blood2: Crack me!

Jak zapewne niektórzy zauważyli na moim twitterze, (który staje się mini blogiem), ostatnio - tweet - uruchamiałem taką starą, wspaniałą grę z przełomu milenium, jaką jest Blood2: The Chosen. W młodości trochę w nią grywałem, jakiś sentyment pozostał. A że chciałem się trochę zrelaksować w weekend, a przy okazji spróbować skonteneryzować i uruchomić tą gierkę spod windowsowego kontenera przez spoon.net, nie pozostało mi nic innego jak po prostu sobie zagrać.

Blood2: The Chosen

Gra jest 32-bitowa, bez problemu ruszyła na moim 64-bitowym systemie. Jedynie instalator jest 16-bitowy, podobnie jak znalezione w sieci cracki umożliwiające uruchamianie gry bez potrzeby umieszczania płytki w napędzie. Zdecydowałem się "na łamanie" z dwóch powodów, pierwszy mówi, żę gdzieś kiedyś miałem płytkę, ale nie mogłem jej teraz znaleźć, a drugi, że gdybym ją znalazł, to zabawa w manipulowanie płytką w napędzie jest mało wygodne, szczególnie, że zamiast napędu CD/DVD mam w laptopie drugi dysk. Sprawa się wyjaśniła sama.

Z instalatorem łatwo można sobie poradzić, instalując manualnie, poprzez skopiowanie wymaganych plików. Podobnie wygląda instalacja łatek. Z crackiem jest już nieco większy problem. Wszystkie programy tego typu jakie znalazłem w sieci, to proste aplikacje 16-bitowe spod DOS-a, czyli standardowe, jednosegmentowe comy.

I tutaj nasuwa się fajny pomysł, potraktować Blooda2 jako prosty program typu "Crack me!" i się trochę pobawić. Jako, że rzadko bawię się w reverse engineering, choć ostatnio w firmie oraz prywatnie dzieje się trochę więcej niż dotychczas, to jest to niewątpliwie ciekawa okazja jakiej nie mogłem przepuścić. A przecież zagrać jakoś trzeba w Blooda!

Osobiście nigdy nie bawiłem się w crackera i testowania swoich umiejętności i (lub nabierania) doświadczenia z tego typu programami. Na pewno więcej mógłby powiedzieć tutaj Gynvael Coldwind, jego videocasty o re mogą być pomocne dla osób nie mających do tej pory styczności z tym tematem. Zainteresowanych odsyłam na jego kanał youtube-owy.

Muszę się przyznać, że sam na początku poszedłem trochę inną ścieżką, po mniejszej linii oporu, i najpierw zajrzałem do cracka, aby sprawdzić co on robi, aby spróbować to odtworzyć, czy to ręcznie, czy w postaci jakiegoś małego 32-bitowego programiku. Kolejne interesujące wyzwanie, przypomnieć sobie, jak to kiedyś działało 16-bitowo po DOS-em. Ale o szczegółach później, w dalszej części, na razie zacznijmy od zabawy z kodem gry.

No to zaczynamy...

Stare poczciwe zabezpieczenia różnych gierek i programów z minionej epoki bazowały często na wymaganiu posiadaniu oryginalnego (niekoniecznie) nośnika CD i potwierdzaniu tego faktu poprzez włożenie go do napędu wtedy, kiedy tylko chciało się zagrać lub popracować. Chyba nikt obecnie nie robi już takich niewygodnych, uprzykrzających życie mechanizmów i tricków.

Odpalając naszą gierkę, bez dostępnej płytki w stacji, otrzymamy stosowny komunikat, o jej braku i prośbę o jej włożenie:

Please insert the Blood2 CD-ROM into your CD-ROM drive.

Analiza

Wstępnie spróbujmy ustalić, gdzie takowy komunikat znajduje się w pliku i w jakim towarzystwie kodu. Ja głównie korzystam z jednego z najlepszych diasemblerów – IDA (The Interactive Disassembler), ale nie stoi nic na przeszkodzie, aby użyć innych narzędzi, czy typowego debuggera, który powinien w zupełności wystarczyć i umożliwić zlokalizowanie funkcji i interesującego nas miejsca w kodzie.

Niestety poszukiwany string nie istnieje w pliku, za to jest inny, bardzo podobny, który leży pod adresem 0x0042C0B0:

.data:0042C0B0 aPleaseInsertTh db 'Please insert the game CD-ROM into the drive.',0

który jest używany przez funkcję spod adresem 0x00401000, IDA przypisała nazwę sub_401000.

Jak się dobrze przyjrzymy miejscu, gdzie owy tekst jest wykorzystywany, zauważymy, że jest to w pobliżu wywołania funkcji LoadStringA, która pobiera string z zasobów dołączonych do wskazanego pliku. Możemy zatem przypuszczać, że wykorzystywane są w grze lokalizacje i uznajemy, że to nasz poszukiwany string. A czy taka będzie prawda to się przekonamy później.

IDA, jako wspaniałe narzędzie, prócz adresu w pamięci pokazuje nam również adres, a raczej offset od początku pliku do miejsca, do którego odnosi się dana pozycja. W naszym przypadku jest to 0x02A4B0. Zatem, jeśli zerkniemy do pliku w jakimś hex-edytorze, to pod tą pozycja zobaczymy nasz komunikat.

blood2_crackme_string

Rzucając okiem z dalszej perspektywy na kod funkcji (sub_401000) możemy łatwo zauważyć 2 różne ścieżki wykonania. Blok kodu, w którym pobierany jest adres interesującego stringu zaznaczono ciemnym kolorem. Łatwo zauważyć ze ścieżka czerwona pomija cały kawał kodu związanego z wyświetlaniem komunikatu.

blood2_crackme_chart

Zatem jest to dobry punkt zaczepienia, aby przyjrzeć się bliżej temu fragmentowi kodu, w którym następuje rozłam i podjęcie decyzji, co dalej wykonać. Poniżej zrzut funkcji z istotnymi dla nas fragmentami:

.text:00401000 sub_401000      proc near
[...]
.text:0040101F                 call    sub_42210B
.text:00401024                 mov     ecx, [eax+4]
.text:00401027                 call    sub_418D0A
.text:0040102C                 mov     [esp+9Ch+var_4], 0
.text:00401037                 call    sub_401150                       ; (1)
.text:0040103C                 test    eax, eax
.text:0040103E                 jz      short loc_401050                 ; (2)
.text:00401040                 mov     [esp+9Ch+var_4], 0FFFFFFFFh
.text:0040104B                 jmp     loc_40111F
.text:00401050
.text:00401050 loc_401050:
.text:00401050                 ; MessageBoxA i przyjaciele
[...]
.text:0040111F
.text:0040111F loc_40111F:
.text:0040111F                 call    sub_42210B
.text:00401124                 mov     ecx, [eax+4]
.text:00401127                 call    sub_418D1F
.text:0040112C                 mov     eax, 1                       ; (3)
.text:00401131                 jmp     short loc_401135
[...]
.text:00401135 loc_401135:
[...]
.text:0040114D                 retn
.text:0040114D sub_401000      endp

Podjęcie decyzji następuje na podstawie wyniku zwracanego przez funkcje sub_401150 [1]. Jeśli jest on pozytywny (eax != 0) następuje skok [2] na koniec funckji, gdzie przypisana zostanie do rejestru eax wartość dodatnia [3], będąca w rzeczywistości wynikiem zwracanym przez naszą, analizowaną funkcję. W przeciwnym przypadku, gdy [2] zwróci 0, następuje skok do bloku kodu pod adresem 0x401050, związanego z wyświetleniem stosownego komunikatu i sprawdzenie płyty.

Przedstawiając to w bardziej ludzkim pseudokodzie można to zapisać (w skrócie) jako:

bool sub_401000() {
 
	bool result = false;
 
	eax = sub_42210B();
	sub_418D0A(*[eax + 4]);
 
	int x = 0;						// [esp+9Ch+var_4]
 
	if (sub_401150()) {				// (1) (2)
 
		x = -1;
		eax = sub_42210B();
		sub_418D1F(*[eax + 4]);
		result = true;				// (3)
 
	} else {
		// wyswietl komunikat
		// sprawdz plyte CD-ROM
		// [...]
	}
 
	return result;
}

Modyfikowanie

Po analizie funkcji, najprostszym sposobem obejścia tego zabezpieczenia jakie mogę obecnej chwili zrobić to odwrócić warunki w instrukcji if, czyli po prostu zamienić instrukcje skoku warunkowego z jz na jnz. Obie te instrukcje są do siebie strukturalnie podobne. W naszym przypadku są to skoki relatywne, po adresie widzimy, że skok następuje o 10 bajtów do przodu, a cała instrukcja zajmuje dokładnie 2 bajty.

Miejsce w pliku (lub w widoku Hex w IDA), gdzie znajduje się ta instrukcja (0x043E), zobaczymy jej binarna postać - 2 bajty o wartości 0x7410. Opcode dla jz to wartość 74h, a 75h dla jnz. Zamieniamy pierwszy bajt i sprawdzamy czy ta prosta modyfikacja zadziała.

Whola! Wszystko działa, gra się odpala i bez problemu można pograć bez płytki. A jednak strzał, że to ten string okazał się słuszny!

Jest to najprostsze rozwiązanie problemu, ale nieidealne. Odwrócony warunek, spowoduje, że gdy płytkę włożymy do napędu, program będzie nadal o nią prosił, i bez analizy dodatkowych fragmentów kodu programu, ciężko powiedzieć co się stanie. Uznajmy, że będzie to zachowanie niezdefiniowane.

Nieco lepszym rozwiązaniem jest modyfikacja kodu programu tak, aby program zawsze podążał tą ścieżką, niezalenie od wyniku funkcji sub_401150. Dodatkowo zakładam, że nie chcę modyfikować innych funkcji, ani wnikać w ich działanie, bo mogą one spełniać jakieś istotne dla działania gry operacje. Ograniczam się jedynie do tego miejsca w sub_401000.

W rzeczywistości możliwości jest wiele. Kilka z nich chciałbym tutaj przytoczyć.

Moja pierwsza modyfikacja, zamieniająca skok jz na jnz jest trochę ułomna, bo nadal zależy od wartości flag, jakie zostały ustawione przez instrukcje test. Lepszym zamiennikiem skoku warunkowego będzie jego wersja bezwarunkowa - jmp. Przy skoku relatywnym, zajmie on dokładnie tyle samo co poprzednicy, a jego opcode to 0xEB.

Niestety zamiana skoku nie jest taka prosta jak poprzednio. Patrząc na oryginalny kod, flow zostaje zaburzony skokiem [2], w sytuacji kiedy sub_401150 [1] zwróci false. Zatem, jeśli usilnie chcemy skorzystać z jmp-a, to trzeba skoczyć do następnej instrukcji - jmp 0. Co będzie trochę bezsensowne, ale jak najbardziej poprawne i działające.

W istocie działanie takiej modyfikacji, jest typowym zmarginesowaniem tejże instrukcji, aby nie wykonywała ona żadnego widocznego rezultatu i nie wpływa na działanie funkcji. Gdyby usnąć oryginalny skok, przepływ działania kodu podążałby wymaganą ścieżką. Usunąć kodu nie możemy, musimy miejsce to wyplenić jakąś 2 bajtową operacją, która, jak wspomniałem, nie będzie miała żadnego wpływu na pozostałe instrukcje i działanie.

Do dyspozycji mamy 2 instrukcje, które zajmują po 2 bajty:

blood2_crackme_opcode

Każdą z nich możemy zmodyfikować lub nawet obie naraz.

Najprościej nadpisać jz korzystając z instrukcji nop, która nic nie robi, poza marnowaniem czasu procesora lub wykorzystać inne 1 lub 2 bajtowe operacje. Bez obaw możemy także manipulować zawartością rejestru eax:

; dwa nopy daja rade
90		nop
90		nop
 
; albo jakies inne czary
90		nop
40		inc ax
 
; bardzo podobne
40		inc ax
48		dec ax
 
; mozna tez inaczej
04 01	add al, 1
 
; byle 2 bajtowo ;)
b4 01	mov ax, 1

Podobnie możemy nadpisać instrukcję test eax, eax. Możemy wykorzystać powyższe przykładowe operacje lub inne, należy jednak pamiętać by w tym przypadku wykonane operacje nie ustawiały flagi ZF (Zero Flag).

Nie trzeba się ograniczać do modyfikacji 2 bajtowych, nic nie stoi na przeszkodzie, aby wykorzystać całą 4-bajtową przestrzeń obu instrukcji. Jak widać opcji jest wiele, różnych kombinacji dużo, a to dopiero jedna z możliwości obejścia tego prostego zabezpieczenia.

To jeszcze nie koniec

Udało się zcrackować grę w bardzo prosty sposób, oczywiście nie jest to jedyna opcja. A wystarczyło przecież zmodyfikować te kilka bajtów w pliku binarnych, aby zapomnieć o CD. I to bez większych nakładów pracy i wysiłku, bez zbędnego analizowania dużych fragmentów kodu.

A to jeszcze nie koniec. Wkrótce wracam do kodu Blooda. W następnych odcinkach będę chciał przyjrzeć się bliżej używanym tutaj funkcjom i mechanizmowi wykrywania nośnika, co może być bardzo ciekawe. A to może pozwoli zcrakować grę jeszcze inaczej, być może nawet prościej, choć wymagać będzie to większego nakładu pracy. Obiecana analiza działania dostępnego w sieci cracka również wkrótce się pojawi. Ciekawe co on dokładnie robi i w jaki sposób obchodzi zabezpieczenie. Choć sam już to wiem, to jednak analiza tej prostej 16-bitowych aplikacji z funkcjami DOS również może być ciekawa. O tym wkrótce…

Dla informacji, wszystkie operacje były przeprowadzone na binarce po zastosowaniu wszystkich patchy (wersja 2.1). A cały tekst jest prezentowany oczywiście tylko w celach edukacyjnych i poznawczych.

A tymczasem idę się rozkoszować Bloodem.

Jedno przemyślenie nt. „Blood2: Crack me!”

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *