Blood2: Analiza cracka
• tech • 1876 słów • 9 minut czytania
Kolejna część dotycząca mojej starej ulubionej (ostatnio) gry, którą potraktowałem jako narzędzie analizy i zabawy w reverse enginering. Jak wspomniałem w poprzedniej (pierwszej) części, miałem problem ze znalezieniem odpowiedniego programu neutralizującego wymóg posiadania płyty CD. A wszystko na co trafiłem było jednosegmentowymi aplikacjami DOS-a. A jak wiadomo Windowsy 64-bitowe nie posiadają już subsystemu do odpalania 16-bitowych programów. Mimo, iż 64-bitowe procesory w trybie “long mode” jako tako dałyby radę z takim kodem, o ile nie wymagałyby wirtualnego trybu chronionego. Tak, czy inaczej, zostałem zmuszony do zabawy w crackera, co w ostatniej notce przedstawiłem. Obiecałem również, że przyjrzę się bliżej temu “comowemu” crackowi z bliska.
16-bitów z sieci
Po diasemblingu, kod programu okazał się bardzo prosty, typowe operacje na plikach, korzystając z funkcji DOS - poprzez przerwanie 21h
. Nic odkrywczego i ciekawe tutaj nie znalazłem. Aczkolwiek pewne fragmenty mnie zaciekawiły, bo albo były błędne, albo ja coś do końca nie kontaktowałem za dobrze, kiedy to analizowałem.
W dalszej części, w przedstawianym kodzie pomijam wszelkie nieistotne dla nas informacje i fragmenty kodu, do których mogę zaliczyć informacje i komunikaty identyfikujące autora, grupę i inne zbyteczne. Kod wzbogaciłem o komentarze, opisujące niektóre instrukcje. Część etykiet i nazw zamieniłem na bardziej czytelne, odpowiednie dla danych fragmentów, co powinno ułatwić czytanie i zrozumienie tegoż, nazbyt prostego kodu.
Początek trywialny, otwieramy plik wykonywalny gry. Jeśli operacja się powiedzie to przechodzimy dalej. W przypadku błędu, wyświetlamy odpowiedni komunikat i zamykamy program z kodem błędu 1
.
seg000:014A mov ax, 3D02h ; otwarcie pliku dyskowego w trybie rw
seg000:014D mov dx, 152h ; o nazwie spod DS:DX - "blood2.exe"
seg000:0150 jmp short 1015Dh
seg000:0152 db 'blood2.exe',0
seg000:015D int 21h ; (1)
seg000:015D
seg000:015F jnb short FileOpen ; jak sie powiedzie to skok
seg000:015F
seg000:0161 mov ah, 9 ; wyswietlenie komunikatu na ekran
seg000:0163 mov dx, 168h ; string musi byc zakonczony "$"
seg000:0166 jmp short 1018Eh
seg000:0168 db 'blood2.exe not found or is read-only!$'
seg000:018E int 21h
seg000:018E
seg000:0190 mov ax, 4C01h ; zakonczenie procesu z kodem wyjscia 1
seg000:0193 int 21h
Miksowanie kodu programu i danych w plikach COM jest rzeczą najzwyklejszą, choć czyta się to trochę dziwnie, bo pełno skoków pomijających stringi.
Gdy plik zostanie już poprawnie otwarty, wykonywane jest pobranie jego rozmiaru. Potrzebne jest to do walidacji. Rozmiar pobierany jest za pomocą typowej sztuczki. Polega ona na przesunięciu wskaźnika pozycji w pliku na jego koniec i odczytania bieżącej pozycji zwracanej przez funkcję po poprawnym wykonaniu operacji.
seg000:0195 push ax
seg000:0196 push cs
seg000:0197 pop ds
seg000:0198 jmp short 101A2h
seg000:0198
seg000:019A FileSizeL dw 0B800h ; wymagany rozmiar pliku
seg000:019C FileSizeH dw 19h ; 1 685 504 bajtow
seg000:019E CurPosH dw 0 ; zmienne tymczasowe do operacji
seg000:01A0 CurPosL dw 0 ; na pozycji pliku
seg000:01A2
seg000:01A2 pop bx ; eee?
seg000:01A3 push bx
seg000:01A3
seg000:01A4 mov ax, 4201h ; ustawienie wskaznika pliku wzgledem biezacej pozycji
seg000:01A7 mov cx, CurPosH ; CurPos = 0, czyli na poczatek pliku
seg000:01AB mov dx, CurPosL
seg000:01AF int 21h
seg000:01AF
seg000:01B1 mov CurPosH, dx ; zapamietanie biezacej pozycji
seg000:01B5 mov CurPosL, ax ; wzgledem poczatku pliku [DX:AX]
seg000:01B5
seg000:01B8 mov ax, 4202h ; ustawienie wskaznika pliku wzglededm konca
seg000:01BB xor cx, cx ; zerowanie CX i DX,
seg000:01BD xor dx, dx ; czyli ustawienie na koniec pliku
seg000:01BF int 21h ; w istocie pobranie rozmiaru pliku [AX:DX]
seg000:01BF
seg000:01C1 cmp ax, FileSizeH ; porownanie rozmiaru pliku
seg000:01C5 jnz short FileSizeError ; jesli inny niz spodziewany
seg000:01C7 cmp dx, FileSizeL ; to skocz do obslugi bledu
seg000:01CB jnz short FileSizeError
seg000:01CB
seg000:01CD mov ax, 4200h ; ustawienie wskaznika w pliku wzgledem poczatku
seg000:01D0 mov cx, CurPosH ; na wartosci poczatkowe
seg000:01D4 mov dx, CurPosL
seg000:01D8 int 21h
seg000:01D8
seg000:01DA jmp short FilePatch ; i skok do dalszych operacji
W miedzy czasie, przed operacją wyznaczenia rozmiaru pliku, zapamiętywana jest bieżąca pozycja pliku w zmiennej tymczasowej CurPos
, aby na koniec ja przywrócić do stanu początkowego. Ten kod może być zastanawiający. Bo czy naprawdę jest to konieczne? Funkcja DOS 3D
otwierająca plik, wedle dokumentacji, po poprawnym wykonaniu swojej roboty, ustawia wewnętrzny pointer w pliku na jego początek. Także wydaje mi się, że jest to zbędne.
Prócz tego powyższy kod według mnie nie jest poprawny, jego działanie jest niezdefiniowane. Uchwyt do otwartego pliku przez funkcję 3D
przerwania 21h
[1], zwracany jest w rejestrze AX
. Pozostale funkcje operujące na plikach pod DOS-em spodziewają się uchwytu w rejestrze BX
, a tutaj nie ma żadnej interakcji z tym związanej. Jest tylko dziwny kod ze stosem [2], ale nie bardzo rozumiem do czego miałby on tutaj służyć. W dalszych (niżej) fragmentach kodu, uchwyt jest poprawnie przekazywany przez rejestr BX
przy operacjach plikowych. Jest to zastanawiające. Może IDA coś pomieszała, ale nie potwierdza tego kod binarny - w HEX-ach jest podobnie, opcody się zgadzają. Może ktoś bardziej doświadczony w 16-bitowcach mógłby rozjaśnić sprawę.
Wracając do kodu, gdy plik ma inny rozmiar niż spodziewany, na przykład w sytuacji próby zcrackowania nie tej wersji pliku (inny patch), wyświetlany zostaje odpowiedni komunikat i proces zostaje zakończony z kodem wyjścia 1, oznaczającym błąd:
seg000:01DC FileSizeError:
seg000:01DC jmp short 101F4h
seg000:01DE db 'Incorrect filesize!',0Dh,0Ah,'$'
seg000:01F4 mov ah, 9 ; wyswietlenie na ekranie
seg000:01F6 mov dx, 1DEh ; wskazanego komunikatu
seg000:01F9 int 21h
seg000:01F9
seg000:01FB mov ax, 4C01h ; zakonczenie procesu z kodem wyjscia 1
seg000:01FE int 21h
A gdy walidacja rozmiaru pliku zakończyła się poprawnie, lecimy dalej…
seg000:0200 FilePatch:
seg000:0200 pop ax ; pobranie uchwytu pliku (3)
seg000:0201 mov bx, ax
seg000:0203 push ax
seg000:0203
seg000:0204 mov ah, 42h ; ustawienie wskaznika w pliku
seg000:0206 mov al, 0 ; wzgledem poczatku
seg000:0208 mov cx, 0 ; na pozycje 1086
seg000:020B mov dx, 43Eh
seg000:020E int 21h
seg000:020E
seg000:0210 pop ax ; pobranie uchwytu pliku
seg000:0211 mov bx, ax
seg000:0213 push ax
seg000:0213
seg000:0214 mov ah, 40h ; zapisywanie do pliku
seg000:0216 mov cx, 2 ; 2 bajtow
seg000:0219 mov dx, 21Eh ; z tego miejsca
seg000:021C jmp short 10220h
seg000:021E dw 9090h ; NOP NOP (4)
seg000:0220 int 21h
Tutaj bez problemów wyłaniają się fragmenty kodu związane z uchwytem pliku, przez kooperacje na rejestrach AX
i BX
[3]. Kod ten można byłoby uprościć wrzucając do jakieś zmiennej w pamięci uchwyt do pliku lub trzymać go w rejestrze BX
od razu po jego otwarciu. To drugie tylko w sytuacji, gdy żadne inne operacje nie modyfikują tego rejestru. Nie pamiętam, czy jakieś funkcje IO na plikach zwracają coś w tym rejestrze.
Wskaźnik w pliku przesuwany jest na pozycję odpowiadającą 1086
bajtowi, pod która zapisywane są 2 bajty danych. Te zapisywane dane to dokładnie 2 opcody instrukcji NOP
, a pozycja, pod która są zapisywane - 0x043E
, to znany z pierwszej części offset do miejsca wystąpienia instrukcji jz short loc_401050
.
Ciekawą uwagą wartą wspomnienia, jest fakt, że miejsce przechowywania danych do zapisania w pliku [4] IDA w pełni świadomie i inteligentnie potraktowała jako operacje NOP
, uznając że następuje wyrównanie pamięci do 4-ki. Wstawiając w tym miejscu stosowną informacje:
seg000:021E align 4
W rzeczywistości znajdują się tam dane, jak zaprezentowano w kodzie powyżej.
Na koniec zamykany jest plik, wyświetlany stosowny komunikat mówiący o pomyślnej modyfikacji pliku i następuje zakończenie procesu z kodem wyjscia równym 0
, który świadczy o pozytywnej modyfikacji pliku gry.
seg000:0222 pop ax ; pobranie uchwytu pliku
seg000:0223 mov bx, ax
seg000:0225 mov ah, 3Eh ; zamkniecie pliku
seg000:0227 int 21h
seg000:0227
seg000:0229 mov ah, 9 ; wyswietlenie komunikatu na ekranie
seg000:022B mov dx, 230h
seg000:022E jmp short 10241h
seg000:0230 db 'Patch applied.',0Ah, 0Dh,'$'
seg000:0241 int 21h
seg000:0241
seg000:0243 mov ax, 4C00h ; zakonczenie procesu z kodem wyjscia 0
seg000:0246 int 21h
Podsumowując powyższą analizę, kod bardzo prosty - szału nie ma.
Crack modyfikuje kod binarny gry, w taki sam sposób, jaki zrobiłem to ręcznie w pierwszej części. Bo jak wspomniałem w tamtej notatce, jest to najprostsze i najszybsze rozwiązanie, które działa i rozwiązuje problem. A przy tym nie wymaga zbytniej ingerencji i głębszej analizy innych fragmentów kodu.
Moje 32 bity
Jako uzupełnienie analizy, postanowiłem napisać własnego cracka, oczywiście w 32 bitach i w asemblerze. Czemu tak? Bo w końcu to tylko kilka wywołań funkcji, a fajnie czasem jest coś napisać na niskim poziomie. W tworzonym programie nie chciałem uzależniać się od jakiejkolwiek biblioteki (np. C run-time), więc skorzystałem jedynie z API systemowego. Dzięki temu kod wynikowy wynosi dokładnie 1 KB.
format PE GUI 4.0
include 'win32a.inc'
push 0
push FILE_ATTRIBUTE_NORMAL
push OPEN_EXISTING
push 0
push FILE_SHARE_READ or FILE_SHARE_WRITE
push GENERIC_READ or GENERIC_WRITE
push FileName
call [CreateFile]
cmp eax, INVALID_HANDLE_VALUE
jne FileOpen
mov eax, FileNotFound
jmp short ErrorExit
FileOpen:
mov ebx, eax
push NULL
push ebx
call [GetFileSize]
cmp eax, [FileSize]
jz FilePatch
mov eax, IncorrectSize
jmp short ErrorExit
FilePatch:
push FILE_BEGIN
push 0
push [PatchPos]
push ebx
call [SetFilePointer]
push 0
push tmp
push [PatchSize]
push PatchData
push ebx
call [WriteFile]
push ebx
call [CloseHandle]
push MB_OK
push MessageTitle
push PatchAppiled
push 0
call [MessageBox]
mov eax, 0
ret
ErrorExit:
push MB_OK
push MessageTitle
push eax
push 0
call [MessageBox]
mov eax, 1
ret
tmp dd 0
FileSize dd 19B800h
PatchPos dd 43Eh
PatchSize dd 2h
PatchData db 90h,90h
FileName db "blood2.exe", 0
MessageTitle db "Blood2: Crack me!", 0
FileNotFound db "The blood2.exe file not found!", 0
IncorrectSize db "Incorrect filesize!", 0
PatchAppiled db "Patch applied!", 0
data import
library \
kernel32, 'kernel32.dll', \
user32, 'user32.dll'
import user32, \
MessageBox, 'MessageBoxA'
import kernel32, \
CreateFile, 'CreateFileA', \
GetFileSize, 'GetFileSize', \
SetFilePointer, 'SetFilePointer', \
WriteFile, 'WriteFile', \
CloseHandle, 'CloseHandle'
end data
Po składni, niektórzy mogą się domyślić, że użyłem FASM-a. Nigdy nie miałem z nim styczności, ale gdy trafiłem na niego w sieci, uznałem, że warto kiedyś spróbować - i tak się potoczyło. Ja głownie swoje zabawy wykonuję w NASM-mie lub MASM-ie, a najwięcej to w debugerze z VS, czy IDA.
Kod jest poprawny i działający, ale można byłoby go ulepszyć. Przede wszystkim podzielić na sekcje danych, kodu, itd. Do tego FASM udostępnia makra i ciekawą składnie, usprawniająca tworzenie programów w niskopoziomowym języku. Głównie mam tutaj na myśli operacje invoke
, która pozwoliłaby na zamianę standardowych wywołań funkcji przez zrzucanie na stos argumentów i skoku do procedury poprzez call
, do prostej linii:
invoke func, arg1, arg2, arg3, ...
Na pewno skróciłoby to znacznie kod źródłowy, ale tak się składa, że bardziej jestem przyzwyczajony przez różnego rodzaju zabawy z re do “normlanej” metody, więc trzymałem się własnych przyczajeń.
Słowem zakończenia…
Mimo prostego kodu, analiza była bardzo ciekawa, poznając funkcje DOS-owe przerwania 21h
, trochę frajdy z tym było. Osobiście nigdy w życiu nie miałem okazji napisania w asemblerze 16-bitowego kodu pod DOS-a, więc wydało mi się to fajną zabawą i poniekąd nauką. Choć to już trochę leciwa technologia to zawsze się coś ciekawego człowiek dowie.
Napisanie wersji 32-bitowej w asemblerze, też było ciekawym doświadczeniem. Mimo prostoty (jak wyżej), stworzenie całego programu w asemblerze, a nie tylko krótkie wstawki, czasami jest dobrym pomysłem. Na pewno szare komórki się przewietrzą.
Jedna mała uwaga, w czasie pisania tego tekstu, udało mi się znaleźć jeszcze kilką programów łamiących zabezpieczenie gry. I niekoniecznie były to dosowe aplikacje, trafiła się jedna nawet z 32-bitowym GUI. No ale, w międzyczasie już skupiłem się na tym problemie i osobistym udziale, wieć należało się trzymać planu.
Pozostało jeszcze trochę ciekawych rzeczy do sprawdzenia i przeanalizowania w kodzie gry, dotyczących zabezpieczenia i mechanizmu wykrywania nośnika w napędzie. O tym wkrótce w kolejnej (prawdopodobnie) części.
Podobnie, jak w pierwszej części, wszystkie przedstawione materiały służą jedynie celom poznawczym i edukacyjnym.
Komentarze (0)