Uvod patchovani systemovych volani v Linuxu (LKM)

08.04.2003 21:12

V tomto clanku se pokusim vysvetlit principy patchovani systemovych volanii a nazorne je predvest na nekolika jednoduchych prikladech. Na tomto principu pracuji LKM rootkity jako Adore, Knark a spousta dalsich. Clanek
je urcen zacatecnikum a jeho ucelem ukazat, ze na tom neni vlastne nic sloziteho...

Co je to syscall?
Procesy bezici v uzivatelskem (neprivilegovanem) prostoru nemaji pravo komunikovat primo s hardwarem, maji povoleno vykonavat omezenou instrukcni sadu a v pameti maji pristup jen tam, kam jim jadro dovoli. Proto, pokud chce proces dejme tomu pristupovat k nejakemu zarizeni, musi rict jadru,co chce provest, a jadro to provede za nej. K tomu slouzi systemova volani (system calls nebo jen syscalls). Chceme treba vypsat obsah adresare pomoci ls. Jaka systemova volani ls vola?

$ strace ls

Na stderr se nam vypise spousta systemovych volani, ktere program pouziva. Muzete si to prohlednout podrobne, ale nas v tuhle chvili zajima, ze k zjisteni obsahu adresare vola getdents64. To znamena, ze pokud bychom chteli napr. skryt soubory, musime patchnout toto systemove volani tak, aby nase soubory ignorovalo. Kompletni seznam systemovych volani najdete v /usr/include/bits/syscall.h.

Jak muzu zmenit kod systemoveho volani?
Systemova volani jsou soucasti jadra, takze musime napr. napsat modul kernelu (LKM - loadable kernel module), abychom meli pravo je prepsat. Existuji samozrejmei jine zpusoby,ale ty jsou prece jenom \"o neco\" obtiznejsi;) V kernelu je uchovavana tabulka s adresami jednotlivych syscallu (void *sys_call_table[]). Je exportovana spolu s dalsimi symboly v kernel symbol table (cat /proc/ksyms). Chceme-li zmenit systemove volani, staci si napsat svoji funkci a jeji adresu ulozit do sys_call_table na misto puvodniho syscallu.

LKM ?
LKM je kus kodu, ktery se loadne do jadra (man insmod, man modprobe) a jako kod jadra ma vsechna privilegia. Proto pri psani modulu musime byt opatrni, protoze muzete se systemem udelat moc skarede veci ;) (me se to podarilo uz nekolikrat, nastesti nic trvaleho ;)) Moduly se pouzivaji hlavne jako ovladace zarizeni. Ovladace je mozne zkompilovat primo do kernelu, ale velkou vyhodou modulu je, ze nemusime pokazde, kdyz chceme pridat nejakou funkci (napr. ten ovladac), kompilovat cele jadro. Staci za behu loadnout modula mame po starostech. Takze jak se takovy modul pise? Nejlepsi bude, kdyz si to vysvetlime
na priklade:

#define __KERNEL__
#define MODULE

#include
#include

