Rootkiti u korisničkom načinu rada - korištenje mehanizma Windows kuka

Prije nego što se u potpunosti odbaci pristup korišten u programu Naive, praktično je na tom primjeru pokazati jednu iznimno važnu tehniku koja se vrlo često koristi kod zloćudnih programa i često će se spominjati u raznim kontekstima u ovome radu.

Program Naive nije imao mogućnost detektiranja aplikacije, tj. nije mogao sam otkriti kada se pokrenuo Task Manager, odnosno Windows Explorer proces. Predloženo je rješenje u kojemu se izvodi radno čekanje s ispitivanjem da li su odgovarajući prozori stvoreni. Takav je pristup iznimno nepraktičan i naivan. No operacijski sustav Windows posjeduje mehanizam koji omogućava aplikacijama da djeluju na aktivnosti kao što su pokretanje novih prozora, njihovo zatvaranje, aktiviranje nekog postojećeg prozora, minimizacija prozora, zatim rad s tipkovnicom i mišem ("očitavanje" trenutnih koordinata miša ili praćenje unosa s tipkovnice), presretanje poruka između dviju aplikacija i dr. Mehanizam na engleskom nosi naziv Windows Hooks, hrvatski je prijevod pomalo nespretan, no koristit će se naziv Windows kuke.

Windows kuka je mehanizam koji se sastoji od dva entiteta: događaja (engl. event) i filtrirajuće funkcije koja se poziva kao reakcija na taj događaj. Mehanizam je vrlo sličan mehanizmu obrade prekida u operacijskim sustavima: kada se dogodi neki prekid, pokreće se procedura za obradu prekida čija je adresa određena u tablici prekidnih vektora/rutina. Windows operacijski sustav dozvoljava postojanje više filtrirajućih funkcija koje se odnose na jedan te isti događaj pa se u operacijskom sustavu interno održava lanac filtrirajućih funkcija, pri čemu se zadnje stvorena filtrirajuća funkcija dodaje na početak lanca. Filtrirajuća funkcija se aktivira nakon što se dogodi događaj za koji je ona registrirana (postavljena). Postavljanje filtrirajuće funkcije obavlja se pozivom funkcije SetWindowsHookEx(). Nakon što je filtrirajuća funkcija aktivirana, ona kao parametar prima strukture podataka koje se odnose na odgovarajući događaj i te parametre funkcija može izmijeniti, može ih propustiti nepromijenjene drugim funkcijama (korištenjem funkcije CallNextHookEx()) ili čak zaustaviti daljnje pozivanje filtrirajućih funkcija u lancu, eksplicitno zaustavljajući protok poruke kroz lanac. Ilustracija jednog takvog lanca dana je na slici.

Primjer lanca Windows kuke za WH_CBT tip kuke

Postoje različite kategorije Windows kuka u ovisnosti o događajima na koje želimo reagirati. Sve kategorije počinju s nazivom WH, iza čega slijedi naziv koji pobliže opisuje događaje na koje je potrebno reagirati. Na slici desno prikazan je primjer lanca za kategoriju kuke WH_CBT (engl. CBT – Computer Based Training). Ova vrsta kuke namijenjena je za stvaranje aplikacija koje korisnika uče radu na računalu, reagiraju kada se pokrene neki program ispisujući najčešće razne informacije o programu, prate aktivne prozore i slično (više o ovoj vrsti Windows kuke kao i o kukama općenito može se pročitati u vrlo dobrom tekstu [17K. Marsh, Microsoft Inc, Win32 Hooks, MSDN, srpanj 1993.]). Ta vrsta kuke pokazat će se vrlo korisnom u nadogradnji programa Naive. Na slici desno operacijski sustav želi stvoriti prozor porukom WM_CREATE, no prije nego što se prozor stvori, proces stvaranja prozora prolazi kroz WH_CBT lanac. Svaka filtrirajuća funkcija može prije slanja podataka ostalim funkcijama u lancu promijeniti parametre, što je naznačeno različitim bojama iscrtkanih strelica na slici. Tek nakon što zadnja funkcija završi s izvođenjem, šalje se poruka WM_CREATE odgovarajućem prozoru. Naravno, svaka funkcija u lancu može spriječiti pozivanje daljnjih funkcija, pa je moguće zaustaviti stvaranje odgovarajućeg prozora, tj. poruka WM_CREATE se nikada neće isporučiti. Nepisano pravilo kaže da je uvijek poželjno "nastaviti" lanac, tj. poslati podatke sljedećoj filtrirajućoj funkciji u lancu, no ništa ne garantira da je to uvijek slučaj. Štoviše, nakon umetanja filtrirajuće funkcije u lanac nemoguće je sa sigurnošću odrediti na kojem se mjestu u lancu ta funkcija nalazi (osim odmah pri samom umetanju kada je filtrirajuća funkcija sigurno na početku lanca). Ne postoje nikakve garancije da će umetnuta filtrirajuća funkcija ikada biti pozvana jer je naknadno možda umetnuta filtrirajuća funkcija koja parametre ne prosljeđuje ostalim funkcijama u lancu. Ipak, takvi su slučajevi vrlo rijetki zbog navedenog nepisanog pravila o pozivanju sljedeće funkcije u lancu kojeg se većina programa ipak pridržava.

