................... ...::: phearless zine #7 :::... ................>---[ Hiding Processes Using Windows Drivers ]---<.............. ............................>---[ by C0ldCrow ]---<............................. c0ldcrow.don@gmail.com SADRZAJ: [> 1. <] UVODNA RIJEC [> 2. <] IZRADA WINDOWS DRAJVERA [> 3. <] WINDOWS SYSTEM CALL HOOKING [> 3.1 <] Hooking u SSDT-u [> 3.2 <] Uklanjanje zastite s SSDT [> 3.3 <] Hooking NtQuerySystemInfromation funkcije [> 3.4 <] Kod lazne funkcije [> 4. <] DIRECT KERNEL OBJECT MANIPULATION [> 4.1 <] EPROCESS objekt [> 4.2 <] Izmjena EPROCESS bloka [> 4.3 <] Trazenje procesa po imenu [> 5. <] KRAJ //////////////////////////////////////////////////////////////////////////////// [> 1. <] UVODNA RIJEC \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ Prvobitna zamisao za ovaj tekst bila mi je napisati daleko opsirniji i veci tekst posvecen usporedbi skrivanja procesa na windows i linux operativnom sustavu koristeci iste tehnike. Kako sam sve vise pisao tekst shvatio sam da ce tako nesto biti dosta tesko napraviti. Tekst bi vjerojatno narastao preko 1500 linija. Tesko bi mi bilo povezati teme u tako velikom tekstu pogotovo ako bih se odlucio prvo predstaviti Windows pa potom Linux, pa onda razlike. Vjerujem da bi trebalo mnogo koncentracije za citanje takvoga tekst, a i sam bi se, vrlo vjerojatno, pogubio u vlastitoj temi. Tako je i nastao ovaj tekst, velik onoliko koliko mi moje slobodno vrijeme dopusta, a proizasao iz odluke da temu ipak podijelim na dva teksta. Prvi se bavi Windows OS-om, a drugi ce biti posvecen Linuxu i samoj usporedbi. Moji planovi su da taj drugi dio objavim u sljedecem broju phearless-a. Nadam se da ce se ostvariti. Ukoliko ne, ovo je i ovako samostalan tekst i tema. Ovu temu inspiriralo je vlastito iskustvo u pisanju rootkita za Windows OS, a sama cinjenica da u dosadasnjim brojevima phearless-a nije bilo takve teme me je dodatno potaknila da izlozim svoja iskustva. Testirao sam ovo skrivanje procesa s raznim alatima koji prikazuju listu procesa na racunalu. To su: - Windows Task Manager - ProMo (An advanced Windows NT taskmanager) by rattle http://www.awarenetwork.org/home/rattle/projects/ - TuneUp Process Manager Niti jedan alat nije otkrio skriveni process niti pokazao ikakve sumnjive ili cudne rezultate. Moram se prije pocetka teksta ispricati zbog problema s nazivima i prevodenjem s engleskog jezika. Termini na engleskom mi se izmejenjuju s hrvatskim jezikom bez imalo pravila. No nazalost ne znam kako da ih prevedem da zadrze isti smisao kao i u engleskom jeziku. Takoder moj kod je uvijek na engleskom jeziku, iznimka su jedino komentari koje sam napisao na hrvatskom, no filozofski gledano komentari zapravo i nisu dio koda ;) Tekst je organiziran u dva velika djela posvecena dvijema tehnikama koje opisujem. Prvi dio posvecen je "Hooking-u Windows System Calls", a drugi "Direct Kernel Object Manipulation" tehnici. Cak se svaki od tih djelova moze citati kao zaseban tekst. Svaki dio opet ima neke svoje poddjelova u kojima rjesavam korak po korak probleme na koje se moze naici. Tesko mi je reci koje bi predznanje bilo potrebno za razumjevanje teksta. Sav kod je u C programskom jeziku. Jedino sto mislim da bi trebalo malo poznavati za potpuno razumjevanje je "type casting" kojega ovdje ima i previse, buduci da je kernel prepun najrazlicitijih i najcudnijih vrsta podataka. Takoder svako iskustvo u programiranju je dobrodoslo. Nadam se da sam napisao nesto korisno. //////////////////////////////////////////////////////////////////////////////// [> 2. <] IZRADA WINDOWS DRAJVERA \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ Prije no sto pocnem prikazivati samu tehniku skrivanja procesa na windows kernelu bilo bi dobro da ukratko spomenem kako uopce napraviti neki kernel modul za windows i kako ga pokrenuti. Nas kod ce biti izgraden u obliku drajvera napisanog u programskom jeziku C. Normalno, to je potrebno kompajlirati, no kompajliranje ide nesto drukcije nego kod standardnih C programa. Visual Studio C++ ne moze kompajlirati drajvere za Windows kernel. Za izradu drajvera trebati ce vam DDK (Driver Development Kit). Iso image mozete skiniti s microsoftove stranice. http://www.microsoft.com/whdc/devtools/ddk/default.mspx Iso image verzije za Windows 2003 SP1 ima oko 230 MB-a. Ta verzija moze kompajlirati drajvere za Windows 2003, XP i 2000. Takoder postoji free i checked okruzenje za izradu drajvera za svaku verziju OS-a. Razlika izmedu njih je u tome sto kod free okruzenja kompajler ukljucuje optimizacije i sl. jer se podrazumjeva da je tada drajver gotov i spreman za kernel. Checked se koristi za razvoj i testiranje drajvera. Mali problem je vrijeme potrebno za instalaciju DDK-a. Meni je trebalo gotovo 1h, pa je dobro pripremiti se na to. Kod samoga drajvera stavljajte u jedan direktorij ili poddirektorije direktorija koji sadrzi jos dvije datoteke. Jedna je MAKEFILE (podsjeca na linux), a druga SOURCES. Obje su obicne tekstualne datoteke, a SOURCES bi trebala izgledati otprilike ovako: -------------------------- SOURCES --------------------------------------------- TARGETNAME=MOJDRAJVER TARGETPATH=OBJ TARGETTYPE=DRIVER SOURCES=mojkod.c -------------------------- SOURCES --------------------------------------------- TARGETNAME je ime vaseg drajvera. TARGETPATH kontrolira gdje ide drajver kada je kompajliran. Ostavite na obj i kompajlirani drajver ce vam biti u podirektoriju pod ovim imenom objfree_wxp_x86/i386/.sys. Ako radite za win xp. TARGETTYPE definira da treba kompajlirati kernel drajver, to ce te, razumljivo uvijek postaviti na vrijednost DRIVER. DDK moze kompajlirati i "normalne" programe ukoliko u TARGETTYPE stavite vrijednost PROGRAM. U SOURCES ide popis svih datoteka koje sadrze kod vaseg drajvera. U makefile datoteci dovoljna je ova linija: -------------------------- MAKEFILE -------------------------------------------- !INCLUDE $(NTMAKEENV)\makefile.def -------------------------- MAKEFILE -------------------------------------------- S naredbom "build" bilo u free bilo u checked okruzenju kompajlirate svoj drajver. Ispocetka je malo nezgodno raditi s DDK-om buduci da nemate IDE kao sto je slucaj s MSVC++, nego ce te morati raditi u sucelju kakvo ima i cmd.exe. Na to sam mislio kada sam rekao "naredba build". Kod vaseg drajvera mora imati funkciju DriverEntry() koja predstavlja ulaznu tocku vasega koda (ekvivalent main() funkciji u "normalnim" programima). System proces poziva DriverEntry() funkciju ukoliko je drajver ucitan u kernel preko SCM-a. Osim DriverEntry() funkcije moguce je imati i funkciju za unload drajvera, odnosno funkciju koja ce biti pozvana kada treba maknuti drajver iz kernela. Ona se mora registrirati kao callback funkcija unutar DriverEntry()-a. -------------------------- KOD driver.c ---------------------------------------- NTSTATUS DriverEntry(IN PDRIVER_OBJECT ThisDriverObject, IN PUNICODE_STRING RegistryPath) { ThisDriverObject->DriverUnload=exitroutine; } NTSTATUS exitroutine() { } -------------------------- KOD driver.c ---------------------------------------- Situacija je slicna Linux kernelu gdje imamo init_module() i cleanup_module(). Windows drajveri ne moraju registrirati DriverUnload funkciju. Ukoliko to ne naprave nece se moci maknuti iz kernela skroz do sljedeceg reboota. Prilikom pisanja drajvera korisno bi bilo da mozete gledati sto on trenutno radi i kako se kod izvrsava. Umjesto da se mucite s kernel debugerima mozete koristiti jednu funkciju koja je ekvivalent printf() funkciju u kernel modu. Rijec je o funkciji DbgPrint(), koja prihvaca sve argumente bas kao i printf. Ta funkcija moze biti od velike pomoci prilikom otklanjanja odredenih gresaka. Problem je jedino kako pronaci output funkcije. Za to vam moze koristiti ovaj program: http://www.microsoft.com/technet/sysinternals/utilities/debugview.mspx Normalno DbgPrint() i nije bas zamjena za debuggera, ali svakako olaksava posao. I takoder u konacnoj verziji drajvera nebi bilo dobro da imate niti jedan DbgPrint() jer ipak se trudite biti sto vise stealth. Pokretanje vaseg drajvera (ucitavanje u kernel) je nesto slozenije. Najpravilniji nacin za to je uporaba SCM (Service Control Managera). Mozete se sami napisati jednostavan C program koji ce koristeci SCM API ucitati vas drajver u windows kernel i pokrenuti ga. //////////////////////////////////////////////////////////////////////////////// [> 3. <] WINDOWS SYSTEM CALL HOOKING \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ Hooking u biti znaci promjena toka izvršavanja koda unutar kernela. Cilj te tehnike je osiguranje izvršavanja nekoga našega koda svaki puta kada neki program zatraži popis svih procesa koji se trenutno izvršavaju na računalu. Taj naš kod bi se trebao izvršiti umjesto pravoga koda koji radi taj posao, napraviti isto što i pravi kod, ali samo iz popisa procesa izbaciti onaj proces koji zelimo sakriti i takav popis potom vratiti programu koji je to zatrazio od kernela. Slika: +-+-+-+-+-+ UPIT: Lista procesa +-+-+-+-+-+-+ | | ---------------------------> | | |Userland | | Kernel | |process | ODGOVOR: Lista procesa | kod | | | <--------------------------- | | +-+-+-+-+-+ +-+-+-+-+-+-+ +-+-+-+-+-+ UPIT: Lista procesa | | ------------------------->XX +-+-+-+-+-+-+ |Userland | | | | |process | | | Kernel | | | ODGOVOR: lazna lista | | kod | +-+-+-+-+-+ <------------------| | | | | | +-+-+-+-+-+-+ | | | | | |--> +-+-+-+-+-+-+ | | | | | Nas kernel| | | kod | |-------- | | +-+-+-+-+-+-+ Pri tome, normalno, proces koji je trazio listu od kernela ne zna da je dobio laznu listu. On ce tu listu upotrijebiti kao da je prava i korisniku prikazati procese bez onoga kojega smo mi sakrili. Ostatak ovoga teksta sve do DKOM tehnike opisuje kako tocno to ostvariti uz pomoc koda i objasnjava sto je sve potrebno znati i koje probleme rjesiti da bi uspjeli napraviti kod koji obavlja taj zadatak. Na Linux operativnom sustavu princip hookinga je doslovce isti. Osigurati da se jedan kod glumi drugi. Kratko i jasno. -----[ 3.1 Hooking u SSDT-u ]--------------------------------------------------- Proces ukoliko zeli neku uslugu od kernela ili informaciju (kao sto je lista trenutno aktivnih procesa) mora prijeci u kernel mod gdje ce se potom izvrsiti odredena funkcija (system call) koji ce obaviti trazeni posao. Na windowsu prijelaz u kernel mod se postize uz pomoc dvije assmebly instrukcije "int 2e" ili "sysenter". Na linuxu takoder postoji koncept system call-a i prelaska u kernel mod, to se ostvaruje uz pomoc instrukcije "int 0x80". Ta "int" instrukcija u biti pokrece "software interrupt". Pri tome prijelazu vazna nam je jedna vrijednsot. To je vrijednost pohranjena u registru EAX (u trenutku izvrsavanja prijelaza) i ona predstavlja index sys call funkcije koju zelimo pozvati. Taj index jednoznacno odreduje system call i nacin je na koji user mode proces kaze kernelu koju je funkciju potrebno izvrsiti. On konkretno sluzi za pronalazak memorijske adrese funkcije koju treba izvrsiti. Indexi se obicno vezu uz polja i dohvat njihovih clanova. O tome je rijec i ovdje. Taj index se koristi kako bi se vrlo brzo pronasla adresa funkcije u jednom polju koje sadrzi mnogo tih adresa. Vrijednost indexa se pomnozi s 4 (rijec je o 4 bajta (32 bita) koliko je dugacka jedna adresa u polju) time se dobije offset koji treba dodati na pocetak polja kako bi se doslo do prvog bajta adrese koju trazimo. Ukoliko imamo index 0x25 (rijec je o system call-u NtCreateFile koji sluzi za otvaranje datoteka) znaci da je adresa te funkcije 37 po redu u tome polju adresa. Jedna stranica koja sadrzi popise windows system call funkcija i njihovih indexa za razlicite verzije OS-a je: http://www.metasploit.com/users/opcode/syscalls.html Polje koje sadrzi te adrese u windows kernelu naziva se "System Service Dispatch Table" u daljnjem tekstu SSDT. Rijec "Table" iz naziva moze navesti malo na krivo razmisljalje. U stvarnosti nije rijec o tablici jer u memoriji je to u biti organizirano kao sljedni niz istovrsnih clanova (polja u programiranju). Dakle u tom polju su zapisane adrese system call funkcija. Uz SSDT biti ce nam potrebno jos jedno polje. Ovo se zove KeServiceDescriptorTable. To polje ce nam trebati da uopce mozemo doci do SSDT-a u memoriji, tj da nademo gdje pocinje SSDT i koliko je veliki. To polje, izmedu ostaloga, sadrzi adresu SSDT-a te broj elemenata (adresa) u SSDT-u. Koristiti cemo njega za pristpu SSDT-u buduci da je to polje exported u kernelu, pa cemo prema tome njemu moci lagano pristupiti. Iako je ono exportirano u kernelu to polje nije dokumentirano. Takoder malo uvjetno shvatite ovo polje. Mi cemo u kodu clanovima KeServiceDescriptorTable-a pristupati preko struktue koju cemo koristiti da napravimo type cast. Sve ovo sto sam opisao prikazuje ova slika: KeServiceDescriptorTable: +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ SSDT Base | ServiceCounterTable | Broj Elemenata | SSPT Base | address | | u SSDT | address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | | | \|/ SystemServiceDispatchTable: +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 1. adresa | 2. adresa | | Zadnja adresa | | (4 bajta) | (4 bajta) | (...) | (4 bajta) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Od ovoga zapravo nije tesko sebi postaviti pitanje sto ce se dogoditi ako neku adresu u SSDT zamjenimo s nekom drugom. Odgovor je nista, poceti ce se izvrsavati kod na toj drugoj adresi. To znaci da, ukoliko umjesto neke adrese stavimo adresu funkcije koju sadrzi nas drajver ona ce se izvrsiti umjesto nekoga sys call-a cija je adresa tamo prije bila zapisana. I to je "hooking". Na taj nacin mozemo osigurati da se nas kod izvrsava svaki puta kada bi se trebao izvrsiti neki syscall. Sada znamo da imamo polje u kernelu koje se koristi kako bi se nasla adresa funkcije koju je neki korisnicki program zatrazio. Znamo takoder kako doci do toga polja - uz pomoc KeServiceDescriptorTable-a. I znamo da, ukoliko zamjenimo adresu nekog syscalla u SSDT s adresom svoje funkcije osigurali samo da se nasa funkcija izvrsava umjesto pravog syscalla. Na Linuxu takoder postoji jedno ovakvo polje koje ima istu ulogu kao sto ima i na windowsu. Sadrzi adrese syscall funkcija. I na Linuxu se do tih adresa dolazi ovisno o indexu koji se nalazi u registru eax prilikom prelaska u kernel mod. To polje se zove "sys_call_table". Ima jedna zanimljivost sto se tice razlike izmedu 2.4 i 2.6 kernela. Na 2.4 kernelu to polje je lagano dostupno bilo kojem kodu kernela, no na 2.6 kernelu sys_call_table[] vise nije exported simbol. Pa je hooking na 2.6 kernelu utoliko teze izvesti sto je prvo potrebno pronaci adresu sys_call_table-a u memoriji. Ovim kratkim osvrtom na linux zelio sam samo istaknuti veliku slicnost izmedu dva OS-a. Buduci da postoje gotovo iste strukture podataka i "hooking" se ostvaruje generalno na isti nacin. ----- [ 3.2 Uklanjanje zastite s SSDT ] ---------------------------------------- Da nebi stvari bile tako jednostavne postoje neki problemi koje treba prvo prekociti. Prvi od njih je memorijska zastita koju kernel postavlja na SSDT. Naime, SSDT se nalazi u djelu memorije koji je mapiran kao "read only". Time su nasi pokusaji da zamjenimo adrese u startu osudeni na propast. Mozete i probati pisati po takvome dijelu memorije, nece se nista posebno opasno dogoditi, osim jednog brzog reboot-a (barem meni, ili BSOD). To je i jedan od razloga zasto kernel programiranje zna biti izrazito tesko. Ako vam se dogodi da pogrijesite na takvome mjestu znatno je teze uociti gdje je tocno greska, kao kod normalnih programa koje samo opet pokrenete pa isprobavate dalje. To je napravljeno u kernelu jer pretpostavlja se da nema legitimne potrebe za mjenjanje sadrzaja SSDT-a, nego je dovoljno samo procitati adrese. Znaci nas drajver nece moci pisati po tome polju jer mu je to zabranjeno u kernelu. No tu sada u biti treba shvatiti da, iako su nasem drajveru tu postavljene prepreke, on je jos uvijek dio kernela i moze raditi sto zeli. Pa i jednostavno promjeniti odnosno ukloniti zastitu s tog djela memorije i potom pisati po SSDT-u. Kada u SSDT-u drajver promjeni ono sto je trebao, moze jednostavno vratiti zastitu kako je bila na pocetku i kao da se nista nije dogodilo. Jedan od nacina uklanjanja zastite za pisanje s SSDT-a je uporaba MDL-a. MDL je akronim za "Memory Descriptor List". Pomocu MDL-a mi mozemo opisati jedan dio memorije u kernelu. Svaki tako opisan dio memorije ima svoju pocetnu adresu, proces kojemu pripada, broj bajtova i odredne flagove koji opisuju atribute tog djela memorije. Da bi opisali dio memorje s MDL-om u drajveru cemo koristiti strukture koje su deklarirane u "ntddk.h" Trebati ce nam tip podataka PMDL koji u biti predstavlja pokazivac na strukturu _MDL. Prvo moramo alocirati jedan MDL koji ce biti dovoljno veliki da mapiramo dio memorije na kojemu zelimo promjeniti zastitu. -------------------------- KOD driver.c ---------------------------------------- typedef struct ServiceDescriptorTable { unsigned int *SSDPBase; unsigned int ServiceCounterTable; unsigned int NumberOfService; unsigned int *SSPTBase; } ServiceDescTable; __declspec (dllimport) ServiceDescTable KeServiceDescriptorTable; -------------------------- KOD driver.c ---------------------------------------- Prvi dio koda i nema mnogo veze s MDL-ovima ali je nuzan jer nam omogucava da u nasem programu pristupimo polju "KeServiceDescriptorTable" koje je exportirano u kernelu. Deklarirali smo strukturu koja nam treba za type casting. U strukturi nisu vazna imena, nego je vazna velicina, mora biti jednaka stvarnom polju. Znaci ako je KeServiceDescriptorTable u kernelu 45 bajtova onda toliko mora biti velika i nasa struktura (45 bajtova samo za primjer). -------------------------- KOD driver.c ---------------------------------------- PMDL PmdSSDP; PVOID *MappedSSDT; PmdSSDP=IoAllocateMdl(KeServiceDescriptorTable.SSDPBase, (KeServiceDescriptorTable.NumberOfService*4), FALSE, FALSE, NULL); if(!PmdSSDP) return STATUS_UNSUCCESSFUL; MmBuildMdlForNonPagedPool(PmdSSDP); PmdSSDP->MdlFlags=PmdSSDP->MdlFlags | MDL_MAPPED_TO_SYSTEM_VA; MappedSSDT=MmMapLockedPages(PmdSSDP, KernelMode); -------------------------- KOD driver.c ---------------------------------------- Dalje, deklarirali smo varijablu koja nam predstavlja pokazivac na strukturu _MDL. Funkcija IoAllocateMdl() alocira MDL dovoljno veliki za mapiranje odredenog djela memorije, a on je odreden s prvim argumentom toj funkciji koji predstavlja pocetnu adresu u memoriji za koju treba napraviti MDL i drugim argumentom koji predstavlja za koliko bajtova treba napraviti MDL (racuna se od pocetne adrese). Ta funkcija ima i dodatne mogucnosti koje ovdje ne koristimo, pa su zato tako postavljeni zadnja tri argumenta. Tu nam sada koristi ono polje "KeServiceDescriptorTable" jer preko njega dajemo funkciji IoAllocateMdl() pocetnu adresu SSDT-a u memoriji i broj bajtova koje ona sadrzi. U biti mi smo ovdje izgradili jedan MDL koji opisuje cijeli SSDT. Funkcija MmBuildMdlForNonPagedPool() je jednostavna. Nju mozemo rastumaciti da ona nas MDL koji opisuje neki memorijski prostor prebacuje iz paged memory i osigurava da se ono nalazi u non paged djelu memorije. Linija koda odmah ispod te funkcije u biti skida zastitu s SSDT-a. Samo smo promjenili flagse koji pripadaju nasem MDL-u. Na kraju funkcija MmMapLockedPages() mapira memoriju opisanu nasim MDL-om i time varijabla MappedSSDT predstavlja istu adresu kao i SSDT, samo sto sada mozemo pisati po toj memoriji. Uz pomoc toga kratkoga koda mi smo uspjeli ukloniti zastitu s SSDT-a i sada slobodno mozemo pisati po SSDT-u kako nas je volja. ----- [ 3.3 Hooking NtQuerySystemInfromation funkcije ] ------------------------ Dosli smo i do samoga djela u kojemu cemo zamjeniti vise te adrese u SSDT. Jos nisam rekao koju funkciju cemo zamjeniti s nasom. Rijec je o funkciji (dugoog imena) NtQuerySystemInformation(). Funkcija se koristi za pribavljanje raznih informacija o sustavu (racunalu). U prvom argumentu funkciji dajete tip informacija kojeg zelite dobiti, a u drugom argumentu buffer u kojega ce vam funkcija vratiti rezultat u formatu koji ovosi o prvom argumentu. Jedan od tipova informacija koje ta funkcija moze "pribaviti" jesu informacije o svakom procesu na racunalu. Upravo to koriste programi tipa "task manager". Opcenito govoreci, da bi zamjenili adrese mi moramo doci u SSDT-u do mjesta gdje je zapisana adresa "Nt" funkcije i to mjesto prebrisati s svojom adresom. To cemo napraviti uz pomoci indexa. Moramo naci index Nt funkcije koji ona ima u SSDT-u i onda pomocu varijable "MappedSSDT" jednostavno doci do mjesta kojeg trazimo, tamo upisati nasu adresu i posao je gotov. Index koji trazimo pronaci cemo u jednoj drugoj funkciji i to u njezinome kodu. Ta druga funkcija se zove ZwQuerySystemInformation(). Ima iste parametre kao i "Nt" funkcija, ali ona je dostupna u kernelu i njezina adresa je poznata. Odakle se stvorila ta funkcija sada? Ona je zapravo dio standardnog procesa prelaska iz user moda u kernel mod. Kada neki program pozove neku Win32 funkciju, taj poziv preuzimaju neke od funkcija u Win32 DLL-ovima. One na neki nacin "pripremaju" teren za pravi posao, npr. provjera parametara. No na kraju one moraju pozvati jednu od funkcija koje na raspolaganju ima NTOSKRNL. A za pozivanje tih funkcija postoji jos jedan DLL. To je NTDLL.DLL. On sve funkcije koje ima NTOSKRNL cini dostupnima Win32, POSIX ili OS/2 podsistemu. Jedan od glavnih zadaca NTDLL.DLL-a je da u eax registar postavi index systemcall-a koji se treba izvrsiti. Taj proces se odvija kada korisnici program prelazi u kernel mod. Za drajvere koji pozivaju neke od syscall-a prijelaz se odvija upravo pomocu vec spomenute Zw funkcije. One pripremaju eax registre i pozivaju int 2e. To znaci da ce nas kod morati citati kod te Zw funkcije i naci dio gdje ona postavlja vrijednost indexa u eax. To i nece biti problem jer je Zw funkcija dostupna i eksportirana u NTOSKRNL-u. Tako da nam je njezina adresa poznata. Funkcija zapocinje ovim kodom: mov eax,BROJ BROJ je tipa ULONG i predstavlja index Nt funkcije u SSDT-u. Prema tome, ovaj jednostavni kod bi trebao vratiti index Nt funkcije u SSDT-u: *(PULONG)((PUCHAR)ZwFunction+1) Varijabla "ZwFunction" sadrzi adresu "Zw" funkcije i uz pomoc malo carolije type castinga mi dohvacamo vrijednost koja se nalazi jedan bajt poslje adrese koju sadrzi varijabla "ZwFunction" i to dohvacamo sljedeca 4 bajta poslje te adrese (povecamo ZwFunction adresu za 1 i gledamo na to kao pokazivac na ULONG). Time smo dohvatili "BROJ" iz instrukcije mov eax,BROJ. Jer treba se sjetiti da u memoriji instrukcije nisu zapisane u assemblyu (normalno) vec u binarnom obliku, konkretno mov instrukcija kao jedan bajt i njezin operand odmah iza nje. Zato dodajemo 1 na adresu ZwFunkcije. Sada kada imamo index, nije problem zamjeniti adrese u SSDT-u. MappedSSDT[INDEX]=(LONG)NasaFunkcija; I to je to. No nebi bila dobra praksa ostaviti nas kod u bas takvome obliku. Prvo bi trebali napraviti da se ta operacija zamjene adresa odvija atomski. Kako bi osigurali da nas nista nece prekinuti u sred posla, jer ipak je SSDT jedna od osnovnih kernel struktura. Za atomske operacije mozemo koristiti "InterlockedExchange" A osim toga, zgodno je taj kod staviti u macro-e kako bi se mogli jednostavno upotrebljavati u ostatku programa. -------------------------- KOD driver.c ---------------------------------------- #define FIND_SYSCALL_INDEX(Func) *(PULONG)((PUCHAR)Func+1) #define HOOK_SYSCALL(OrigFunction, OurFunction) InterlockedExchange((PLONG)&MappedSSDT[FIND_SYSCALL_INDEX(OrigFunction)], (LONG)OurFunction) -------------------------- KOD driver.c ---------------------------------------- Ovdje je i jedna veca razlika izmedu Linuxa i Windowsa. Linux, naime, nema jednu funkciju tipa QuerySystemInformation koja bi vracala razne informacije o racunalu. Lista trenutno aktivnih procesa na linuxu obicno se dobiva iz proc fs-a, kao sto to radi ps. Pa bi onda bilo potrebno na linuxu napraviti hooking syscall-a koji omogucuju citanje po proc fs-u. ----- [ 3.4 Kod lazne funkcije ] ----------------------------------------------- Sada smo osigurali da nasa funkcija bude pokrenuta svaki puta kada netko trazi listu procesa na racunalu i za to koristi funkciju NtQuerySystemInformation(). Jos nam je preostalo da napisemo nasu laznu funkciju koja ce vratiti sve procese na racunalu ali bez onoga kojega mi skrivamo. Nije tesko pretpostaviti da pisanje cijelog toga koda nije lagana stvar i kod bi bio prilicno dugacak. Zato cemo napraviti da nasa funkcija jednostavno pozove pravu funkciju, koja ce joj vratiti pravu listu procesa. Nasa funkcija ce iz te prave liste procesa ukloniti ono sto nebi trebalo tamo biti i dalje vratiti tu listu aplikaciji. Kao sto sam prije napisao funkcija vraca listu procesa u jednom bufferu. Taj buffer u biti sadrzi jednu strukturu za svaki proces koji postoji na racunalu. Strukture izgledaju ovako: -------------------------- KOD driver.c ---------------------------------------- struct SYSTEM_THREADS_INFO { LARGE_INTEGER KernelTime; LARGE_INTEGER UserTime; LARGE_INTEGER CreateTime; ULONG WaitTime; PVOID StartAddress; CLIENT_ID ClientIs; KPRIORITY Priority; KPRIORITY BasePriority; ULONG ContextSwitchCount; ULONG ThreadState; KWAIT_REASON WaitReason; }; struct SYSTEM_PROCESSES_INFO { ULONG NextEntryOffset; ULONG ThreadCount; ULONG Reserved[6]; LARGE_INTEGER CreateTime; LARGE_INTEGER UserTime; LARGE_INTEGER KernelTime; UNICODE_STRING ProcessName; KPRIORITY BasePriority; ULONG ProcessId; ULONG InheritedFromProcessId; ULONG HandleCount; ULONG Reserved2[2]; VM_COUNTERS VmCounters; IO_COUNTERS IoCounters; //windows 2000 only struct _SYSTEM_THREADS Threads[1]; }; -------------------------- KOD driver.c ---------------------------------------- SYSTEM_PROCESSES_INFO struktura je ona koju funkcija vraca. U toj strukturu zanimljivi su nam clanovi NextEntryOffset koji sadrzi broj bajtova do sljedece strukture u bufferu. Tu cinjenicu cemo iskoristiti tako da cemo na to mjesto staviti neki lazni broj bajtova da bi preskocili strukturu naseg procesa, buduci da svaka aplikacija koja cita taj buffer ce koristiti upravo taj clan da se krece po bufferu sekvencijalno. Slika je puno jasniija: Orginalni pokazivac |-------------- | \|/ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | | | | | Struct 1 | Struct 2 | Struct 3 | ... | | |(nas proc) | | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | /|\ |-------------------------- Promjenjeni pokazivac Dakle manipulacijom clana NextEntryOffset mi cemo prikriti nasu strukturu u bufferu. Pri tome je vazno napomenuti da nas proces ustvari ostaje pohranjen u bufferu, ali samo vise nece biti lagano doci do njega. Pri tome imamo dvije posebne situacije. Ako se nas proces nalazi na kraju buffera morati cemo promjeniti strukturu prije njega tako da ona pokazuje da je ona zadnja u bufferu. A, ukoliko je nas proces na pocetku buffera onda cemo morati vratiti promjenjenu adresu pocetka buffera koja ce pokazivati da buffer pocinje na drugoj strukturi i da je to prva struktura. Sljedeci clan koji ce nam biti zanimljiv je ProcessName. Jasno je sto taj clan sadrzi, samo je potrebno pripaziti prilikom usporedivanja buduci da je rijec o unicode stringovima u kojima svaki znak zauzima dva bajta. I na kraju treba pripaziti na clanove KernelTime i UserTime. Oni sadrzavaju vrijeme izvrsavanja procesa, prvi u kernel modu, drugi u user modu. To vrijeme cemo pribrojiti nekom drugom procesu kako bi bilo teze uociti da nesto nije uredu s listom procesa, najbolje je to pribrojiti "System Idle Processu". Nejga je lagano prepoznati buduci da je jedini proces koji nema ime. Sljedi kompletni kod koji bi trebao napraviti ovo o cemu sam sada pisao: -------------------------- KOD driver.c ---------------------------------------- NTSTATUS NewZwQueryFunc (IN ULONG SystemInformationClass, IN PVOID SystemInformation, IN ULONG SystemInformationLength, OUT PULONG ReturnLength) { NTSTATUS NtCallStatus; struct SYSTEM_PROCESSES_INFO *curr_proc, *prev_proc, *idle_proc; struct _SYSTEM_PROCESSOR_TIMES *times; prev_proc=NULL; /* Odmah pozovemo orginalnu funkciju, pod uvijetom da OldNtQueryFunc sadrzi njenu adresu */ NtCallStatus=((NTQUERYSYSTEMINFORMATION)(OldNtQueryFunc))(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength); /* Ako je funkcija uspjela */ if(NT_SUCCESS(NtCallStatus)) { if(SystemInformationClass==5) /* Prvi argument je 5, znaci trazi se lista procesa */ { curr_proc=(struct _SYSTEM_PROCESSES *)SystemInformation; /* Pocnemo od pocetka buffera */ while(curr_proc!=NULL) /* Redom po bufferu */ { if(curr_proc->ProcessName.Buffer!=NULL) /* Proces ima ime, nije Idle, moze biti nas */ { /* Usporedivanje imena, rijec je o unicode stringovima, dva bajta za svaki znak */ if((memcmp(curr_proc->ProcessName.Buffer, L"NasProc", 14))==0) /* Nas proces */ { Proc_UserTime.QuadPart+=curr_proc->UserTime.QuadPart; /* Spremi vrijeme naseg procesa */ Proc_KernelTime.QuadPart+=curr_proc->KernelTime.QuadPart; if(prev_proc!=0) /* Postoji proces prije nas u bufferu, sigurno nismo na pocetku */ { if(curr_proc->NextEntryDelta!=0) /* U sredini liste smo, ima netko poslje nas */ prev_proc->NextEntryDelta+=curr_proc->NextEntryDelta; /* Neka prvi proces preskoci nas */ else /* Zadnji smo u listi */ prev_proc->NextEntryDelta=0; /* Napravi prijasnji proces zadnjim */ } else { if(curr_proc->NextEntryDelta!=0) /* Prvi u listi smo */ { /* Promjeni pocetak buffera, neka pocne od sljedeceg procesa */ (char *)SystemInformation+=curr_proc->NextEntryDelta; } else /* Jedini proces u listi smo (ne znam kada bi se to moglo dogoditi :) */ { SystemInformation=NULL; /* Neka onda nema procesa */ } } } } else { /* Trenutno smo na procesu koji nema ime, to je Idle proces, spremi njegovu adresu */ idle_proc=curr_proc; } prev_proc=curr_proc; if(curr_proc->NextEntryDelta!=0) /* Ima jos procesa */ ((char *)curr_proc+=curr_proc->NextEntryDelta); /* Nastavi dalje po bufferu, s novim procesom */ else /* kraj buffer, petlja se prekida */ curr_proc=NULL; } } /* Dodaj nase vrijeme idle procesu */ idle_proc->UserTime.QuadPart+=Proc_UserTime.QuadPart; idle_proc->KernelTime.QuadPart+=Proc_KernelTime.QuadPart; Proc_UserTime.QuadPart=Proc_KernelTime.QuadPart=0; } return NtCallStatus; } Objasnjenja su u komentarima. //////////////////////////////////////////////////////////////////////////////// [> 4. <] DIRECT KERNEL OBJECT MANIPULATION \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ DKOM je akronim za "Direct Kernel Object Manipulation". Iz samoga naziva se zakljucuje da je rijec o mijenjaju nekih objekata u kernelu. Sto su to tocno kernel objekti? Kernel objekti bi bili sve one strukture podataka koje kernel kreira i mijenja tijekom rada racunala, a oslanja se na njih za upravljanje racunalom i resursima. Ova tehnika ide u jednom potpuno drugome smjeru od prethodno opisanoga hookinga. Temelji se na mijenjaju objekta koji kernelu sluzi za upravljanje procesima. Taj objekt cemo promjeniti tako da prikrijemo nas proces. Jednom kada smo to napravili nas kod vise ne mora raditi bilo sto. Nema vise funkcija koje se stalno pozivaju pa moramo napisati svoj kod koji ce glumiti orginalnu funkciju. Samo jedna mala (ali uistinu) mala promjena je sve sto ce nam trebati da sakrijemo svoj proces. I opet, nije problem napraviti kod koji ce promjeniti neki objekt u kernelu nego je problem napraviti kod koji ce pripremiti sve za tu promjenu i problem je kako tocno znati sto trebamo promjeniti, koji objekt konkretno. To su neka od osnovnih problema kod DKOM tehnike. Treba znati kako objekt izgleda u kernelu, koje sve podatke sadrzi, kako pronaci adresu u memoriji, kako kernel koristi taj objekt... To su samo neka od pitanja koja moraju biti odgovorena s apsolutnom sigurnoscu jer nema puno mjesta nagadanjima. ----- [ 4.1 EPROCESS objekt ] -------------------------------------------------- Svaki proces na Windows operativnom sustavu u kernelu je predstavljen s jednom EPROCESS strukturom. EPROCESS dolazi od executive process. Prilikom nastanka procesa struktura se alocira za novi proces a prilikom zavrsetka procesa njegova struktura se dealocira, tako da mozemo reci koliko struktura toliko procesa. Ta struktura sadrzi mnogo vaznih informacija koje opisuju jedan proces. Neke od tih informacija su: -------------------------- OUTPUT WinDbg --------------------------------------- nt!_EPROCESS +0x000 Pcb : _KPROCESS +0x06c ProcessLock : _EX_PUSH_LOCK +0x070 CreateTime : _LARGE_INTEGER +0x078 ExitTime : _LARGE_INTEGER +0x084 UniqueProcessId : Ptr32 Void +0x088 ActiveProcessLinks : _LIST_ENTRY +0x090 QuotaUsage : [3] Uint4B +0x09c QuotaPeak : [3] Uint4B +0x0a8 CommitCharge : Uint4B +0x0ac PeakVirtualSize : Uint4B +0x0b0 VirtualSize : Uint4B +0x18c LockedPagesList : Ptr32 Void +0x190 ThreadListHead : _LIST_ENTRY +0x198 SecurityPort : Ptr32 Void +0x19c PaeTop : Ptr32 Void +0x1a0 ActiveThreads : Uint4B -------------------------- OUTPUT WinDbg --------------------------------------- Ovo je dobiveno uporabom WinDbg-a. WinDbg je besplatan kernel debugger od microsofta i jako je koristan kod pisanja rootkita, ako za nista drugo onda barem za prikaz raznih struktura (kao sto je ovdje slucaj). S naredbom "dt _eprocess" WinDbg ce nam ispisati sadrzaj EPROCESS strukture. Stvarni output nebi smio izgledati ovako kako sam ga ja napisao jer sam ja uzeo samo neke stavke za primjer, ima ih jos mnogo. Veliki broj tih drugih stavki su zapravo podstrukture koje opet imaju svoje clanove itd. Ukratko, EPROCESS je prilicno velika i slozena struktura. A s obzirom da sadrzi informacije o processu jasno je zasto. Od svih tih stavki nama ce biti vazne samo dvije. To je: "UniqueProcessId" i druga je "ActiveProcessLinks". UniqueProcessId cemo koristiti da pronademo EPROCESS strukturu nasega procesa jer kao sto sam vec rekao ima ih mnogo. Za kretanje listom koristiti cemo ovu drugu stavku. ActiveProcessLinks je jedna struktura tipa _LIST_ENTRY koja izgleda ovako: -------------------------- OUTPUT WinDbg --------------------------------------- +0x088 ActiveProcessLinks : _LIST_ENTRY +0x000 Flink : Ptr32 _LIST_ENTRY +0x004 Blink : Ptr32 _LIST_ENTRY -------------------------- OUTPUT WinDbg --------------------------------------- Ona sadrzi dva pokazivaca. Flink pokazuje unutar sljedece EPROCESS strukture, a Blink pokazuje unutar prijasnje EPROCESS strukture. Dakle svi procesi na racunalu su povezani u jednu dvostruko vezanu listu. Konkretno, rijec je o kruzno dvostruko vezanoj listi. Slika predocava kako to izgleda: +-+-+-+-+-+ +-+-+-+-+-+ | | | | |EPROCESS |-----> |EPROCESS | | |<----- | | +-+-+-+-+-+ +-+-+-+-+-+ /|\ | | /|\ | | | | | | | | | | | | | \|/ \|/ | +-+-+-+-+-+ +-+-+-+-+-+ | | | | |EPROCESS |<----- |EPROCESS | | |-----> | | +-+-+-+-+-+ +-+-+-+-+-+ Ovakva organizacija procesa, kao kruzno dvostruko vezane, liste zapravo nam znatno olaksava posao. Ako uzmemo bilo koju EPROCESS strukturu mozemo doci do EPROCESS strukture naseg procesa bez obzira da li se krecemo na ljevu ili desnu stranu (Flink ili Blink), i lista nema kraja, pa ne moramo na to paziti prilikom mijenjanja EPROCESS strukture, sto ce se vidjeti kasnije u kodu. I na kraju opisa EPROCESS strukture jedna cinjenica koja ce nam otezati posao. Gore sam spomenio da Flin i Blink pokazuju unutar EPROCESS strukture. Po tipu podataka iz gore navedenog output WinDbg-a jasno je da pokazuju na ActiveProcessLinks druge EPROCESS strukture. Time su nam stvari otezane utoliko sto ne mozemo jednostavno skociti na sljedeci element u vezanoj listi. Tako da necemo koristiti klasican algoritam za kretanje kroz vezanu listu. Da bi dobili adresu sljedece EPROCESS strukture u nizu potrebno je od adrese zapisane u Flink oduzeti offset koji ima ActiveProcessLinks. Detaljno cu to pojasniti u kodu. Sada znamo kako izgleda EPROCESS struktura koju ce nas kod promjeniti. Znamo otprilike kako cemo se kretati po listi da bi pronasli nas process u njoj. Sljedeci korak je pronalazak bilo koje EPROCESS strukture u memoriji. Bilo koje, jer cim imamo jednu vise nam nece biti problem doci do ostalih. To je mislim i najjednostavniji korak buduci da nam kernel nudi jednu gotovu funkciju koja vraca pokazivac na EPROCESS strukturu trenutnog procesa (i u kernel modu postoji kontekst aktivnog procesa). Mi samo trebamo pozvati funkciju i spremiti vrijednost koju nam je vratila. Rijec je o funkciji PsGetCurrentProcess(). Funkcija ne prima niti jedan argument. ----- [ 4.2 Izmjena EPROCESS bloka ] ------------------------------------------ Kada jednom nademo eprocess strukturu koja pripada nasem procesu, trebamo je nekako sakriti. To znaci da tu eprocess strukturu moramo izbaciti iz liste. Ako nasa EPROCESS struktura nije u listi ciniti ce se i da nas process nigdje ne postoji. Najjednostavniji nacin da izbacimo EPROCESS strukturu iz liste je da ne mjenjamo nista u njoj vec da promjenimo Flink pokazivac prethodne EPROCESS tako da u njega spremimo adresu sljedece EPROCESS strukture (gledajuci s obzirom na nasu). I potom promjenimo Blink pokazivac sljedece EPROCESS strukture da sadrzi adresu prethodne EPROCESS strukture. Tim dvijema promjenama nasu EPROCESS strukturu cemo izbaciti iz liste i svaki kod koji cita listu i za kretanje po njoj koristi Flink i Blink nece je pronaci. Slika pokazuje kako cemo promjeniti EPROCESS strukture: |------------------- | | +-+-+-+-+-+ | +-+-+-+-+-+ | +-+-+-+-+-+ | | | | | |-----> | | |EPROCESS |------->XX |EPROCESS |----------> |EPROCESS | | |<--------- | (nasa) |XX<-------- | | +-+-+-+-+-+ +-+-+-+-+-+ | +-+-+-+-+-+ /|\ | --------------------------| No susjedni elementi nisu jedini koje treba promjeniti. Ako nas proces zavrsi s radom, njegova struktura ce se dealocirati i sve ce biti uredu. Ali sto ce se dogoditi ako neki od susjeda naseg procesa zavrse s radom. Flink i Blink pokazivaci ce onda pokazivati na neodredeni dio memorije, sto bi moglo uzrokovati nepredvidene probleme. Tako da je najjednostavnije Flink i Blink pokazivace nasega procesa postaviti da pokazuju sami na sebe. Nakon toga skrivanje procesa je gotovo. Nas drajver vise ne mora raditi bilo sto. Jednom kada napravi ovu promjenu, proces je skriven skroz dok je aktivan. To je ujedno i jedna mana ovakvoga nacina skrivanja, ukoliko ponovo pokrenemo proces on ce dobiti novu EPROCESS strukturu koju treba ponovo sakriti. Koraci koje treba poduzeti za skrivanje procesa ovom tehnikom su: 1. Pronaci adresu bilo koje EPROCESS strukture u memoriji. 2. Redom se kretati po listi dok ne naidemo na EPROCESS strukturu trazenoga procesa. 3. Promjeniti Flink prijasnje i Blink sljedece strukture kako bi prikrili nasu. 4. Promjeniti Flink i Blink nase EPROCESS strukture da pokazuje sama na sebe. -------------------------- KOD driver.c ---------------------------------------- #define PID_TO_HIDE 123 #define PID_OFFSET 0x84 #define FLINK_OFFSET 0x88 DWORD EprocAddr; int CurrPID=0, StartPID=0, IterCount=0; PLIST_ENTRY PtrActiveProcess; EprocAddr=(DWORD)PsGetCurrentProcess(); StartPID= *((int *)(EprocAddr+PID_OFFSET)); CurrPID=StartPID; while(1) { if(PID_TO_HIDE==CurrPID) break; if((IterCount>=1) && (StartPID==CurrPID) break; PtrActiveProcess=(PLIST_ENTRY)(EprocAdr+FLINK_OFFSET); EprocAddr=(DWORD)PtrActiveProcess->Flink; EprocAddr=EprocAddr-FLINK_OFFSET; CurrPID = *((int *)(EprocAddr+PID_OFFSET)); IterCount++; } -------------------------- KOD driver.c ---------------------------------------- Kada se ovaj kod izvrsi u varijabli EprocAddr bi trebala biti spremljena adresa EPROCESS strukture od procesa s pid-om PID_TO_HIDE. Prvi #define dakle deklarira pid procesa koji trazimo. Sljedeca dva #define-a su vrlo vazna i ovise o verziji Windowsa. Naime, mi nemamo strukturu pomocu koje bi mogli napraviti type cast adrese pa onda jednostavno dohvacati clanove strukture. Mi imamo samo pocetnu adresu neke EPROCESS strukture. Ako zelimo dohvatiti bilo koji njezin clan mi cemo na tu pocetnu adresu dodati broj bajtova (offset) do toga clana i dobiti cemo njegovu adresu u memoriji. Taj offset od pocetka EPROCESS strukture do nekog njezina clana se mijenja izmedu razlicitih verzija Windowsa, pa cak i verzija servicea packova. To nam ujedno i predstavlja problem ukoliko mislimo napisati rootkit koji bi trebao raditi na windows 2000, xp i 2k3. Offset mozemo jednostavno saznati pomocu WinDbg-a. Vec sam prije napisao primjer outputa iz WindDbg-a. Ona prva brojka ispred koje je znak + predstavlja offset. +0x084 UniqueProcessId : Ptr32 Void Dakle offset do UniqueProcessId je 0x84, tako koristim i u kodu. Isto vrijedi i za FLINK_OFFSET, to nam je offset do Flink pokazivaca koji nam treba za kretanje po listi. Ova dva offseta su za Windows XP Professional SP2. Nemam ostale verzije windows OS-a da se sam uvjerim kolike su te vrijednsoti, ali trebale bi biti ovako: Windows NT: PID OFFSET : 0x94 FLINK OFFSET : 0x98 Windows 2000 PID OFFSET : 0x9C FLINK OFFSET : 0xA0 Windows XP (i SP2) PID OFFSET : 0x84 FLINK OFFSET : 0x88 Windows 2003 PID OFFSET : 0x84 FLINK OFFSET : 0x88 Zanimljivo je da su izmedu XP-a i 2k3 offseti isti, no bez obzira na to treba se uvijek uvjeriti koji je tocan offset, jer i jedan bajt znaci puno. Bilo bi dobor da vas drajver zna na kojoj verziji operativnog sustava radi, pa da u skladu s time moze primjenjivati razlicite offsete. Jednostavan nacin na koji se to moze utvrditi je uporabom jedne kernel funkcije. To je PsGetVersion(). Ima 5 argumenata. U novijim windowsima (xp i 2k3) postoji i nova funkcija za dobivanje takvog info-a. To je RtlGetVersion(). Prima samo pokazivac na jednu strukturu tipa RTL_OSVERSIONINFOW (info se uvijek moze naci na msdn-u koji ukljucuje i dokumentaciju za razvoj kernel koda). koju onda "napuni" informacijama. Natrag na kod. Poslje #define-ova sljede deklaracije varijabli. PLIST_ENTRY je deklariran u ntddk.h kao pokazivac na strukturu _LIST_ENTRY u kojoj su spremljeni Flink i Blink, pa cemo pomocu njega moci vrlo jednostavno napraviti type cast i dohvatiti Flink i Blink. Sljedece koristimo PsGetCurrentProcess() da dohvatimo adresu EPROCESS strukture trenutnog processa i spremamo je u EprocAddr. I onda radimo ono sto sam malo prije objasnjavao s offsetima. Na EprocAddr dodamo PID_OFFSET time smo sada dobili adresu od PID-a, nju dereferenciramo i dobivamo PID od trenutne EPROCESS strukture. U while petlji zapravo se krecemo po listi i provjeravamo za svaki PID da li je jednak onom trazenom. Takoder provjeravamo da li smo vec jednom napravili krug po listi. To ce se dogoditi onda ako smo ponovo na istom PID-u a brojac skokova po listi je veci od jedan. Ako se to dogodi znaci da nismo nasli PID koji trebamo sakriti, pa bi kod trebao na to adekvatno reagirati (ovdje samo prekida petlju). Sto se tice samog skoka na sljedecu EPROCESS strukturu u listi on se izvodi u ove tri linije koda: -------------------------- KOD driver.c ---------------------------------------- PtrActiveProcess=(PLIST_ENTRY)(EprocAdr+FLINK_OFFSET); EprocAddr=(DWORD)PtrActiveProcess->Flink; EprocAddr=EprocAddr-FLINK_OFFSET; -------------------------- KOD driver.c ---------------------------------------- Prvo iz trenutne EPROCESS strukture izvucemo adresu Flink-a i pomocu PLIST_ENTRY napravimo type cast. Zatim iz toga dohvatimo vrijednost Flink-a i spremimo u EprocAddr. To sada znaci da EprocAddr ima adresu Flink-a (rekao sam da Flink ne pokazuje direktno na sljedecu EPROCESS strukturu vec na njezin Flink) sljedece EPROCESS strukture. Ako od toga oduzmemo FLINK_OFFSET dobiti cemo bas pocetnu adresu sljedece EPROCESS strukture. -------------------------- KOD driver.c ---------------------------------------- PtrActiveProcess=(PLIST_ENTRY)(EprocAddr+FLINK_OFFSET); *((DWORD *)PtrActiveProcess->Blink)=(DWORD)PtrActiveProcess->Flink; *((DWORD *)PtrActiveProcess->Flink+1)=(DWORD)PtrActiveProcess->Blink; PtrActiveProcess->Flink=(PLIST_ENTRY) &(PtrActiveProcess->Flink); PtrActiveProcess->Blink=(PLIST_ENTRY) &(PtrActiveProcess->Flink); -------------------------- KOD driver.c ---------------------------------------- Ovaj kod je sakrio nas proces. Na prvi pogled je tesko razumljiv. Druga linija mjenja Flink pokazivac prethodnog procesa tako da on pokazuje na sljedeci proces (govorim iz gledista nase EPROCESS strukture).U Flink prethodnog stavlja vrijednost naseg Flinka, a do prethodnog Flinka dode preko naseg Blinka. Treca linija radi obrnuto. Mijenja Blink sljedeceg procesa da pokazuje na prethodni. U Blink sljedeceg stavi vrijednost naseg Blinka, a do Blinka sljedeceg dode preko naseg Blinka. Zadnje dvije linije osiguravaju da vlastiti Flink i Blink pokazuju sami na sebe kako bi izbjegli probleme s dealokacijom nasih susjednih procesa. ----- [ 4.3 Trazenje procesa po imenu ] ---------------------------------------- Sve je uredu s ovim kodom, osim sto moramo znati PID procesa kojeg zelimo sakriti. Vjerojatno nikada necete pogoditi PID vaseg procesa u buducnosti tako da ne mozete jednostavno staviti tu vrijednsot da bude hardcoded u drajveru. Drugo rjesenje bi bilo da napravite komunikaciju izmedu procesa i drajvera koji ga skriva pa da onda proces moze drajveru javiti svoj pid. Ta komunikacija je zapravo jako dobro rjesenje za vece rootkitove jer puno toga se moze izmjenjivati izmedu drajvera i procesa. Takva komunikacija bi se odvijala preko IOCTL-ova i IRP-ova. No tu je potrebno dosta koda i u drajveru i u procesu kako bi sve tetklo glatko. Ovdje cu samo pokazati kako se procese moze traziti i po njihovom imenu. Ime kao i niz drugih informacija spremljeni su u EPROCESS strukturi. Ovaj kod bi trebao pronaci offset koji onda mozemo koristiti za pronalazak naseg procesa po imenu. -------------------------- KOD driver.c ---------------------------------------- ULONG NameOffset; PEPROCESS CurrProc=PsGetCurrentProcess(); for(NameOffset=0; NameOffset 5. <] KRAJ \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ Zahvaljujem se svima koji su procitali ovaj tekst bez obzira sto mislili o njemu. Pozdravljam sve one koje poznajem, s kojima sam u kontaktu ili sam bio: hess, nimrod, MilkFairy, Shatterhand, h4z4rd, h44rp, ]seth[ ...