/* provede se pri inicializaci modulu */
int init_module() {
printk(KERN_DEBUG \"Module loaded..n\");
return 0;
}

/* provede se pri ukonceni modulu */
void cleanup_module() {
printk(KERN_DEBUG \"Module unloaded..n\");
}

Ze zacatku definujeme dve makra. __KERNEL__ musi byt definovano pro kod jadra a makro MODULE musi byt definovano jako kod, ktery se preklada jako modul jadra. Hlavickovy soubor linux/module.h je nutny pro moduly jadra a soubor linux/kernel.h definuje nektere casto pouzivane funkce, v tomto pripade funkci printk. printk se pouziva jako printf, neumoznuje vsak tisk cisel s pohyblivou carkou. KERN_DEBUG urcuje prioritu zpravy. Je to totez,jako bychom napsali printk(\"<7>Module...atdn\"). Tato makra jsou definovana v linux/kernel.h.

Dale tu jsou dve funkce. Funkce init_module se vola pri loadnuti modulu do jadra a cleanup_module (prekvapive;) pri odstraneni modulu z jadra.

Modul zkompilujeme pomoci

$ cc -Wall -O2 -c modul.c

-Wall zapne vsechny warning hlasky, moduly jadra podle by podle nekterych zdroju mely byt kompilovany se zapnoutou optimalizaci alespon O2 (no.. me jely i bez optimalizace, ale co, -O2 nicemu neuskodi), -c vypne linkovani souboru. To probiha az pri loadovani do jadra. Po kompilaci si modul muzeme vyzkouset:

$ insmod modul.o ; lsmod; dmesg| tail -n 1; rmmod modul; dmesg | tail -n1

Patchovani syscallu
Vysvetlovat budu opet az po malem prikladku..

#define __KERNEL__
#define MODULE

#include

#include
#include

#include

#define MAGIC_SIG 69

extern void *sys_call_table[];

int (*orig_kill) (pid_t, int);

/* nase nove systemove volani */
int new_kill(pid_t pid, int sig) {
int ret=0;
if (sig!=MAGIC_SIG)
/* provede puvodni kill */
ret=(*orig_kill)(pid,sig);
else {
/* nas kod pri zadani kouzelneho signalu ;) */
printk(\"<7>signal 69 rulzzz...n\");
}
return ret;
}

int init_module() {
/* ulozi puvodni kill */
orig_kill=sys_call_table[__NR_kill];

/* nahradi adresu syscallu v tabulce systemovych volani
* adresou nasi funkce */
sys_call_table[__NR_kill]=new_kill;

return 0;
}

void cleanup_module() {
/* vrati zpet puvodni kill */
sys_call_table[__NR_kill]=orig_kill;
}

Mame pristup ke vsem symbolum exportovanym pres kernel symbol table (muzeme take libovolny symbol - funkce, struktury, promenne - prepsat). Pristup k sys_call_table nam zajistuje prave radek

extern void *sys_call_table[];

Pak nasleduje ukazatel funkci puvodniho systemoveho volani.

int (*orig_kill) (pid_t, int);

Umisteni zdrojovych souboru nekterych systemovych volani najdete zde .

Dale vidime funkci new_kill, ktera, jak asi uz vsichni tusite, je nase nove systemove volani. Zkontroluje, zdaneni cislo signalu nas tajny signal, pokud je, provede nas kod.. pokud neni, provede puvodni systemove volani a vrati jeho navratovy kod.
Ve funkci init_module nahrazujeme zaznam v sys_call_table[]. Misto __NR_kill muzeme pouzit i SYS_kill (viz. /usr/include/bits/syscall.h). No a nakonec musime pri odstraneni modulu dat puvodni systemove volani do poradku.. Tady bych rad upozornil na jednu vec. Nesnazte se loadnout vic modulu, patchujuci stejne systemove volani, najednou (resp. jeden modul vicekrat. create_module (systemove volani pouzivane pro loadovani modulu) vam to sice nedovoli, ale pokud bude jeden skryty, tak o nem nemusi vedet). Predstavte si, ze loadnete modul, ten si ulozi originalni system call a po unloadnuti se zase vrati zpet. Zadny problem. Ale co kdyz loadnete modul v
dobe, kdy uz jeden modul patchujici stejne volani loadnuty je? Ulozi si jako originalni systemove volani to, ktere je momentalne v sys_call_table, tzn. to patchnute. Pak unloadneme prvni modul, syscall se vrati do puvodniho stavu. Je vam snad jasne co se stane, kdyz unloadneme druhy modul. Jako originalni system call ma ulozenou adresu patchnute verze, jenomze funkce, ktera se na tehle adrese nachazela je z kernelu spolu s modulem unloadnuta. Takze misto aby vratil syscall do puvodniho stavu, tak ho nastavi na adresu, kde se nachazelo nase patchnute systemove volani, ktere tam uz samozrejme neni, takze pokud se jakykoli proces pokusi zavolat dane systemove volani, vypise se nam pekna chybova hlaska (Segmentation fault) :) To je taky duvod, proc je nutne pri odpojeni modulu (v cleanup_module) dat systemove volani do poradku.

Timto zpusobem patchovani menime adresu funkce systemoveho volani v sys_call_table. Existuji utilitky (napr. kstat ), ktere tenhle zasah do systemu dokazi hrave rozpoznat. Pokud byste tedy chteli psat rootkit nebo neco podobneho, pocitejte s tim, ze pokud budete patchovat volani timto zpusobem, nebude vas modul nikdy neviditelny. Zajimavy (osobne neovereny) zpusob jak to obejit, popisuje SpaceWalker ve svem clanku (link na konci clanku). Muzeme si vytvorit vlastni sys_call_table, do ktere zkopirujeme tabulku puvodni. Vsechny zmeny systemovych volani provadime v nasi sys_call_table. Samozrejme potom musime prepsat adresu sys_call_table na adresu nasi tabulky. S tim se vaze jeden bug a to ten, ze pokud nekdo loadne modul patchujici systemove volani, zjisti, ze jeho modul na syscall nema zadny vliv, takze je necov neporadku. Proto bysme jeste museli kazdemu nove pripojenemu modulu
vnutit nasi sys_call_table.

Skryti modulu

Zamereni tohoto clanku nemi skryvani modulu, takze jen strucne popisu nektere zpusoby a odkazu zajemce o podrobnejsi popis na prislusne zdroje. Zavedene moduly zjistime pomoci lsmod, z vypisu ksyms, vypsanim /proc/modules nebo treba pomoci kstat. Existuje nekolik cest, jak ucinit nas modul temer neviditelny. Musime zajistit, aby modul nebyl videt ve vypisu ksyms (resp. cat /proc/ksyms) ani lsmod. Muzeme patchnout query_module, ale to samotne nestaci, lze to snadno obejit (napr. cat /proc/modules ani cat /proc/ksyms nepouzivaji systemove volani query_module). Museli bychom patchnout jeste read(), pro pripad, kdy cteme z /proc/modules resp. /proc/ksyms. To vsak porad neni stoprocentni. Modul porad vypiseme pomoci dd if=/proc/modules bs=1.Vyhodou tohoto zpusobu vsak je, ze muzeme modul bez problemu unloadnout.

Takovy modul snadno objevi kstat. Ten cte primo /dev/kmem, kde hleda mimo jine i pripojene moduly. Jak tuhle utilitku prelstit opet popisuje SpaceWalker v jeho clanku. V podstate pise, ze bysme mohli modul unloadnout, ale pritom ho \"zapomenout\" odstranit z pameti ;) Tuhle akci nemuzeme provest primo pri loadovani modulu (v init_module()), protoze v te dobe modul jeste neni zaregistrovan. Musime proto patchnout nektere systemove volani tak, aby v urcitem (nami definovanem) pripade provedl nase falesne odpojeni modulu. Podrobnosti nebudu zbytecneopisovat, jeho clanek najdete dole v odkazech.

Abyste nerekli ze tu neni zadny kod, tak popisu sice mene dumyslny (kstat modul samozrejme obevi), ale o to jednodussi zpusob. Do zacatku nam to bude stacit;) Muzeme napsat modul, ktery pri loadnuti skryje nas modul a tim jeho mise zkonci, takze jej zas unloadneme. Tohoto zpusobu vyuziva napr. rootkit Adore. Pred vysvetlenim jeste trochu teorie:
Seznam zavedenych modulu je jednosmerny kruhovy seznam s prvky typu
struct module, ktery je definovan v hlavickovem souboru linux/module.h.

struct module
{
unsigned long size_of_struct; /* == sizeof(module) */
struct module *next;
const char *name;
unsigned long size;

....
atd.

Pro nas je v tuhle chvili dulezity radek struct module *next, coz je ukazatel na dalsi modul. K aktualnimu modulu mame pristup pres strukturu typu module s nazvem __this_module. To nam staci vedet k tomu, aby se nam povedlo napsat hider. V nasem pripade postaci, kdyz se skryje posledni loadnuty modul pred loadnutim hideru. To provedeme ve funkci init_module:

int init_module(void)
{
if (__this_module.next)
__this_module.next = __this_module.next->next;

return 0;
}

Po loadnuti modulu bude tedy nasledujici modul vynechan, protoze aktualni modul ukazuje az na ten za nim. Potom v klidu hider unloadneme. Skryty modul neni videt v lsmod, v ksyms o nem neni ani zminka. Skryty modul taky nejde unloadnout, protoze kernel o nem nevi. Nevyhodou tohoto reseni je, ze potrebujeme o modul vic. Dalsi a mnohem jednodussi moznosti je provest neco podobneho primo v modulu, ktery chceme skryt. Do init_module pridame:

int init_module() {

...

__this_module = *__this_module.next;
MOD_INC_USE_COUNT;
}

Tenhle zpusob skryvani jsem nikde jinde pouzivat nevidel. Proto si nejsem jisty, jestli to nedela nejake osklive veci, ale na zadne jsem zatim neprisel. Takto skryty modul nejde (alespon pomoci rmmod) unloadnout, nejde videt v lsmod, ksyms, neni v /proc/modules, takze finty s dd vam taky nepomuzou. Kstat vsak modul objevi..

Nejaky konkretni priklad?
Pro zvedaveho roota bych tu mel modul pro logovani cinnosti uzivatelu. To se muze hodit napr. pokud provozujete honeypot a chcete logovat dejme tomu vsechny pokusy o cteni libovolneho souboru v adresari /etc/. Snadno se
da rozsirit o dalsi funkce (to vam necham za domaci ukol;)...treba o logovani cteni souboru, zapis do souboru,vytvareni nebo prejmenovani souboru atd..)). I kdyz user sejme syslog, porad o jeho cinnosti budete vedet vic,nez mu bude mile. Je to velmi jednoduchy modul. Pouze zmeni systemove volani open tak, ze pokud uzivatel volajici tento syscall patri do urcite skupiny a otevirany soubor je v urcitem adresari, pak se tato akce zapise. Potom se provede puvodni volani open. GID a adresar muzete definovat bud primo ve zdrojaku neb jako parametr pri loadovani modulu (viz man insmod, man modinfo). Program si muzete stahnout zde . Program jsem zacal psat jako priklad ke clanku, takze je jeste v hodne rozpracovane podobe (v podstate nic neumi;). Na druhou stranu je aspon jednoduchy, takze dobre poslouzi jako priklad, muzete jej sami vylepsovat a tim se spoustu veci naucit..

Pro neposedneho usera mam jednoduchy backdoor. Dejme tomu, ze se mu povedlo ziskat rootovska prava a chce si je nejak pojistit. Ma spoustu moznosti, jedna z nich je, ze patchne nektere ze systemovych volani tak, ze pri predani urcite hodnoty, nastavi modul aktualnimu nebo zadanemu procesu uid=gid=euid=egid=0.Tento modul patchuje kill. Kill ma 2 vstupni parametry. PID a cislo signalu. Kill je patchnuty tak, ze pokud bude cislo signalu 69, nastavi rootovske prava procesu se zadanym PID. Po loadnuti modulu neni nic jednodussiho, nez napsat program, ktery posle signal 69 sam sobe a pote spusti shell ;) (ono by stacilo i obycejne $ kill -69 PID). Modul i program pro root shell roste tady .