Postoje dva osnovna oblika Windows kuka – na razini cjelokupnog sustava i na razini pojedine dretve. Neke kategorije kuka vezane su isključivo za cjelokupni sustav, dok se većina kuka može postaviti i na razini cijelog sustava i na razini pojedine dretve (potpuni popis vidjeti na prije navedenoj stranici [17K. Marsh, Microsoft Inc, Win32 Hooks, MSDN, srpanj 1993.]). Osnovna razlika između ta dva oblika jest kontekst u kojem se odgovarajuće filtrirajuće funkcije postavljene za tu kategoriju kuke izvode. U slučaju kuke na razini dretve, filtrirajuća se funkcija izvodi isključivo u kontekstu te dretve. Takav oblik kuke ne odnosi se na globalne događaje na razini cijelog sustava, već se odnosi na događaje vezane uz tu dretvu (npr. presretanje poruka poslanih toj dretvi, kontrola korisničkog unosa s tipkovnice i slično). Ako se želi pokrenuti akcija na razini cjelokupnog sustava (primjerice, otkriti stvaranje ili zatvaranje bilo kojeg prozora u sustavu), tada se kuka mora postaviti na razini cjelokupnog sustava. Kako bi to programski bilo moguće, filtrirajuća funkcija mora biti napisana u biblioteci dinamičkih veza (engl. DLLDynamic link library) koja se izravno može ubaciti u bilo koji proces u sustavu i izvoditi u kontekstu bilo koje dretve. Vrlo je važno uočiti da se filtrirajuća procedura u slučaju "globalne kuke" može izvesti u kontekstu bilo koje dretve na sustavu, a iz te činjenice proizlazi nekoliko problema. Prvi problem je dijeljenje resursa između dva odvojena procesa. Sve globalne varijable u tom su slučaju vidljive samo unutar jednog procesa i u potpunosti su nepoznate nekom drugom procesu. Varijable koje želimo dijeliti između procesa unutar DLL-a moraju biti smještene u dijeljeni odsječak adresnog prostora procesa. Tom dijelu adresnog prostora mogu pristupiti i ostali procesi pa se na taj način mogu dijeliti podaci između procesa. Prilikom dijeljenja podataka nikako se ne smiju dijeliti pokazivači na neke memorijske adrese (pokazivači nažalost čine većinu podatkovnih struktura Windows API-a) jer pokazivač u jednom procesu nema nikakvo značenje u drugom procesu i velika je vjerojatnost da pokazuje na posve krivu memorijsku adresu. Razrješavanje krive memorijske adrese najčešće vodi do pogreške pristupa (engl. access violation) i do rušenja programa. Budući da se filtrirajuća funkcija globalne kuke izvodi u kontekstu bilo koje dretve, greška u kôdu vjerojatno će biti pogubna po rad operacijskog sustava i zahtijevat će ponovno pokretanje sustava. To je ujedno i drugi problem globalnih kuka – prilikom stvaranja filtrirajuće funkcije nužna je velika pozornost kako bi kôd bio u potpunosti ispravan. Još jedan problem globalnih kuka jesu performanse operacijskog sustava. U slučaju postavljanja npr. WH_KEYBOARD globalne kuke koja je zadužena za otkrivanje, procesiranje i modificiranje svih događaja vezanih za tipkovnicu, naša specificirana filtrirajuća procedura (kao i sve ostale procedure u tom lancu) izvest će se svaki put kada korisnik pritisne tipku na tipkovnici. Jasno je da to može imati drastične posljedice po performanse sustava, osobito ako je filtrirajuća funkcija složena ili loše napisana. Zbog svega navedenoga pri postavljanju globalnih kuka i odgovarajućih filtrirajućih procedura potrebno je biti vrlo oprezan.

