Cracking - zasada działania

aZgon

Pewnego dnia siedząc na ircu, wdałem się w rozmowe z Gynvael Coldwind na temat podstaw i zasad crackowania programow. Kilka dni temu kiedy zostałem na weekend off-line postanowilem napisać artykuł na podstawie loga. Dzieki temu możecie przeczytać:

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~1\dev-cpp\bin;c:\masm32\bin

Jezeli wszystko dobrze zrobiles nie powinienes ujrzec zadnego bledu.

Zacznijmy od skompilowania programu (program nazwijmy first.c) ktorego kod zrodlowy widnieje ponizej:

#include<stdio.h>
#include<windows.h>

int
main( void )
{
  int i = 50;

  while( i < 100 )
  {
    i--;
    if( i < 0 ) i = 99;
    Sleep( 100 );   
    printf(" %4i\r", 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/C++ różnica miedzy „i—-a"/ i „(*i)-—", a te dwie linie to i--;. Następnie jest to <kbd>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 " %4i\r") 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:

C745FC32000000

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:\masm32\bin 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 jg (jump greater, skocz jeśli większy) czyli znowu nasmem byśmy musieli się posłużyć albo wpisać tam po prostu nop 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.

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.

Źródło:
http://azgon.s0laris.int.pl/cracking_–_zasada_działania.htm

24 komentarzy

Cracking to Reverse engineering ukierunkowany na zabezpieczenia. Należy rozgraniczyć łamanie zabezpieczeń i działalność 'cywilną'.

Ja tam kiedyś dość mocno zmieniłem IIS 5.0 pod Win2k żeby generował listę plików zgodną ze standardami :) więc nie zawsze cracking to ZŁO! ;)

Hehe raczej się nie napalać, bo jeszcze zaczniecie dzieciaczki crackować wielkie legalne aplikacje! W sumie to przerobienie prostego programu trwa do 2h. Gorzej z takimi grami komputerowymi :/

Bardzo ciekawy artykuł!! Chyba nauczę się podstaw Assemblera :D

Oczywiście wszelkie informacje wyłącznie w celach naukowych.
Dzieci, nie róbcie tego w domu ;)

404 był z powodu ł w adresie. 4p robiło z niego utf, a był w iso (czy tam cp). Art przerzuciłem tutaj, nie będzie chyba już problemu :-)

wrzuć to jako art albo to wywalam, bo znów 404 - bezsensu takie coś

Przecież Word ma możliwość zapisu do html... nie widzę problemu :) (no fakt - zależy jaką wersję używasz...)

Chyba najlepszym byloby html... Coz, nie ocenie, bo nie mam jak przeczytac :(

Thane: nie tylko u ciebie :P napewno juz usunęli z serwera :d

tak przy okazji napomnknę tylko że w pliku "hex_setup.exe" jest wirus. Innych jeszcze nie sprawdzałem.

No wlasnie cokolwiek tylko nie *doc
btw Office chodzi pod linuksem i to nawet ladnie :D
(potrzebny jest: crossover.office.pro.3.0.0 )

Bleh, zrób to w czymś innym, czymkolwiek, txt, html, tex, tylko nie jakiś zafajdany doc!
Albo scrackuj tak M$ office, żeby natywnie ruszył pod linuksem i był za darmo :P

azgon: plizzz nie .doc ;p pod konsola nie mam czym tego otworzyc... html, pdf, xhtml. xml, txt, cokolwiek, tylko nie m$ doc;p

Mi pisze, że strona została zablokowana. Nie mógłbyś podać linku do strony producenta tego oprogramowania?

Narzędzia można sciagnać ze strony:
http://azgon.republika.pl/cracking/

Ja opisałem na narzedziach, ktore posiadałem lub ktoś mi je polecił, jeżeli jednak ktoś preferuje inne to bardzo prosze, moze ich używać ;] A dzisiaj w nocy postaram sie wszystkie narzedzia wzucic pod jeden adres.

czemu tego sie nie da ściągnąć ?? :(

Chwilowo nie widać załączników - można go ściągnąć stąd: http://4programmers.net/bin/Cracking.doc
ale nie ma tam zbyt wiele tego 'formatowania' - polecałbym jednak wrzucić to jako tekst a nie załącznik. No i zbyt mocno uczepiłeś się wybranych narzędzi - nie mam żadnego z tych, które wymieniłeś a jednak takie sztuki nie są mi obce. To taka uwaga do ewentualnych kolejnych artów.

Jeszcze HTM: free.of.pl/s/sapero/Cracking.htm

free.of.pl/s/sapero/Cracking.zip oraz .gz