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