Globalne kuke (odnosno odgovarajuće filtrirajuće funkcije) postavljaju se najčešće tako da postoji glavni program koji je implicitno povezan s DLL-om u kojem se nalazi filtrirajuća procedura (povezivanje je izvedeno tijekom prevođenja samog programa) i koji poziva funkciju SetWindowsHookEx(). Nakon što se dogodi odgovarajući događaj, filtrirajuća se funkcija poziva u kontekstu procesa koji je uzrokovao aktiviranje filtrirajuće funkcije, tj. koji je uzrokovao događaj. Najčešće glavni program mora biti obaviješten o uspješnom aktiviranju kuke pa filtrirajuća funkcija porukom dojavljuje to glavnom programu. Ovaj je postupak ilustriran slikom dolje lijevo (slika je velikih dimenzija, no namjerno je ostavljena u takvom obliku radi što bolje preglednosti).

Djelovanje globalne kuke

Na slici su prikazana 2 procesa: Worker i Aplikacija. Worker program ima implicitno uključenu DLL datoteku koja se u kôd ugradila tijekom prevođenja. U DLL datoteci nalazi se poziv funkcije SetWindowsHookEx() kao i odgovarajuća filtrirajuća funkcija. Worker program pozivom funkcije SetWindowsHookEx() postavlja globalnu WH_CBT kuku. U trenutku kada Aplikacija želi stvoriti prozor pozivom funkcije CreateWindow(), taj se poziv (točnije isporuka poruke WM_CREATE od strane operacijskog sustava) privremeno zaustavlja i započinje se izvoditi filtrirajuća procedura u kontekstu Aplikacije. To je na slici prikazano umetanjem DLL biblioteke u adresni prostor Aplikacije. Namjerno je na slici DLL datoteka u adresni prostor Aplikacije smještena nešto "niže" nego što je to slučaj s Worker programom, kako bi se naglasila potpuna neovisnost tih adresnih prostora i činjenica da pokazivačka varijabla u jednom procesu pokazuje vrlo vjerojatno na sasvim krivu adresu u drugom procesu. Nakon umetanja DLL-a, pokreće se filtrirajuća funkcija koja funkcijom SendMessage() obavještava Worker program o aktiviranoj proceduri i eventualnim rezultatima. Filtrirajuća funkcija završava pozivanjem sljedeće funkcije u lancu. Pretpostavlja se da sve funkcije u lancu ispravno završavaju i da se dopušta stvaranje prozora kojeg je zatražila Aplikacija.

Mehanizam Windows kuka je u svojoj potpunosti znatno opširniji nego što je ovdje najosnovnije prikazano, no na navedenoj stranici [17K. Marsh, Microsoft Inc, Win32 Hooks, MSDN, srpanj 1993.] mogu se pronaći ostale potrebne informacije.

Kako se mehanizam kuka može iskoristiti kod Naive programa? Način njegove upotrebe identičan je onome opisanom na prikazanoj slici. Osnovna funkcionalnost Naive programa ostat će ista (za potrebe demonstracije skrivat će se samo proces u Task Manager aplikaciji, ne i direktorij u Windows Explorer aplikaciji, iako je postupak u principu isti). Programu se dodaje DLL datoteka unutar koje će biti funkcije za postavljanje i uklanjanje kuke odnosno filtrirajuće funkcije, koja se također nalazi unutar DLL datoteke. Glavni program će se "smjestiti" u sistemsku traku s programima i čekat će odgovarajuću poruku koju će primiti kada se stvori Task Manager proces (ili će reagirati na poruku izlaska koju mu može poslati korisnik odabirom opcije Exit u kontekstnom meniju). Nakon primitka te poruke, pokreće se već poznato "skrivanje" procesa.

