Copy Link
Add to Bookmark
Report
7x07 Hiding_Processes_Using_Windows_Drivers
...................
...::: 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/<ime_drajvera>.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 izvravanja koda unutar kernela.
Cilj te tehnike je osiguranje izvravanja nekoga naega koda svaki puta kada
neki program zatrai popis svih procesa koji se trenutno izvravaju na raèunalu.
Taj na kod bi se trebao izvriti 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<PAGE_SIZE; NameOffset++)
if( !strncmp( "System", (PCHAR)CurrentProc + NameOffset, strlen("System")))
break;
-------------------------- KOD driver.c ----------------------------------------
Kada se drajver ucitava u kernel preko SCM-a DriverEntry funkciju ce uvijek
pozvati System proces. Ovaj kod potom odmah dohvaca adresu njegove EPROCESS
strukture i skenira ju od pocetka, kada nade na string System vraca pocetak i
taj pocetak potom predstavlja offset u EPROCESS strukturi koji je potreban da
bi se doslo do imena procesa. Pri tome treba pripaziti da u EPROCESS strukturi
ime procesa predstavljeno je s 16 bajtova i tih 16 bajtova su obicno pocetni
znakovi fajla iz kojega je proces pokrenut.
Tehnika je s sysinternalsa.
Ili jos jednostavnije uz pomoc WinDbg-a napravite dt _eprocess i pronadete stavku
"ImageFileName" i pogledate njezin offset.
////////////////////////////////////////////////////////////////////////////////
[> 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[ ...