Posledni priklad je o neco malo slozitejsi nez predchozi dva. Predstavte si situaci, ze potrebujete nebo proste chcete pouzivat moduly (tzn. mit jejich podporu v jadre), ale chcete se nejak ochranit pred zavadeni LKM rootkitu a podobnych zakernosti. Reseni se primo nabizi: patchnout create_module tak, aby vracelo chybu, takze uz nebude mozne loadnout zadny modul. Nemoznost odpojeni modulu zaridime pomoci MOD_INC_USE_COUNT, coz znamena, ze se nas modul bude tvarit, ze je pouzivan, tudiz ho kernel odmitne odstranit. Aby nebyl modul tak jednoduchy, pridal jsem jeste moznost zadavat soubory, ktere bude modul chranit pred oteviranim (pro cteni i zapis - patchnutim open) a mazanim (patchnutim unlink). Nazvy souboru jsou ulozeny v jednosmernem linearnim seznamu, takze se dynamicky podle potreby zmensuje a zvetsuje. Pro komunikaci s modulem jsem patchnul lchown. Pomoci tohoto systemoveho volani muzeme zakazat nebo povolit moznost unloadnuti modulu a pridavat a odstranovatchranene soubory ze seznamu. Jakou akci chcete provest definujete pomoci UID, ktere si opet nastavte pri kompilaci modulu (nejake vysoke, ktere se ve vasem systemu nevyskytuje). Pridal jsem jednoduchy programek, pomoci ktereho uzivatel komunikuje s modulem. Zapinani a vypinani moznosti unloadnout modul je chraneno heslem, ktere si nastavite ve zdrojaku modulu pred kompilaci. Rad bych sem pastnul zdrojaky a podrobneji je popsal, ale zdrojak, komentare (v komentarich se nerad rozkecavam..;) a pouzijte na par minut svuj mozek.. Zdrojaky se urcite povalujou nekde tady . Na podobne veci nejspis existujou profesionalni programy, bezpecnostni patche a ja nevim co jeste, ale opet to berte jen jako priklad.. rozhodne nerikam, ze to bude nejvhodnejsi pro zabezpecovani serveru ;)

