Uvod do patchovani systemovych volani v Linuxu pres LKM
V tomto clanku se pokusim vysvetlit principy patchovani systemovych volani
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 samozrejme
i 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 modul
a mame po starostech.
Takze jak se takovy modul pise? Nejlepsi bude, kdyz si to vysvetlime
na priklade:
#define __KERNEL__
#define MODULE
#include <linux/module.h>
#include <linux/kernel.h>
/* 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...atd\n"). 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 <sys/types.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <sys/syscall.h>
#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, zda neni 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 neco v 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 zbytecne opisovat, 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 odstranovat
chranene 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
i tak je clanek uz celkem dlouhy, takze si misto toho radeji prectete
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 to 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).
by 2k
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)