Mały wstep do crackingu.
I - Zasada dzialania programu
W tym kursie ?zasady dzialania crackingu? zajmiemy sie patchowaniem programu napisanego przez nas samych. Do tego celu potrzebowac bedziemy kompilator jezyka C (Dev C ), kompilatora assemblera (masm oraz nasm), hexedytora (hexplorer) i najwazniejsze ? duzo cierpliwosci. Na samym poczatku powinnismy ?spersonalizowac? nasze srodowisko pracy poprzez utworzenie dowiazan w wierszu polecen do naszego kompilatora - gcc i masma (zakladam ze zainstalowaliscie je tam gdzie ja), wykonujemy to jednym poleceniem:
set path=%path%;c:progra~1dev-cppbin;c:masm32bin
Jezeli wszystko dobrze zrobiles nie powinienes ujrzec zadnego bledu.
Zacznijmy od skompilowania programu (program nazwijmy first.c) ktorego kod zrodlowy widnieje ponizej:
#include
#include
int
main( void )
{
int i = 50;
while( i < 100 )
{
i--;
if( i < 0 ) i = 99;
Sleep( 100 );
printf(" %4ir", i );
}
puts("OK!");
return 0;
}
Aby skompilowac nasz program w wierszu polecen (cmd.com) piszemy:
gcc -o first.exe first.c
Jezeli kompilacja przebiegnie pomyslnie w miejscu gdzie utworzylismy plik first.c powinnismy takze dostrzec plik o nazwie first.exe Zasada dzialania programu jest bardzo prosta: program w petli zmniejsza licznik i o 1 a gdy wartosc i jest mniejsza od 0 to przypisujemy jej nowa wartosc ? 99 Gdy i osiagnie wartosc 100 zostanie spelniony warunek a w nastepstwie zobaczymy tekst ?OK!? jednak przy normalnej pracy programu nie jest mozliwe spelnienie tego warunku. Naszym zadaniem bedzie odpowiednio zmodyfikowac nasz skompilowany program tak by w jakis sposob spelnic warunek dla i Konczymy dzialanie naszego programu uzywajac kombinacji klawiszy CTRL C i zabieramy sie do pracy.
Na poczatek zacznijmy od paru uwag, skoro czytasz ten artykul zakladam, ze nie wiesz co to sa rejestry procesora i stos. Dzialanie stosu postaram sie zobrazowac na przykladzie monet. Wyobraz sobie stos monet, kladziesz najpierw pierwsza monete, na niej druga, trzecia, itd. Teraz patrzac na nasz stos monet od gory widzimy, ze mamy dostep tylko do monety znajdujacej sie na samej gorze, jezeli chcemy wziac monete przedostatnia musimy najpierw usunac ze stosu monete ostatnia i dopiero wtedy wziac monete przedostatnia. Identyczna zasada dzialania obowiazuje nas przy stosach w pamieci.
Zacznijmy od tego, ze stos jest zwyklym obszarem pamieci, kazde miejsce na stosie ma 4 bajty. W zwiazku z tym niezaleznie od tego czy funkcja jako parametr przyjmuje znak (char ? 1bajt), czy liczbe (int ? 4bajty), po wepchaniu tego na stos zajmuje to 4bajty.
Za stos odpowiedzialne sa dwa rejestry:
?dno stosu? - jest tam adres pierwszego (tego na samym dole) elementu stosu ? rejestr EBP, oraz
?wierzch stosu? - jest tam adres elementu tego na samej gorze (a dokladniej adres elementu przed nim tj. adres elementu ktory bedzie nastepnie polozony) ? rejestr ESP.
Aby moc korzystac z stosu, uzywamy 2 podstawowe instrukcje:
push costam - kladzie cos na stosie (w miejscu wskazywanym przez ESP) i przesuwa ESP o 4 do przodu (w gore).
pop costam - zdejmuje to co jest pod esp i cofa esp o 4
Nie mozesz zdjac czegos od dolu poniewaz dostep masz tylko do elementu na gorze !!!
Jeszcze jedno, pamiec prawie kazdego procesu zaczyna sie od miejsca 0x00400000 i tam znajduje sie kod, natomiast poczatek stosu zaczyna sie mniej wiecej w rejonie 0x006f0000. Natomiast zeby bylo troche smieszniej: zalozmy ze ESP == 1234 to wrzucenie czegos na stos (push) spowoduje ze ESP == 1230, rejestr ?rosnie? do zera, czyli EBP >= ESP. Miedzy innymi dzieki temu parametry funkcji sa wrzucane do konca. Zalozmy, ze masz:
asd( 1,2,3,4);
push 4
push 3
push 2
push 1
call asd
Architektura stosu spowoduje ze w pamieci beda po kolei:
ESP tu wskazuje -> [ ] [1] [2] [3] [4] <- EBP tutaj
Najwazniejsze zebys zapamietal z tego, ze zmienne lokalne, alokowane sa na stosie i asembler/komputer dobiera sie do nich poprzez rejestr EBP czyli wzglednie od dna stosu. Zeby bylo smieszniej dno stosu jest czesto przesuwane, ale nie bede Ci macil na razie.
W bibliotekach DLL sa przechowywane wszystkie funkcje API i teraz system podczas uruchamiania programu laduje te dllki do pamieci pod wskazany adres. Disassemblery sa na tyle sprytne, ze potrafia rozpoznac jaka funkcja w danym miejscu jest wywolywana z biblioteki, poprzez co bardzo latwo sledzic funkcje API.
Wracajac do tematu ? zdisasemblujmy nasz program wydajac polecenie:
dumppe first.exe -disasm > first.dasm
Jak widzimy stworzyl nam sie piekny listing asemblera, widzimy pelno ciekawych rzeczy o naszym exeku i nie tylko. Mimo, iz nasz program ma tylko kilka linii, listing ma ich ponad 3100. Jednak jak juz wczesniej mowilem wywolanie funkcji sleep jest latwe do znalezienia ? wykorzystajmy to i znajdzmy fragment gdzie funkcja ta zostaje wywolana. Znajdz slowo ?sleep? ...
00402AF0 fn_00402AF0:
00402AF0 FF25E4604000 jmp dword ptr [Sleep]
Pare slow o tym co to jest. Pierwsza kolumna to adres w pamieci (nie w pliku!), w drugiej linii i drugiej kolumnie widzimy skompilowany rozkaz asma. Zaczne od dzialania funkcji call. Wiec call dziala tak:
Zapisuje adres powrotu (nastepnego rozkazu) i skacze w podane miejsce. Skoro juz pisze o call to poznaj rowniez ?siostre? funkcji call, ktora jest ret. Calosc sprobuje zobrazowac na ponizszym kodzie:
...
call asd
mov eax, 1234
...
asd:
mov ebp, 1234
ret
...
w/w fragment spowoduje wykonanie czegoś takiego:
call asd
mov ebp, 1234
ret
mov eax, 1234
Nota bene ten adres powrotu zachowywany jest na stosie (jest to jedna z ulubionych zabawek hackerów)
Podsumowując ten ?podrozdział? powiem, że możesz zrobić call w callowanej funkcji w callowanej funkcji, a potem 3 razy ret i jesteś na początku.
Disasmy jako, że są sprytne to wszystkie miejsca do, których skaczą calle oznaczają jako fn_adress (fn_00402AF0 ? w naszym przypadku) a miejsca do których skaczą jmp - loc_adres.
To co znaleźliśmy:
0402AF0 fn_00402AF0:
00402AF0 FF25E4604000 jmp dword ptr [Sleep]
to nie jest jeszcze to czego szukamy :). Niektóre kompilatory (czyt. większość) robi sobie tablice wektorów, i potem się do niej odwołuje. Pytanie więc: ?Czego my w zasadzie szukamy?? No więc szukamy pętli z i.
Ale wiemy jedno: program żeby wywołać funkcje sleep z API najpierw skacze w to miejsce które znaleźliśmy a z tego miejsca skacze (już bez zachowania adresu) do właściwej funkcji. Jak już się zapewne zorientowałeś wywołanie funkcji sleep będzie związane z call i fn, które znaleźliśmy. Zatem poszukajmy fn_00402AF0 w naszym listingu. Znaleźliśmy coś takiego:
004012CE E81D180000 call fn_00402AF0
Tak jak już wcześniej mówiłem najpierw wrzucany jest na stos parametr: 64h == 100 a potem call do funkcji. Wiemy, że ten call po dwóch skokach zaprowadzi nas do sleep, więc mamy Sleep( 100 )
Teraz się trochę rozejrzyjmy do koła ? myślę, że najlepiej będzie jak będę wklejał po kolei linijki i tłumaczył za co są one odpowiedzialne.
004012A8 C745FC32000000 mov dword ptr [ebp-4],32h
Widać, że coś pod adresem względem EBP jest kombinowane. EBP == zmienne lokalne, więc wiemy że to jest int i = 50; a dokładniej część i = 50 bo 32h = 50 (3*16 2)
004012AF loc_004012AF:
004012AF 837DFC63 cmp dword ptr [ebp-4],63h
004012B3 7E02 jle loc_004012B7
004012B5 EB34 jmp loc_004012EB
4 kolejne linie, loc_... <- do tej linii skacze jakiś jmp
cmp = compare (porównaj) - to porównuje dwie wartości:
mov dword ptr [ebp-4]
cmp dword ptr [ebp-4]
Jak widać adres EBP się nie zmienił, wiec to pewnie jest nasza zmienna i
63h = 99... czemu 99? a nie 100? w końcu mamy w programie i < 100 ale i < 100 to to samo co i <= 99 kompilator wiec przekształcił to trochę żeby mu było wygodniej. Następna instrukcja:
jle = jump less equal - skocz jeśli mniejsze lub równe, czyli nasze <=
jmp - skok bezwzględny
Ten skok jmp wyprowadza nas poza while ale jako ze ten warunek jest spełniony teraz, zawsze to program skoczy do loc_004012B7 czyli dwie linie niżej.
004012B7 loc_004012B7:
004012B7 8D45FC lea eax,[ebp-4]
004012BA FF08 dec dword ptr [eax]
004012BC 837DFC00 cmp dword ptr [ebp-4],0
Teraz mamy coś takiego jak lea i dec. lea działa na zasadzie prostej, mianowicie adres zmiennej (czyli wartość wyrażenia ebp-4) wpisuje do rejestru EAX a dec zmniejsza zmienna pod adresem wpisanym w eax, jest duża różnica miedzy dec eax, a dec dword ptr [eax]. Dokładnie taka sama jak w C różnica miedzy ?i?-a"/ i ?(*i)-?", a te dwie linie to i--;. Następnie jest to cmp:
004012BC 837DFC00 cmp dword ptr [ebp-4],0
004012C0 7907 jns loc_004012C9
dword ptr [ebp-4] <-- czyli nasza zmienna i
cmp porównuje i z 0, jns = jump if not sign (chyba) czyli skocz jeśli nie ma znaku (znaku minus)
if( i < 0 ) <-- to jest ta cześć, skok nie wykona się jeśli i będzie mniejsze od 0, czyli będzie miało minus
004012C2 C745FC63000000 mov dword ptr [ebp-4],63h czyli i = 99;
(63h = 99 co już wcześniej ustaliliśmy).
004012C9 loc_004012C9:
004012C9 83EC0C sub esp,0Ch
sub esp, poprawia odrobinę wartość stosu, nie ważne dla nas.
nasze piękne wywołanie Sleep( 100 ):
004012CC 6A64 push 64h
004012CE E81D180000 call fn_00402AF0
Znowu jakaś korekcja stosu... kompilator nie miał włączonej optymalizacji, wiec widać efekt:
esp = esp 12 a potem esp = esp - 8, jak by nie można dać esp = esp 4 :)
004012D3 83C40C add esp,0Ch
004012D6 83EC08 sub esp,8
Idziemy dalej:
004012D9 FF75FC push dword ptr [ebp-4]
004012DC 6880124000 push 401280h
004012E1 E87A170000 call fn_00402A60
004012E6 83C410 add esp,10h
Pierwsza linia wrzuca na stos nasze i (cały czas to dword ptr [ebp-4] się pojawia, potem wrzuca na stos adres stringa w pamięci (tego " %4ir") a potem wywołuje printf (ten call) i znowu korekcja stosu... (trzeba to co wrzuciło się na stos jakoś z niego wyrzucić).
004012E9 EBC4 jmp loc_004012AF <-- idz na poczatek petli, czyli do while.
Kolejny krok:
04012EB loc_004012EB:
004012EB 83EC0C sub esp,0Ch <-- korekcja stosu
004012EE 6886124000 push 401286h <-- wrzuć adres stringa "OK!" na stos
004012F3 E858170000 call fn_00402A50 <-- calluj puts
004012F8 83C410 add esp,10h <-- korekcja stosu
Teraz, kiedy już wszystko mamy ?przetłumaczone? przejdziemy do kolejnego zadania.
II - Pokaż OK.! ? sposób pierwszy.
W tym podrozdziale ?przekonamy? nasz program na dwa sposoby. Pierwszym i chyba zdezydowanie najłatwiejszy sposób to zamienienie naszej liczby 32h na dowolnie większą od tej (prawie dowolnej, ponieważ nie możemy przekroczyć maksymalnej wartości zmiennej integer). Zwróć uwagę na tą linijkę gdzie i = 50 przed whilem:
004012A8 C745FC32000000 mov dword ptr [ebp-4],32h
dokładniej na C745FC32000000 a w szczególności na:
C745FC*32*000000
mov dword ptr [ebp-4],*32h*
Jeżeli zmienimy to 32h na np. 70h to warunek nie zostanie spełniony i zostanie wyświetlona linijka ?OK!?
Jednak jak to zrobi? skoro adres: 004012A8 to adres w pamięci a nie w pliku. Popatrzmy jeszcze raz na nasz listnig, interesują nas teraz dwa wpisy: pierwszy ? początek pamięci procesu: image base:
Image Base 00400000
Aby móc trafić do odpowiedniego offsetu w pliku potrzebujemy jego adres w sekcji, aby dowiedzieć się jakie sekcje występują w naszym programie ponownie zerkamy na listing i szukamy ?Section Table?
Nas dokładniej interesuje sekcja gdzie zapisany jest kod programu (bo tam musimy zmodyfikować odpowiednie dane), jak widać jest to sekcja: ?01 .text? ? kod programu, która wygląda tak:
Section Table
-------------
01 .text Virtual Address 00001000
Virtual Size 00001BA8
Raw Data Offset 00000400
Raw Data Size 00001C00
Relocation Offset 00000000
Relocation Count 0000
Line Number Offset 00000000
Line Number Count 0000
Characteristics 60000060
Code
Initialized Data
Executable
Readable
Teraz interesuje nas coś takiego jak Raw Data Offset i Virtual Address:
Raw Data Offset 00000400
Virtual Address 00001000
Adres sekcji w pamięci to jest Image Base Virtual Address sekcji. A jak się dowiedzieć adres w pliku ?
Najpierw trzeba się dowiedzieć adres bajtów od początku sekcji czyli:
4012a8 - ( 400000 1000 ) <-- to co chcemy mieć
i nam wychodzi 2a8, a teraz potrzebujemy adres sekcji w pliku tj. Raw Data Offset czyli 400 wiec nasz adres w pliku to jest 2a8 400 czyli 6a8 :) W tym miejscu odpalamy hexedytor.
(jeżeli chcemy aby w hexplorerze przeniosło nas do odpowiedniego adresu naciskamy 4 guzik od prawej [taki celownik jakby] i wpisujemy tam w naszym przypadku: 6a8)
A gdyby ktoś jeszcze nie zauważył zasady uzyskiwania adresu w pliku to poniżej to opisuje:
adres w pliku = adres w listingu - (Image Base Virtual Address sekcji ) Raw Offset sekcji
Jesteśmy przy odpowiednim adresie w pliku, przypomnę co mieliśmy w listingu:
004012A8 C745FC32000000 mov dword ptr [ebp-4],32h
a co hexedytor pokazuje nam pod tym adresem w pliku:
C7 45 FC 32 00 00 00 00
Tak więc stoimy na polu C7 (naciśnij 3 razy strzałke w prawo) jeżeli widzisz 32 to zmień je na 70 a jeżeli nie widzisz to poszukaj dobrze :) Teraz zamiast C7 45 FC 32 00 00 00 powinno być:
C7 45 FC 70 00 00 00 83
Teraz zapisz zmiany (file -> save) i uruchom zmodyfikowany program.
Jak widać ?przekonaliśmy? nasz program by pokazał nam ?OK!? :)
Teraz zajmiemy się ?poprawieniem? programu drugim sposobem :)
III - Pokaż OK.! ? sposób drugi.
Skompilujmy nasz program jeszcze raz. W drugim sposobie ?poprawimy? tak program by i nie malało a rosło :) Popatrzmy na tą linijkę:
004012BA FF08 dec dword ptr [eax]
Musimy ja zmienić na:
004012BA FF08 inc dword ptr [eax]
Jako, że nie wiemy jak skompilowany opcode wygląda to się posłużymy nasmem.
Tworzmy nowy plik (asd.nasm) i wpisujemy do niego taki oto kod:
[BITS 32]
inc dword [eax]
Musimy tą instrukcje skompilować, w tym celu wydajemy polecenie:
?nasmw asd.nasm?
jednak by kompilacja doszła do skutku przy użyciu kompilatora nasm musimy wypakować zawartość archiwum z nasmem do katalogu: c:masm32bin Po skompilowaniu utworzy nam się plik ?asd.?, który będzie zajmował 2 bajty. Jeżeli chodzi nasm to jest do tych celów idealny ponieważ tworzy czysty kod programu bez żadnych ?dodatków?. Po podglądnięciu naszego nowego pliku hexplorerem zauważyliśmy że ma on w sobie tylko FF 00, wnioskujemy, że trzeba zamienić stare dwa bajty w pliku first.exe:
FF 08 na nowe: FF 00
W myśl zasady o, której mówiłem wcześniej: 004012BA - ( 00400000 00001000 ) 400 = 6ba
Kiedy już zmienimy nasze dwa bajty, zapisujemy plik (file -> save) i uruchamiamy. Jak widać teraz nasze i się nie zmniejsza ale zwiększa dzieki czemu po krótkiej chwili osiągnie wartość 100 i pokaże nam się ?OK!?. Jeżeli wszystko jest dla Ciebie zrozumiałe w drugim sposobie przejdziemy do sposobu trzeciego.
IV - Pokaż OK.! ? sposób trzeci.
W sposobie trzecim zmodyfikujemy nasz program poprzez zmianę jumpów. Jedną z najważniejszych instrukcji crackera jest instrukcja 90 czyli nope ? nic nie rób czyli brak operacji. Zacznijmy od przekompilowania naszego programu first.exe. Zajmiemy się teraz warunkiem while czyli tym kodem:
004012AF loc_004012AF:
004012AF 837DFC63 cmp dword ptr [ebp-4],63h
004012B3 7E02 jle loc_004012B7
004012B5 EB34 jmp loc_004012EB
Jak wcześniej wywnioskowaliśmy, jeśli i <= 63 to skocz do tego pierwszego loc a jeśli nie to wykonaj drugi skok. Ten pierwszy skok tak naprawdę wrzuca nas na początek pętli i--; a do drugiego w normalnych warunkach program nie dociera czyli fajnie by było gdyby ten pierwszy jump zniknął albo trochę się zmienił. Sposoby są dwa: albo jle zmienimy na jgnop nop, czyli 7E 02 zastąpić 90 90.
Skorzystajmy ze sposobu numer dwa. Odszukajmy adres w pliku 6b3 i zmieńmy te dwa bajty na 90 90.
(jump greater, skocz jeśli większy) czyli znowu nasmem byśmy musieli się posłużyć albo wpisać tam po prostu
V - Zakończenie
Tak więc dobrnęliśmy do końca artykułu na temat podstaw crackingu. Mam nadzieje, że pomogłem co niektórym przybliżyć informacje o crackingu oraz w pewnym sensie zasadę wykonywania się programów w systemie operacyjnym. Podczas następnego ?natchnienia? omówię proces crackowania ?ambitniejszych? programów. A teraz chciałbym bardzo podziękować Gynvaelowi, bo właśnie między innymi dzięki niemu posiadłem wiedzę umożliwiającą napisanie tego artykułu.