Prvi korak je izrada DLL datoteke. DLL datoteka je u osnovi običan skup funkcija koji se može uključiti u bilo koji program (koji to eksplicitno zatraži, ili kojem programer namjerno umetne DLL kôd u adresni prostor, kao u ovom slučaju). Programski model biblioteka dinamičke veze sličan je bilo kojoj aplikaciji pisanoj u jeziku C, osim što je ulazna procedura (engl. entry point) nešto drukčija, a za Naive program (ova verzija nosit će naziv Naive – improved, glavni program nosi ime NaiveWorker, a DLL datoteka NaiveDLL) prikazana je na sljedećem ispisu:

BOOL APIENTRY DllMain(HINSTANCE hInstance, DWORD ul_reason_for_call, LPVOID lpReserved) { switch(ul_reason_for_call) { case DLL_PROCESS_ATTACH: hDLLInstance = hInstance; UWM_TASKMANCREAT = RegisterWindowMessage(UWM_TASKMANCREAT_MSG); break; case DLL_PROCESS_DETACH: //MakniTaskHook(hDialog); break; } return TRUE; }

Ulazna procedura nosi ime DllMain i prima nekoliko parametara od kojih nam je najvažniji drugi parametar. Taj parametar određuje "razlog" pozivanja DllMain() funkcije. DllMain() funkcija poziva se u slučaju kada neki proces ili neka dretva "ubace" kôd DLL datoteke u svoj adresni prostor (kôdovi DLL_PROCESS_ATTACH i DLL_THREAD_ATTACH), odnosno kada taj proces ili dretva završavaju (kôdovi DLL_PROCESS/THREAD_DETACH). U ovom slučaju, prilikom ubacivanja DLL datoteke u adresni prostor procesa inicijalizira se globalna varijabla koja pokazuje na instancu DLL datoteke i registrira se poruka UWM_TASKMANCREAT u operacijskom sustavu Windows. Dotična poruka će se slati procesu NaiveWorker i zbog toga ju je potrebno registrirati. Kada proces završava, najčešće se poziva funkcija koja oslobađa zauzete resurse (u ovom slučaju uklanja postavljenu kuku), no to će se obaviti iz NaiveWorker programa koji je kuku i postavio, tako da u ovom slučaju to nije potrebno.

U DLL datoteci potrebno je definirati i funkciju PostaviTaskHook() koja će obaviti postavljanje odgovarajuće kuke:

__declspec(dllexport) BOOL PostaviTaskHook(HWND hWnd) { if(hDialog != NULL) return false; //već postoji hook //tip kuke je WH_CBT, to nam omogućava praćenje stvaranja novih prozora //Procedura koja se poziva u slučaju stvaranja novog prozora je HookProc //Kuka je globalna, postavlja se za sve procese u sustavu, sto doslovno znači //da će se HookProc izvesti za svaki novostvoreni prozor hHook = SetWindowsHookEx(WH_CBT, HookProc, hDLLInstance, 0); //ako se nije dogodila greška, inicijaliziramo odgovarajući prozor koji predstavlja Worker //proces if(hHook != NULL) { hDialog = hWnd; return true; } return false; }

Sam potpis funkcije označava da se radi o funkciji unutar DLL datoteke, pa se s __declspec(dllexport) konstruktom označava da se ta funkcija "izvozi" iz datoteke, tj. da se može pokrenuti iz bilo kojeg procesa koji je uključio DLL datoteku u svoj adresni prostor. Na novostvorenu kuku pokazuje ručica hHook. Ostatak funkcije vrlo je dobro komentiran i ne traži dodatno objašnjenje. Tip kuke koji se postavlja je WH_CBT jer želimo reagirati u trenutku stvaranja Task Manager procesa (i to prije nego što se dotični prozor uopće stvori!).

Funkcija kao parametar prima ručicu na prozor koji označava glavni (ujedno i nevidljivi) prozor NaiveWorker programa kojem će se slati UWM_TASKMANCREAT poruka o aktiviranom Task Manager-u. Prozor se sprema u dijeljenu varijablu hDialog, što je ostvareno na sljedeći način:

#pragma data_seg(".SHRD") HWND hDialog = NULL; #pragma data_seg() #pragma comment(linker, "/section:.SHRD,rws")

Osim funkcije za postavljanje kuke, uvijek mora postojati funkcija za uklanjanje kuke (iako se sama kuka briše završetkom procesa koji je kuku postavio). Pozivom funkcije UnhookWindowsHookEx() uklanja se postavljena kuka. Funkcija MakniTaskHook() koja poziva tu funkciju i uklanja kuku ima sljedeći oblik:

__declspec(dllexport) BOOL MakniTaskHook(HWND hWnd) { if(hWnd == NULL || hWnd != hDialog) return false; BOOL ret = UnhookWindowsHookEx(hHook); if(ret) hDialog = NULL; return ret; }

Filtrirajuća funkcija ima naziv HookProc() i sljedećeg je oblika:

LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam) { TCHAR szBuf[256]; //provjeri da li je riječ o našoj kuki, ako nije, zovi sljedeću kuku u nizu if(nCode < 0) return CallNextHookEx(hHook, nCode, wParam, lParam); //ako je stvoren novi prozor if(nCode == HCBT_CREATEWND) { //dohvati ručicu na stvoreni prozor HWND hWinNew = (HWND) wParam; DWORD dPID; //zanima nas PID novog prozora, tj. njegovog procesa GetWindowThreadProcessId(hWinNew, &dPID); //otvaramo taj proces za pisanje (iako se ovaj kôd izvodi upravo u tom procesu) HANDLE hProc = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, dPID); //dohvaćamo ime procesa koji je određen ručicom hProc GetModuleFileNameEx(hProc, NULL, szBuf, 256); //ako se radi o Task Manager procesu if(!lstrcmpi(szBuf, _T("C:\\WINDOWS\\system32\\taskmgr.exe")) || \ !lstrcmpi(szBuf, _T("C:\\WINNT\\system32\\taskmgr.exe"))) { //pošalji poruku Worker aplikaciji, tj. njenom prozoru koji je određen //ručicom hDialog - PID se prenosi kao wParam PostMessage(hDialog, UWM_TASKMANCREAT, dPID, 0); } } //uvijek je pristojno pozvati sljedeću kuku u nizu, iako to nije nužno return CallNextHookEx(hHook, nCode, wParam, lParam); }

U slučaju da je stvoren novi prozor (kôd HCBT_CREATEWND) vrši se dojavljivanje NaiveWorker programu. Dok je u programu Naive "prepoznavanje" Task Manager aplikacije izvršeno po nazivu prozora (Windows Task Manager), ovdje se to radi na drukčiji način kako bi se demonstriralo da prvi način nije jedini. Task Manager aplikacija prepoznaje se prema svojoj izvršnoj datoteci koja se uobičajeno nalazi u direktoriju C:\Windows\system32\taskmgr.exe (Windows XP) ili C:\WINNT\system32\taskmgr.exe. Ovaj pristup nema prednosti pred prvim, samo je dan kao demonstracija drukčije mogućnosti. Valja naglasiti da su oba pristupa jednako loša jer se oslanjaju na podatke u operacijskom sustavu koji bi trebali biti uvijek isti, iako to ne mora biti tako. Napredniji program bi morao vršiti složenija ispitivanja kako bi ustvrdio da se doista radi o ciljanoj aplikaciji.

Ako se doista radi o Task Manager aplikaciji, filtrirajuća funkcija šalje UWM_TASKMANCREAT poruku NaiveWorker programu, koji je određen već navedenom hDialog ručicom. U poruci se kao parametar šalje i identifikator Task Manager procesa spremljen u varijabli dPID, a to je nužno kako bi NaiveWorker program mogao ispravno identificirati Task Manager prozor (ručicu na prozor bilo bi "opasno" slati jer, iako su ručice na prozore različitih procesa globalne i iste u oba procesa, preporuka kaže da se nikada porukama ne bi trebale slati nikakve ručice).

Središnji dio NaiveWorker programa čini takozvana petlja poruke u kojoj program prima poslane mu poruke i obavlja akcije u skladu s vrstom poruke. Dotična petlja u NaiveWorker programu ima oblik prikazan na donjem ispisu:

... PostaviTaskHook(hDialog); bool zastE = false; bool zastT = false; while (GetMessage(&msg, NULL, 0, 0)) { //ako je primljena poruka od strane Task managera (pokrenut je Windows Task Manager) if(msg.message == UWM_TASKMANCREAT) { //odspavaj, da dopustimo pojavu Worker aplikacije u listi procesa Sleep(300); //budući da će se kuka aktivirati za SVE STVORENE prozore (čak i za //prozore djecu, a Task manager ima mnogo prozora djece), nakon što smo //primili dojavu za glavni prozor ne želimo daljnje dojave if(zastT == true) continue; //PID se šalje kao wParam u poruci dTaskManPID = (DWORD) msg.wParam; //moramo naći ručicu na Task manager prozor if(!EnumWindows(NadjiProzor, 0)) { //naišli smo ili na Task manager prozor ili se dogodila greška u //enumeraciji if(hTaskManWnd == NULL) ; //ovo je greška, trebalo bi ju nekako handlati //stvori dretvu koja će vršiti obradu HANDLE hThreadT = CreateThread(NULL, 0, TaskManagerObrada, (LPVOID) hTaskManWnd, 0, NULL); CloseHandle(hThreadT); Sleep(5000); zastT = true; } continue; } if(!IsDialogMessage(hDialog, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } zastE = false; zastT = false; } MakniTaskHook(hDialog); ...

U NaiveWorker programu najprije se ugrađuje odgovarajuća kuka, a zatim se ulazi u glavnu petlju poruke. Kada se primi definirana UWM_TASKMANCREAT poruka, započinje se s obradom liste procesa. Najprije je iz PID-a Task Manager procesa potrebno dobiti ručicu na odgovarajući prozor što se obavlja pozivom funkcije EnumWindows(), odnosno pozivom odgovarajuće funkcije povratnog poziva – NadjiProzor(). Važno je uočiti da će se za svaki stvoreni prozor dijete koji pripada Task Manager procesu (a takvih je vrlo mnogo) pozvati filtrirajuća funkcija koja će poslati odgovarajuću poruku i uvijek će gornji uvjet biti istinit. Kako bi spriječili stvaranje dretve i pokretanje obrade za svaki prozor, a ne samo za glavni, uvodimo kontrolne zastavice koje to sprječavaju. Funkcija NadjiProzor() vrlo je jednostavna :

BOOL CALLBACK NadjiProzor(HWND hWnd, LPARAM lParam) { DWORD dTrenutniPID; GetWindowThreadProcessId(hWnd, &dTrenutniPID); if(dTrenutniPID == dTaskManPID) { hTaskManWnd = hWnd; return false; //malo neobično, no false prekida enumeraciju, našli smo naš Task Manager prozor } return true; }

Funkcija uspoređuje PID primljen od strane filtrirajuće funkcije s PID-om prozora koji se trenutno enumerira (kroz funkciju EnumWindows()). Ako su ta dva identifikatora jednaka, onda je pronađen odgovarajući prozor i u petlji prikazanoj na jednom od prethodnih ispisa se dalje stvara dretva koja vrši obradu već poznatom funkcijom TaskManagerObrada(). Ostatak programa identičan je Naive programu.

Uvođenjem mehanizma Windows kuke program nije postao značajnije "bolji", zapravo, niti jedan ključni problem programa Naive nije riješen i taj program i dalje ostaje praktički neupotrebljiv. No iskazani mehanizam vrlo je važan i često korišten, kako u zloćudnim programima, tako i u antivirusnim aplikacijama. Windows kuku može ugraditi običan korisnik, čak i kada se radi o globalnoj kuki i sasvim je jasno da su kuke napadačev najčešći izbor za umetanje zloćudnog koda u bilo koji proces na sustavu. Razvojni inženjeri Windows operacijskog sustava vjerojatno su ostavili globalne kuke "dostupne" i običnom korisniku kako bi antivirusni softver, procesne zaštitne stijene, ali i brojni drugi softver mogao normalno funkcionirati i s ograničenim ovlastima.

U svakom slučaju, program za zaštitu morao bi na neki način imati nadzor nad kukama u sustavu. Ne postoje funkcije koje bi omogućavale prebrojavanje svih kuka, niti su podatkovne strukture u jezgri operacijskog sustava koje se odnose na kuke dokumentirane, no svakako bi program za zaštitu trebao nadzirati barem pozive funkcije SetWindowsHookEx() kao osnovnim (ali ne i jedinim) načinom za stvaranje kuke.

Natrag na vrh