Zde je vyse
popsany modul (ehm.. par radku kodu), ktery skryva posledni loadnuty modul.

Dalsi features?

Vsechno podstatne, co se v systemu deje, je reseno pres systemova volani a my
mame pristup ke kazdemu z nich. Muzeme kterekoli prepsat. Toho se da nalezite
zneuzit a taky ze se toho zneuziva. Jak uz jsem psal v uvodu, existuje
spousta rootkitu, ktere pracuji prave na principu patchovani systemovych
volani pomoci LKM. Dokazi skryvat soubory a procesy (patchnuti getdents
popr. getdents64), sitova spojeni (patchnuti read pro pripad cteni
/proc/net/tcp (resp. udp)), presmerovat spustitelne soubory (execve) a
spoustu dalsich veci.. Diky nim je mozne vytvorit neviditelny backdoor,
nezmenite pritom zadnou binarku, kontrolni soucty souhlasi...
Samozrejme pritom predpokladame, ze je jadro zkompilovano s podporou LKM.
Pokud neni, LKM nam jsou samozrejme na nic. V tom pripade se musime uchylit
napr. k jiz zminovanemu zpusobu -- patchovani primo pres /dev/kmem.
Vic informaci a odkazy najdete tady
nebo tady.

Tolik k uvodu.. :) Pokud bude zajem (a hlavne cas a chut), napisu dalsi clanek/clanky zabyvajici se touto problematikou. Nevim ale, zda to ma smysl, protoze existuje vynikajici a velmi rozsahly guide od pragmatica z THC.Odkaz najdete o kousek niz.

Vsechny priklady byly testovany na Slackware 9.0 (glibc 2.3.1, gcc 3.2.2).

Feedback:
IRCnet, kanal #dump, #blackhole.sk (_2c_)

xhysek02@stud.fit.vutbr.cz

Ctenarsky denik:
- Indetectable Linux Kernel Modules, by SpaceWalker
- (nearly) Complete Linux Loadable Kernel Modules by pragmatic/THC
- list of Linux system calls
- Backdoor and Linux LKM Rootkit
- Linux Device Drivers, 2nd edition
- Linux Documentation Project
- Linux on-the-fly kernel patching without LKM by sd
- Patchovani kernelu pres /dev/kmem, sd
- Zdrojaky ruznych LKM rootkitu
- Zdrojaky kernelu
- Linux: zaciname programovat, Computer press (Wrox press)
2k