Apeluri de sistem. Linux - syscalls. Apeluri de sistem în modulul kernel încărcabil Linux

Mult - a spus Morsa - este timpul să vorbim.
L. Carroll (Citat din cartea lui B. Stroustrap)

În loc de introducere.

În ceea ce privește structura internă a nucleului Linux, în general, diferitele sale subsisteme și apeluri de sistem în special, a fost deja scris și rescris în ordine. Probabil, fiecare autor care se respectă ar trebui să scrie despre acest lucru cel puțin o dată, la fel ca fiecare programator care se respectă trebuie să-și scrie propriul manager de fișiere :) Deși nu sunt un scriitor IT profesionist și, în general, îmi notez doar pentru prima dată dintre toate, pentru a nu uita ceea ce ai învățat prea repede. Dar, dacă notițele mele de călătorie sunt cu adevărat utile cuiva, desigur, mă voi bucura doar. Ei bine, în general, nu poți strica terciul cu unt, așa că poate chiar eu voi putea scrie sau descrie ceva despre care nimeni nu s-a deranjat să menționeze.

Teorie. Ce sunt apelurile de sistem?

Când explică celor neinițiați ce este software-ul (sau sistemul de operare), spun de obicei următoarele: computerul în sine este o bucată de hardware, dar software-ul este ceea ce face posibilă obținerea unor beneficii din această bucată de hardware. Brut, desigur, dar per total, oarecum adevărat. Aș spune probabil același lucru despre sistemul de operare și apelurile de sistem. De fapt, în diferite sisteme de operare, apelurile de sistem pot fi implementate în moduri diferite, numărul acestor aceleași apeluri poate diferi, dar într-un fel sau altul, într-o formă sau alta, există un mecanism de apel de sistem în orice sistem de operare. În fiecare zi, un utilizator lucrează explicit sau implicit cu fișiere. Desigur, poate deschide în mod explicit fișierul pentru editare în MS Word „e sau Notepad” e preferat sau poate lansa pur și simplu o jucărie, a cărei imagine executabilă este, de altfel, stocată și într-un fișier, care, la rândul său, trebuie să deschidă și să citească fișierele executabile ale încărcătorului. La rândul său, jucăria poate deschide și citi zeci de fișiere pe parcursul activității sale. Firește, fișierele pot fi citite nu numai, ci și scrise (nu întotdeauna, adevărat, dar aici nu vorbim despre separarea drepturilor și accesul discret :)). Nucleul gestionează toate acestea (în sistemele de operare microkernel, situația poate fi diferită, dar acum ne vom apleca discret către obiectul discuției noastre - Linux, așa că vom ignora acest punct). Apariția unui nou proces în sine este, de asemenea, un serviciu furnizat de kernel-ul OS. Toate acestea sunt minunate, precum și faptul că procesoarele moderne funcționează la frecvențe ale gamelor de gigaherți și constau din multe milioane de tranzistoare, dar ce urmează? Da, ce se întâmplă dacă nu ar exista un mecanism prin care aplicațiile utilizatorului ar putea efectua lucruri destul de banale și, în același timp, necesare ( de fapt, în orice caz, aceste acțiuni banale sunt efectuate nu de aplicația utilizator, ci de kernel-ul OS - ed.), atunci sistemul de operare era doar un lucru în sine - absolut inutil, sau, dimpotrivă, fiecare aplicație de utilizator în sine ar trebui să devină un sistem de operare pentru a-și satisface în mod independent toate nevoile. Frumos, nu-i așa?

Astfel, am ajuns la definiția unui apel de sistem în prima aproximare: un apel de sistem este un fel de serviciu pe care kernel-ul OS îl oferă unei aplicații de utilizator la cererea acestuia din urmă. Un astfel de serviciu poate fi deschiderea deja menționată a unui fișier, crearea acestuia, citirea, scrierea, crearea unui nou proces, obținerea identificatorului de proces (pid), montarea sistemului de fișiere, închiderea sistemului și, în cele din urmă. În viața reală, există mult mai multe apeluri de sistem decât sunt enumerate aici.

Cum arată și cum este un apel de sistem? Ei bine, din cele spuse mai sus, devine clar că un apel de sistem este un subrutină de nucleu care are un aspect corespunzător. Cei care au avut experiență cu programarea Win9x / DOS își vor aminti probabil întreruperea int 0x21 cu toate (sau cel puțin unele) din numeroasele sale funcții. Cu toate acestea, există o mică ciudățenie care afectează toate apelurile de sistem Unix. Prin convenție, funcția care implementează apelul de sistem poate lua N argumente sau deloc, dar într-un fel sau altul, funcția trebuie să returneze o valoare int. Orice valoare non-negativă este interpretată ca executarea cu succes a funcției de apel de sistem și, prin urmare, apelul de sistem în sine. O valoare mai mică de zero este semnul unei erori și conține în același timp un cod de eroare (codurile de eroare sunt definite în antetele include / asm-generic / errno-base.h și include / asm-generic / errno.h) . În Linux, gateway-ul pentru apelurile de sistem până de curând era întreruperea int 0x80, în timp ce în Windows (până la XP Service Pack 2, dacă nu mă înșel) un astfel de gateway este întreruperea 0x2e. Din nou, în kernel-ul Linux, până de curând, toate apelurile de sistem erau gestionate de funcția system_call (). Cu toate acestea, după cum sa dovedit mai târziu, mecanismul clasic de procesare a apelurilor de sistem prin gateway-ul 0x80 duce la o scădere semnificativă a performanței pe procesoarele Intel Pentium 4. Prin urmare, mecanismul clasic a fost înlocuit de metoda obiectelor virtuale dinamice partajate (DSO - fișier de obiecte partajate dinamic. Nu pot garanta traducerea corectă, dar DSO este ceea ce utilizatorii Windows cunosc ca DLL - bibliotecă încărcată și legată dinamic) - VDSO. Care este diferența dintre noua metodă și cea clasică? În primul rând, să ne uităm la metoda clasică care funcționează prin poarta 0x80.

Mecanismul clasic de gestionare a apelurilor de sistem în Linux.

Se întrerupe în arhitectura x86.

Așa cum s-a menționat mai sus, gateway-ul 0x80 (int 0x80) a fost folosit anterior pentru a servi cererile de aplicații ale utilizatorilor. Funcționarea unui sistem bazat pe arhitectura IA-32 este controlată de întreruperi (strict vorbind, acest lucru se aplică în general tuturor sistemelor bazate pe x86). Când apare un eveniment (o nouă bifă a temporizatorului, o anumită activitate pe un dispozitiv, erori - împărțirea la zero etc.), se generează o întrerupere. Întreruperea este denumită astfel deoarece întrerupe de obicei fluxul normal de execuție a codului. Întreruperile sunt de obicei împărțite în întreruperi hardware și software. Întreruperile hardware sunt întreruperile generate de sistem și dispozitive periferice. Atunci când este nevoie ca un dispozitiv să atragă atenția kernel-ului OS, acesta (dispozitivul) generează un semnal pe linia sa de solicitare de întrerupere (IRQ - linie de întrerupere ReQuest). Acest lucru duce la faptul că un semnal corespunzător este generat la anumite intrări ale procesorului, pe baza căruia procesorul decide să întrerupă execuția fluxului de instrucțiuni și să transfere controlul către gestionarul de întreruperi, care află deja ce s-a întâmplat și ce trebuie să fi realizat. Întreruperile hardware sunt de natură asincronă. Aceasta înseamnă că poate avea loc o întrerupere în orice moment. În plus față de dispozitivele periferice, procesorul în sine poate genera întreruperi (sau, mai precis, excepții hardware - de exemplu, diviziunea deja menționată cu zero). Acest lucru se face pentru a notifica sistemul de operare cu privire la apariția unei situații anormale, astfel încât sistemul de operare să poată lua măsuri ca răspuns la apariția unei astfel de situații. După procesarea întreruperii, procesorul revine la execuția programului întrerupt. O întrerupere poate fi inițiată de o aplicație personalizată. Această întrerupere se numește întrerupere software. Întreruperile software, spre deosebire de întreruperile hardware, sunt sincrone. Adică, atunci când este apelată o întrerupere, codul care a apelat-o este suspendat până la întreruperea serviciului. La ieșirea din handler-ul de întreruperi, apare o revenire la adresa îndepărtată salvată mai devreme (când se apelează o întrerupere) în stivă, la următoarea instrucțiune după instrucțiunea de apelare a întreruperii (int). Un handler de întrerupere este o bucată de cod rezidentă (rezidentă în memorie). Acesta este de obicei un program mic. Deși, dacă vorbim despre kernel-ul Linux, atunci tratatorul de întrerupere nu este întotdeauna atât de mic. Un handler de întrerupere este definit de un vector. Un vector nu este altceva decât adresa (segment și offset) de la începutul codului care ar trebui să gestioneze întreruperile cu indexul dat. Lucrul cu întreruperi diferă semnificativ în modul real și modul protejat al procesorului (permiteți-mi să vă reamintesc că în continuare ne referim la procesoare Intel și compatibile cu acestea). În modul real (neprotejat) al funcționării procesorului, gestionarele de întreruperi sunt definite de vectorii lor, care sunt întotdeauna stocate la începutul memoriei, adresa dorită este preluată din tabelul vector de către index, care este și numărul de întrerupere. Rescriind vectorul cu un index specific, vă puteți atribui propriul handler întreruperii.

În modul protejat, gestionarele de întrerupere (porți, porți sau porți) nu mai sunt definite folosind un tabel vector. În locul acestui tabel, se utilizează un tabel de poartă sau, mai corect, un tabel de întrerupere - IDT (Tabelul descriptorilor de întrerupere). Acest tabel este format din nucleu, iar adresa acestuia este stocată în registrul idtr al procesorului. Acest registru nu este accesibil direct. Este posibil să lucrați cu acesta numai folosind instrucțiunile lidt / sidt. Primul dintre ele (lidt) încarcă valoarea specificată în operand în registrul idtr și este adresa de bază a tabelului descriptor de întrerupere, al doilea (sidt) stochează adresa tabelului situat în idtr în operandul specificat. În același mod în care are loc selecția informațiilor despre segment din tabelul descriptor de către selector, are loc și selecția descriptorului de segment care servește întreruperea în modul protejat. Protecția memoriei este acceptată de procesoarele Intel care încep cu procesorul i80286 (nu chiar așa cum este prezentat acum, doar pentru că 286 era un procesor pe 16 biți - prin urmare Linux nu poate rula pe aceste procesoare) și i80386 și, prin urmare, procesorul în sine face toate selecțiile necesare și, prin urmare, nu vom intra adânc în toate subtilitățile modului protejat (și anume, Linux funcționează în modul protejat). Din păcate, nici timpul, nici oportunitățile nu ne permit să ne oprim mult timp asupra mecanismului de gestionare a întreruperilor în modul protejat. Iar acesta nu a fost scopul când am scris acest articol. Toate informațiile oferite aici cu privire la funcționarea familiei de procesoare x86 sunt destul de superficiale și sunt furnizate doar pentru a ajuta la o mai bună înțelegere a mecanismului apelurilor sistemului kernel. Ceva poate fi învățat direct din codul nucleului, deși, pentru o înțelegere completă a ceea ce se întâmplă, este încă recomandabil să vă familiarizați cu principiile modului protejat. Secțiunea de cod care se completează cu valori inițiale (dar nu setează!) IDT se află în arch / i386 / kernel / head.S: / * * setup_idt * * setează un idt cu 256 de intrări care indică spre * ignore_int, porți de întrerupere. Nu se încarcă de fapt * idt - asta se poate face numai după ce paginarea a fost activată * și nucleul mutat în PAGE_OFFSET. Întreruperile * sunt activate în altă parte, când putem fi relativ * siguri că totul este în regulă. * * Atenție:% esi este live în această funcție. * / 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 Câteva note despre cod: codul dat este scris într-un fel de asamblare AT&T, astfel încât cunoștințele dvs. despre asamblare în notația sa obișnuită Intel nu pot fi decât confuze. Cea mai de bază diferență este în ordinea operanzilor. Dacă comanda este definită pentru notația Intel - „acumulator”< "источник", то для ассемблера AT&T порядок прямой. Регистры процессора, как правило, должны иметь префикс "%", непосредственные значения (константы) префиксируются символом доллара "$". Синтаксис AT&T традиционно используется в Un*x-системах.

În exemplul de mai sus, liniile 2-4 stabilesc adresa manipulatorului implicit pentru toate întreruperile. Handler-ul implicit este ignore_int, care nu face nimic. Prezența unui astfel de stub este necesară pentru procesarea corectă a tuturor întreruperilor în această etapă, deoarece pur și simplu nu există încă altele (cu toate acestea, capcanele sunt setate puțin mai jos în cod - consultați manualul de arhitectură Intel pentru capcane sau ceva similar , nu vom fi aici să atingem capcanele). Linia 5 setează tipul supapei. Pe linia 6, încărcăm adresa tabelului IDT în registrul index. Tabelul trebuie să conțină 255 de intrări, câte 8 octeți. În rândurile 8-13, completăm întregul tabel cu aceleași valori stabilite mai devreme în registrele eax și edx - adică aceasta este o poartă de întrerupere care face referire la handler-ul ignore_int. Mai jos definim o macro pentru setarea capcanelor - liniile 14-22. În rândurile 23-26, folosind macro-ul definit mai sus, setăm capcane pentru următoarele excepții: early_divide_err - împărțire la zero (0), early_illegal_opcode - instrucțiuni necunoscute ale procesorului (6), early_protection_fault - fail protection memory (13), early_page_fault - eșecul traducerii paginii (14) ... În paranteză sunt numărul de „întreruperi” generate atunci când apare situația anormală corespunzătoare. Înainte de a verifica tipul procesorului în arch / i386 / kernel / head.S, IDT este setat apelând setup_idt: / * * pornește configurarea sistemului pe 32 de biți. Trebuie să refacem unele dintre lucrurile făcute * în modul pe 16 biți pentru operațiunile „reale”. * / 1.call setup_idt ... 2.call check_x87 3.lgdt early_gdt_descr 4.lidt idt_descr După aflarea tipului de (co) procesor și efectuarea tuturor etapelor pregătitoare din liniile 3 și 4, încărcăm tabelele GDT și IDT, care vor fi utilizate în primele etape ale nucleului.

Apeluri de sistem și int 0x80.

Să revenim de la întreruperi la apeluri de sistem. Deci, ce este nevoie pentru a deservi un proces care solicită un serviciu? Mai întâi, trebuie să treceți de la inelul 3 (nivel de privilegiu CPL = 3) la cel mai privilegiat nivel 0 (inel 0, CPL = 0), deoarece codul nucleului este situat în segmentul cu cele mai mari privilegii. În plus, este necesar un cod de gestionare pentru a deservi procesul. Tocmai pentru asta este utilizat gateway-ul 0x80. Deși există destul de multe apeluri de sistem, toate folosesc un singur punct de intrare - int 0x80. Handlerul în sine este instalat când se apelează funcția arch / i386 / kernel / trap.c :: trap_init (): void __init trap_init (void) (... set_system_gate (SYSCALL_VECTOR, & system_call); ...) Ne interesează cel mai mult această linie în trap_init (). În același fișier de mai sus, puteți vedea codul funcției set_system_gate (): static void __init set_system_gate (unsigned int n, void * addr) (_set_gate (n, DESCTYPE_TRAP | DESCTYPE_DPL3, addr, __KERNEL_CS);) Aici puteți vedea că poarta pentru întrerupere 0x80 (și anume, această valoare este definită de macrocomanda SYSCALL_VECTOR - puteți crede cuvântul :)) este setată ca o capcană cu nivelul de privilegiu DPL = 3 (Ring 3), adică această întrerupere va fi surprinsă atunci când este apelată din spațiul utilizatorului. Problema cu trecerea de la Ring 3 la Ring 0 astfel. rezolvat. Funcția _set_gate () este definită în fișierul antet include / asm-i386 / desc.h. Pentru cei care sunt deosebit de curioși, mai jos este codul, fără explicații lungi, totuși: static inline void _set_gate (poarta int, nesemnat tip int, void * addr, nesemnat seg scurt) (__u32 a, b; pack_gate (& a, & b, (nesemnat lung) addr, seg, tip, 0); write_idt_entry (idt_table , poarta, a, b);) Să ne întoarcem la funcția trap_init (). Se apelează din funcția start_kernel () din init / main.c. Dacă vă uitați la codul trap_init (), puteți vedea că această funcție rescrie din nou unele valori ale tabelului IDT - handlerele care au fost utilizate în etapele timpurii de inițializare a nucleului (early_page_fault, early_divide_err, early_illegal_opcode, early_protection_fault) sunt înlocuite cu cele care vor fi utilizate deja în procesul de procesare a nucleului. Deci, aproape am ajuns la subiect și știm deja că toate apelurile de sistem sunt procesate în același mod - prin gateway-ul int 0x80. Funcția system_call () este setată ca un handler pentru int 0x80, după cum puteți vedea din partea de mai sus a codului arch / i386 / kernel / trap.c :: trap_init ().

system_call ().

Codul funcției system_call () este situat în arch / i386 / kernel / entry.S și arată astfel: # sistem de gestionare a apelurilor stub ENTRARE (apel_sistem) RING0_INT_FRAME # nu se poate „deconecta în spațiul utilizatorului oricum pushl% eax # salvare orig_eax CFI_ADJUST_CFA_OFFSET 4 SAVE_ALL GET_THREAD_INFO (% ebp) # urmărirea apelului de sistem în funcțiune / emulare / număr * Notă, și așa are nevoie de testw și nu testb * / testw $ (_ TIF_SYSCALL_EMU | _TIF_SYSCALL_TRACE | _TIF_SECCOMP | _TIF_SYSCALL_AUDIT), TI_flags (% ebp) jnz syscall_trace_entry cmpl $ (nr_sallcall ea) . Codul nu este afișat integral. După cum puteți vedea, mai întâi, system_call () configurează stiva pentru a funcționa în Ring 0, salvează valoarea transmisă prin eax pe stivă, salvează toate registrele și pe stivă, primește date despre firul de apel și verifică dacă valoarea trecută, numărul de apel al sistemului, depășește limitele tabelului syscall și apoi folosind în cele din urmă valoarea trecută la eax ca argument, system_call () navighează către gestionarul syscall real pe baza cărui intrare de tabel este menționată de index în eax. Acum amintiți-vă vechiul tabel vector de întrerupere a modului vechi. Nu arata nimic? În realitate, desigur, totul este ceva mai complicat. În special, apelul de sistem trebuie să copieze rezultatele din stiva de kernel în stiva de utilizator, să transmită codul de returnare și alte lucruri. În cazul în care argumentul specificat în eax nu se referă la un apel de sistem existent (valoarea este în afara intervalului), apare un salt la eticheta syscall_badsys. Aici, valoarea -ENOSYS este împinsă pe stivă la offsetul la care ar trebui să fie amplasată valoarea eax - apelul de sistem nu este implementat. Aceasta finalizează executarea system_call ().

Tabelul de apeluri de sistem se află în fișierul arch / i386 / kernel / syscall_table.S și are o formă destul de simplă: ENTRY (sys_call_table) .long sys_restart_syscall / * 0 - apel de sistem vechi „setup ()”, utilizat pentru repornirea * / .long sys_exit .long sys_fork .long sys_read .long sys_write .long sys_open / * 5 * / .long sys_close. lung sys_creat ... Cu alte cuvinte, întregul tabel nu este altceva decât o serie de adrese de funcții, aranjate în ordinea numerelor de apel de sistem pe care aceste funcții le servesc. Tabelul este o matrice obișnuită de cuvinte duble (sau cuvinte pe 32 de biți - oricare preferi). Codul pentru unele dintre funcțiile care deservesc apelurile de sistem se află în partea dependentă de platformă - arch / i386 / kernel / sys_i386.c, iar partea independentă de platformă este în kernel / sys.c.

Acesta este cazul cu apelurile de sistem și poarta 0x80.

Noul mecanism pentru gestionarea apelurilor de sistem în Linux. sysenter / sysexit.

După cum sa menționat, a devenit rapid clar că modul tradițional de gestionare a apelurilor de sistem bazate pe poarta 0x80 duce la o pierdere de performanță pe procesoarele Intel Pentium 4. Prin urmare, Linus Torvalds a implementat un nou mecanism în kernel bazat pe instrucțiuni sysenter / sysexit pentru a îmbunătăți performanța kernel-ului pe mașinile echipate cu un procesor Pentium II sau mai mare (cu Pentium II + procesoarele Intel acceptă instrucțiunile sus-menționate sysenter / sysexit). Care este esența noului mecanism? În mod ciudat, esența rămâne aceeași. Execuția s-a schimbat. Conform documentației Intel, instrucțiunea sysenter face parte din mecanismul „apeluri rapide de sistem”. În special, această instrucțiune este optimizată pentru a trece rapid de la un nivel de privilegiu la altul. Mai precis, accelerează tranziția la inelul 0 (Inelul 0, CPL = 0). Procedând astfel, sistemul de operare trebuie să pregătească procesorul pentru a utiliza instrucțiunile sysenter. Această setare se efectuează o dată la încărcarea și inițializarea kernel-ului OS. Când se apelează sysenter, acesta stabilește registrele procesorului în conformitate cu registrele dependente de mașină stabilite anterior de sistemul de operare. În special, sunt setate registrul segmentului și registrul indicatorului de instrucțiuni - cs: eip, precum și segmentul stivei și partea de sus a indicatorului stivei - ss, esp. Trecerea la un nou segment al codului și schimbarea se efectuează de la inelul 3 la 0.

Sysexit face contrariul. Realizează o tranziție rapidă de la nivelul de privilegiu 0 la 3 (CPL = 3). Aceasta setează registrul segmentului de cod la 16 + valoarea segmentului cs stocat în registrul procesorului dependent de mașină. Registrul eip conține conținutul registrului edx. În ss, sunt introduse suma de 24 și valorile lui cs, pe care sistemul de operare le-a introdus anterior în registrul procesorului dependent de mașină la pregătirea contextului pentru ca instrucțiunea sysenter să funcționeze. Esp stochează conținutul registrului ecx. Valorile necesare funcționării instrucțiunilor sysenter / sysexit sunt stocate la următoarele adrese:

  1. SYSENTER_CS_MSR 0x174 - segment de cod, unde este scrisă valoarea segmentului, în care se află codul de gestionare a apelurilor de sistem.
  2. SYSENTER_ESP_MSR 0x175 - pointer către partea de sus a stivei pentru gestionarea apelurilor de sistem.
  3. SYSENTER_EIP_MSR 0x176 - un pointer către un offset din segmentul de cod. Indică începutul codului de gestionare a apelurilor de sistem.
Aceste adrese se referă la registre dependente de model care nu au nume. Valorile sunt scrise în registre dependente de model folosind instrucțiunea wrmsr, în timp ce edx: eax trebuie să conțină părțile superioare și inferioare ale unui cuvânt de 64 de biți, respectiv, și ecx trebuie să conțină adresa registrului la care va scrie fi făcut. În Linux, adresele registrelor dependente de model sunt definite în fișierul antet include / asm-i368 / msr-index.h după cum urmează (înainte de versiunea 2.6.22, cel puțin acestea au fost definite în include / asm-i386 / msr fișier antet .h, permiteți-mi să vă reamintesc că luăm în considerare mecanismul apelurilor de sistem folosind exemplul kernel-ului Linux 2.6.22): #define MSR_IA32_SYSENTER_CS 0x00000174 #define MSR_IA32_SYSENTER_ESP 0x00000175 #define MSR_IA32_SYSENTER_EIP 0x00000176 Codul kernelului responsabil pentru setarea registrelor dependente de model se află în arch / i386 / sysenter.c și arată astfel: 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) + (nesemnat lung) tss; 9.wrmsr (MSR_IA32_SYSENTER_CS, __KERNEL_ wCrms, 0); 10. MSR_IA32_SYSENTER_ESP, tss-> x86_tss.esp1, 0); 11.wrmsr (MSR_IA32_SYSENTER_EIP, (nesemnat lung) sysenter_entry, 0); 12.put_cpu ();) Aici în variabila tss obținem adresa structurii care descrie segmentul stării sarcinii. TSS (Task State Segment) este utilizat pentru a descrie contextul unei sarcini și face parte din mecanismul hardware de multitasking pentru arhitectura x86. Cu toate acestea, Linux practic nu folosește comutarea contextuală a sarcinilor hardware. Conform documentației Intel, trecerea la o altă sarcină se face fie prin executarea unei instrucțiuni de salt intersegmentare (jmp sau apel) care se referă la segmentul TSS, fie la descriptorul porții de sarcini din GDT (LDT). Un registru special de procesor care este invizibil pentru programator - TR (Registrul activităților) conține un selector de descriptor de activități. Când acest registru este încărcat, se încarcă și registrele de bază invizibile și limita asociate cu TR.

Deși Linux nu folosește comutarea contextuală a sarcinilor hardware, nucleul este obligat să lase deoparte o intrare TSS pentru fiecare procesor instalat pe sistem. Acest lucru se datorează faptului că atunci când procesorul trece de la modul utilizator la modul kernel, acesta preia adresa teancului kernel din TSS. În plus, TSS este necesar pentru a controla accesul la porturile I / O. TSS conține o hartă a drepturilor de acces la port. Pe baza acestei hărți, devine posibil să se controleze accesul la port pentru fiecare proces folosind instrucțiuni de intrare / ieșire. Aici tss-> x86_tss.esp1 indică stiva de kernel. __KERNEL_CS indică în mod natural către un segment de cod de nucleu. Adresa funcției sysenter_entry () este specificată ca offset-eip.

Funcția sysenter_entry () este definită în arch / i386 / kernel / entry.S și arată astfel: / * SYSENTER_RETURN indică după instrucțiunea „sysenter” din pagina vsyscall. A se vedea vsyscall-sysentry.S, care definește simbolul. * / # 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 * Nu avem nevoie syspall aici: * / 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 cs, 0 * / / * * Împingeți current_thread_info () -> sysenter_return în stivă. * Este necesară o mică rezolvare a offsetului - 4 * 4 înseamnă cele 4 cuvinte * împinse deasupra; +8 corespunde setării cop_thread "esp0. * / Pushl (TI_sysenter_return-THREAD_SIZE + 8 + 4 * 4) (% esp) CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET eip, 0 / * * Încărcați al șaselea argument potențial din stiva utilizatorului. * Atenție la securitate . * / cmpl $ __ PAGE_OFFSET-3,% ebp jae syscall_fault 1: movl (% ebp),% ebp .section __ex_table, "a" .align 4 .long 1b, syscall_fault. precedente pushl% eax CFI_ADJUST_CFA_THOFFSET) * _TIF_SECCOMP este numărul de biți 8 și, prin urmare, are nevoie de testw și nu de testb * / testw $ (_ TIF_SYSCALL_EMU | _TIF_SYSCALL_TRACE | _TIF_SECCOMP | _TIF_SYSCALL_AUDIT), TI_flags (% jyscalls_cmalls_%),%%,% esp) DISABLE_INTERRUPTS (CLBR_ANY) TRACE_IRQS_OFF movl TI_flags (% ebp),% ecx testw $ _TIF_ALLWORK_MASK,% cx_existers modysit s disable sysexit * / movl PT_EIP (% esp),% edx movl PT_OLDESP (x) 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 .section __al_table 4. "a". 1b, 2b .popsection ENDPROC (sysenter_entry) Ca și în cazul system_call (), cea mai mare parte a muncii se face în linia de apel * sys_call_table (,% eax, 4). Aici este apelat sistemul de gestionare a apelurilor de sistem. Deci, este clar că puțin s-a schimbat fundamental. Faptul că vectorul de întrerupere este acum lovit în hardware și procesor ne ajută să trecem rapid de la un nivel de privilegiu la altul schimbă doar unele dintre detaliile de execuție cu același conținut. Cu toate acestea, modificările nu se termină aici. Amintiți-vă de unde a început povestea. La început, am menționat deja despre obiectele virtuale partajate. Deci, dacă mai devreme implementarea unui apel de sistem, să zicem, din biblioteca de sistem libc arăta ca un apel de întrerupere (în ciuda faptului că biblioteca a preluat unele funcții pentru a reduce numărul de comutatoare de context), acum datorită VDSO apelul de sistem poate fi realizat aproape direct, fără libc. Ar fi putut fi implementat anterior direct, din nou, ca întrerupere. Dar acum apelul poate fi solicitat ca o funcție normală exportată dintr-o bibliotecă legată dinamic (DSO). La pornire, nucleul determină ce mecanism ar trebui și poate fi utilizat pentru o anumită platformă. În funcție de circumstanțe, nucleul setează un punct de intrare la funcția care face apelul de sistem. Apoi, funcția este exportată în spațiul utilizatorului ca bibliotecă linux-gate.so.1. Biblioteca linux-gate.so.1 nu există fizic pe disc. Ca să spunem așa, este emulat de kernel și există exact atât timp cât sistemul rulează. Dacă opriți sistemul, montați FS rădăcină dintr-un alt sistem, atunci nu veți găsi acest fișier pe FS rădăcină a sistemului oprit. De fapt, nu îl veți putea găsi nici măcar pe un sistem care rulează. Fizic, pur și simplu nu există. De aceea linux-gate.so.1 este altceva decât VDSO - adică Obiect virtual partajat dinamic. Nucleul mapează biblioteca dinamică emulată dinamic la spațiul de adrese al fiecărui proces. Este ușor să verificați acest lucru dacă executați următoarea comandă: [e-mail protejat]: ~ $ cat / proc / self / maps 08048000-0804c000 r-xp 00000000 08:01 46 / bin / cat 0804c000-0804d000 rw-p 00003000 08:01 46 / bin / cat 0804d000-0806e000 rw-p 0804d000 00:00 0 ... b7fdf000-b7fe1000 rw-p 00019000 08:01 2066 /lib/ld-2.5.so bffd2000-bffe8000 rw-p bffd2000 00:00 0 ffffe000-fffff000 r-xp 00000000 00:00 0 Aici ultima linie este obiectul care ne interesează: ffffe000-fffff000 r-xp 00000000 00:00 0 Din exemplul dat, se poate observa că obiectul ocupă exact o pagină în memorie - 4096 octeți, practic în curtea din spatiul de adrese. Să facem încă un experiment: [e-mail protejat]: ~ $ ldd `which cat` linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e87000) / lib / ld-linux .so.2 (0xb7fdf000) [e-mail protejat]: ~ $ ldd `which 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 protejat]:~$ Aici tocmai am luat două aplicații direct. Se poate vedea că biblioteca este mapată la spațiul de adrese de proces la aceeași adresă constantă - 0xffffe000. Acum, să încercăm să vedem ce este stocat de fapt pe această pagină de memorie ...

Puteți arunca pagina de memorie în care este stocat codul VDSO partajat folosind următorul program: #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) ; gratuit (tampon); returnează 0;) Strict vorbind, mai devreme acest lucru s-ar fi putut face mai ușor folosind comanda dd if = / proc / self / mem of = test.dump bs = 4096 skip = 1048574 count = 1, dar nucleele de la versiunea 2.6.22, sau poate chiar mai devreme, nu mai mapează memoria procesului la / proc / `pid` / mem. Acest fișier este păstrat, evident în scopuri de compatibilitate, dar nu conține mai multe informații.

Să compilăm și să rulăm programul dat. Să încercăm să demontăm codul rezultat: [e-mail protejat]: ~ / tmp $ objdump --disassemble ./test.dump ./test.dump: format fișier elf32-i386 Demontarea secțiunii .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 sysenter ... ffffe40e: eb f3 jmp ffffe403<__kernel_vsyscall+0x3>ffffe410: 5d pop% ebp ffffe411: 5a pop% edx ffffe412: 59 pop% ecx ffffe413: c3 ret ... [e-mail protejat]: ~ / tmp $ Aici este portalul nostru pentru apeluri de sistem, totul dintr-o privire. Procesul (sau biblioteca de sistem libc), apelând funcția __kernel_vsyscall, ajunge la adresa 0xffffe400 (în cazul nostru). Mai mult, __kernel_vsyscall salvează conținutul registrelor ecx, edx, ebp pe stiva procesului utilizatorului. Am vorbit deja despre atribuirea registrelor ecx și edx mai devreme, în ebp este folosit mai târziu pentru a restabili stiva utilizatorului. Instrucțiunea sysenter este executată, „interceptare de întrerupere” și, în consecință, următoarea tranziție la sysenter_entry (vezi mai sus). Instrucțiunea jmp de la 0xffffe40e este inserată pentru a reporni apelul de sistem cu 6 argumente (a se vedea http://lkml.org/lkml/2002/12/18/). Codul plasat pe pagină este situat în arch / i386 / kernel / vsyscall-enter.S (sau arch / i386 / kernel / vsyscall-int80.S pentru capcana 0x80). Deși am constatat că adresa funcției __kernel_vsyscall este constantă, se crede că acest lucru nu este cazul. De obicei, poziția punctului de intrare la __kernel_vsyscall () poate fi găsită din vectorul ELF-auxv folosind parametrul AT_SYSINFO. Vectorul ELF-auxv conține informații transmise procesului prin stivă la pornire și conține diverse informații necesare în timpul execuției programului. Acest vector conține, în special, variabilele de mediu ale procesului, argumente etc.

Iată un mic exemplu C al modului în care puteți accesa direct funcția __kernel_vsyscall: #include int pid; int main () (__asm ​​("movl $ 20,% eax \ n" "apel *% gs: 0x10 \ n" "movl% eax, pid \ n"); printf ("pid:% d \ n" , pid); returnează 0;) Acest exemplu este preluat de pe pagina Manu Garg, http://www.manugarg.com. Deci, în exemplul de mai sus, facem apelul de sistem getpid () (numărul 20 sau altfel __NR_getpid). Pentru a nu urca în stiva de proces în căutarea variabilei AT_SYSINFO, vom profita de faptul că biblioteca de sistem libc.so la boot copiază valoarea variabilei AT_SYSINFO în blocul de control thread (TCB - Thread Control Block) . Acest bloc de informații este de obicei menționat de un selector în gs. Presupunem că parametrul dorit este situat la offset 0x10 și facem un apel la adresa stocată în% gs: 0x10 $.

Rezultate.

De fapt, în practică, nu este întotdeauna posibil să se obțină un câștig special de performanță chiar și cu sprijinul FSCF (Fast System Call Facility) pe această platformă. Problema este că într-un fel sau altul, un proces rareori vorbește direct cu nucleul. Și există motive întemeiate pentru asta. Utilizarea bibliotecii libc vă permite să garantați portabilitatea programului, indiferent de versiunea kernel-ului. Și prin biblioteca de sistem standard, majoritatea apelurilor de sistem merg. Chiar dacă construiți și instalați cel mai recent kernel construit pentru o platformă care acceptă FSCF, aceasta nu este o garanție a câștigului de performanță. Ideea este că biblioteca dvs. de sistem libc.so va folosi în continuare int 0x80 și poate fi tratată numai prin reconstruirea glibc. Indiferent dacă interfața VDSO și __kernel_vsyscall sunt deloc acceptate în glibc, sincer îmi este greu să răspund în acest moment.

Link-uri.

Pagina lui Manu Garg, http://www.manugarg.com
Scatter / Gather Thoughts de Johan Petersson, http://www.trilithium.com/johan/2005/08/linux-gate/
Bun vechi Înțelegerea kernel-ului Linux Unde putem merge fără el :)
Și, desigur, codul sursă Linux (2.6.22)

Acest material este o modificare a articolului cu același nume de Vladimir Meshkov, publicat în revista „Administrator de sistem”

Acest material este o copie a articolelor lui Vladimir Meșkov din revista „Administrator de sistem”. Aceste articole pot fi găsite la linkurile de mai jos. De asemenea, unele exemple de cod sursă ale programelor au fost modificate - îmbunătățite, rafinate. (Exemplul 4.2 a fost puternic modificat, deoarece a trebuit să intercept un apel de sistem ușor diferit) URL-uri: http://www.samag.ru/img/uploaded/p.pdf http://www.samag.ru/img/uploaded / a3. pdf

Aveți întrebări? Atunci ești aici: [e-mail protejat]

  • 2. Modul de kernel încărcabil
  • 4. Exemple de interceptare a apelurilor de sistem bazate pe LKM
    • 4.1 Prevenirea creării de directoare

1. Vedere generală a arhitecturii Linux

Aspectul cel mai comun vă permite să vedeți un model pe două niveluri al sistemului. nucleu<=>progs În centru (stânga) se află nucleul sistemului. Nucleul interacționează direct cu hardware-ul computerului, izolând programele aplicației de caracteristicile arhitecturale. Nucleul are un set de servicii furnizate programelor de aplicații. Serviciile kernel includ operații de intrare / ieșire (deschidere, citire, scriere și gestionarea fișierelor), crearea și gestionarea proceselor, sincronizarea acestora și comunicarea între procese. Toate aplicațiile solicită servicii kernel prin apeluri de sistem.

Al doilea nivel este alcătuit din aplicații sau sarcini, atât cele de sistem, care determină funcționalitatea sistemului, cât și aplicațiile, care oferă interfața cu utilizatorul Linux. Cu toate acestea, în ciuda eterogenității externe a aplicațiilor, schemele de interacțiune cu nucleul sunt aceleași.

Interacțiunea cu nucleul are loc prin interfața standard de apel de sistem. Interfața de apelare a sistemului este o colecție de servicii kernel și definește formatul cererilor de servicii. Un proces solicită un serviciu printr-un apel de sistem către o anumită procedură de kernel, similară în aparență cu un apel de funcție de bibliotecă normal. Nucleul, în numele procesului, execută cererea și returnează datele necesare procesului.

În acest exemplu, programul deschide un fișier, citește datele din acesta și închide fișierul. În acest caz, operația de deschidere (deschidere), citire (citire) și închidere (închidere) a fișierului este efectuată de kernel la cererea sarcinii, iar funcția deschide (2), citește (2) și închide (2) sunt apeluri de sistem.

/ * Sursa 1.0 * / #include main () (int fd; char buf; / * Deschideți fișierul - obțineți legătura (descriptorul fișierului) fd * / fd = open ("file1", O_RDONLY); / * Citiți 80 de caractere în buffer buf * / read ( fd, buf, sizeof (buf)); / * Închideți fișierul * / închideți (fd);) / * EOF * / Lista completă a apelurilor de sistem OS Linux poate fi găsită în / usr / include / asm / unistd. h fișier. Să vedem acum mecanismul pentru efectuarea apelurilor de sistem folosind acest exemplu. Compilatorul, după ce a îndeplinit funcția open () pentru a deschide fișierul, îl convertește în cod de asamblare, asigurându-se că numărul de apel de sistem corespunzător acestei funcții și parametrii săi sunt încărcați în registrele procesorului și apelul ulterior pentru a întrerupe 0x80. Următoarele valori sunt încărcate în registrele procesorului:

  • în registrul EAX - numărul de apel al sistemului. Deci, pentru cazul nostru, numărul de apel al sistemului este 5 (a se vedea __NR_open).
  • în registrul EBX - primul parametru al funcției (pentru open () este un pointer către un șir care conține numele fișierului deschis.
  • la registrul ECX - al doilea parametru (permisiuni de fișiere)
Al treilea parametru este încărcat în registrul EDX, în acest caz nu îl avem. Pentru a executa un apel de sistem în OS Linux, se utilizează funcția system_call, care este definită (în funcție de arhitectură, în acest caz i386) în fișierul /usr/src/linux/arch/i386/kernel/entry.S. Această funcție este punctul de intrare pentru toate apelurile de sistem. Nucleul reacționează pentru a întrerupe 0x80 apelând funcția system_call, care este, de fapt, un handler pentru întreruperea 0x80.

Pentru a ne asigura că suntem pe drumul cel bun, să ne uităm la codul funcției open () din sistemul libc:

# gdb -q /lib/libc.so.6 (gdb) disas open Dump de cod asamblor pentru funcția deschisă: 0x000c8080 : sunați la 0x1082be< __i686.get_pc_thunk.cx >0x000c8085 : adăugați $ 0x6423b,% ecx 0x000c808b : cmpl $ 0x0,0x1a84 (% ecx) 0x000c8092 : jne 0xc80b1 0x000c8094 : împingeți% 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 ... Deoarece nu este dificil de văzut în ultimele rânduri, parametrii sunt transferați în registrele EDX, ECX, EBX, iar numărul de apel al sistemului este plasat în ultimul registru EAX, egal cu 5, ca știm deja.

Acum să revenim la mecanismul de apelare a sistemului. Deci, nucleul apelează handlerul de întrerupere 0x80 - funcția system_call. System_call pune copii ale registrelor care conțin parametrii de apel pe stivă utilizând macro-ul SAVE_ALL și apelează funcția de sistem necesară cu comanda de apel. Tabelul de indicatori către funcțiile kernel care implementează apeluri de sistem se află în matricea sys_call_table (vezi fișier arch / i386 / kernel / entry.S). Numărul de apel al sistemului, care se află în registrul EAX, este un index din această matrice. Astfel, dacă EAX este 5, va fi apelată funcția kernel sys_open (). De ce este necesară macrocomanda SAVE_ALL? Explicația este foarte simplă. Deoarece aproape toate funcțiile nucleului de sistem sunt scrise în C, ele își caută parametrii în stivă. Și parametrii sunt împinși pe stivă folosind SAVE_ALL! Valoarea returnată de apelul de sistem este stocată în registrul EAX.

Acum să ne dăm seama cum să interceptăm apelul de sistem. Mecanismul modulelor kernel încărcabile ne va ajuta în acest sens.

2. Modul de kernel încărcabil

Loadable Kernel Module (prescurtat în mod obișnuit ca LKM - Loadable Kernel Module) este un cod de program care rulează în spațiul kernel. Principala caracteristică a LKM este capacitatea de a încărca și descărca dinamic fără a fi nevoie să reporniți întregul sistem sau să recompilați nucleul.

Fiecare LKM constă din două funcții principale (minim):

  • funcția de inițializare a modulului. Apelat când LKM este încărcat în memorie: int init_module (void) (...)
  • funcția de descărcare a modulului: void cleanup_module (void) (...)
Iată un exemplu al celui mai simplu modul: / * Source 2.0 * / #include int init_module (void) (printk ("Hello World \ n"); return 0;) void cleanup_module (void) (printk ("Bye \ n");) / * EOF * / Compilați și încărcați modulul. Modulul este încărcat în memorie cu comanda insmod, iar modulele încărcate sunt vizualizate cu comanda lsmod: # gcc -c -DMODULE -I / usr / src / linux / include / src-2.0.c # insmod src-2.0. o Avertisment: încărcarea src-2.0 .o va deteriora nucleul: nu s-a încărcat modulul src-2.0 de licență, cu avertismente # dmesg | tail -n 1 Hello World # lsmod | grep src src-2.0 336 0 (neutilizat) # rmmod src-2.0 # dmesg | coada -n 1 Pa

3. Algoritm pentru interceptarea unui apel de sistem bazat pe LKM

Pentru a implementa un modul care interceptează un apel de sistem, este necesar să se definească un algoritm de interceptare. Algoritmul este după cum urmează:
  • păstrați un indicator către apelul original (original) pentru a-l putea restabili
  • creați o funcție care implementează noul apel de sistem
  • înlocuiți apelurile în tabelul de apeluri de sistem sys_call_table, adică setați indicatorul corespunzător la un nou apel de sistem
  • la sfârșitul lucrului (la descărcarea modulului) restaurați apelul de sistem original folosind indicatorul salvat anterior
Urmărirea vă permite să aflați ce apeluri de sistem sunt implicate în funcționarea aplicației utilizatorului. Prin urmărire, puteți determina ce apel de sistem trebuie interceptat pentru a prelua controlul aplicației. # ltrace -S ./src-1.0 ... deschis ("fișier1", 0, 01 SYS_open ("fișier1", 0, 01) = 3<... open resumed>) = 3 citite (3, SYS_read (3, "123 \ n", 80) = 4<... read resumed>"123 \ n", 80) = 4 închis (3 SYS_close (3) = 0<... close resumed>) = 0 ... Acum avem suficiente informații pentru a începe studierea exemplelor de implementare a modulelor care interceptează apelurile de sistem.

4. Exemple de interceptare a apelurilor de sistem bazate pe LKM

4.1 Prevenirea creării de directoare

Când este creat directorul, se apelează funcția kernel sys_mkdir. Un șir care conține numele directorului creat este specificat ca parametru. Luați în considerare codul care interceptează apelul de sistem corespunzător. / * Sursa 4.1 * / #include #include #include / * Exportați tabelul de apeluri de sistem * / extern void * sys_call_table; / * Definiți un pointer pentru a salva apelul original * / int (* orig_mkdir) (const char * path); / * Să creăm propriul nostru apel de sistem. Apelul nostru nu face nimic, doar returnează o valoare zero * / int own_mkdir (const char * path) (return 0;) / * În timpul inițializării modulului, salvăm indicatorul în apelul original și înlocuim apelul de sistem * / int init_module (void ) (orig_mkdir = sys_call_table; sys_call_table = own_mkdir; printk ("sys_mkdir substituit \ n"); return (0);) / * Când descărcați, restaurați apelul original * / void cleanup_module (void) (sys_call_table = orig_mkdir; sys_mkdir moved_nmkdir ");) / * EOF * / Pentru a obține modulul obiect, rulați următoarea comandă și efectuați câteva experimente pe sistem: # gcc -c -DMODULE -I / usr / src / linux / include / src-3.1. c # dmesg | tail -n 1 sys_mkdir înlocuit # mkdir test # ls -ald test ls: test: Nu există un astfel de fișier sau director # rmmod src-3.1 # dmesg | tail -n 1 sys_mkdir mutat înapoi # mkdir test # ls -ald test drwxr-xr-x 2 rădăcină rădăcină 4096 2003-12-23 03:46 test După cum puteți vedea, comanda "mkdir" nu funcționează, sau mai degrabă nimic se întâmplă. Pentru a restabili funcționalitatea sistemului, este suficient să descărcați modulul. Așa s-a făcut mai sus.

4.2 Ascunderea unei intrări de fișiere într-un director

Determinați ce apel de sistem este responsabil pentru citirea conținutului directorului. Pentru a face acest lucru, vom scrie un alt fragment de test care citește directorul curent: / * Source 4.2.1 * / #include #include int main () (DIR * d; struct dirent * dp; d = opendir ("."); dp = readdir (d); return 0;) / * EOF * / Obțineți executabilul și urmăriți-l: # 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, 0xbffff874) = 0 SYS_fcntl64 (3, 2, 1, 1, 0x4014c2c0) = 0 SYS6a5_ck 1, 0x4014c2c0) = 0 SYS6a5_brk (NS8014c2c0) = 0 SYS6a_brk (NULL = 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 ... Fii atent la ultima linie. Conținutul directorului este citit de funcția getdents64 (getdents este posibil în alte nuclee). Rezultatul este stocat ca o listă de structuri de tip struct dirent și funcția în sine returnează lungimea tuturor intrărilor din director. Suntem interesați de două domenii ale acestei structuri:
  • d_reclen - dimensiunea înregistrării
  • d_name - numele fișierului
Pentru a ascunde înregistrarea fișierului despre fișier (cu alte cuvinte, faceți-l invizibil), este necesar să interceptați apelul de sistem sys_getdents64, să găsiți înregistrarea corespunzătoare în lista structurilor primite și să o ștergeți. Luați în considerare codul care efectuează această operațiune (autorul codului original este Michal Zalewski): / * Sursa 4.2.2 * / #include #include #include #include #include #include #include #include extern void * sys_call_table; int (* orig_getdents) (u_int fd, struct dirent * dirp, u_int count); / * Definiți-ne propriul apel de sistem * / int own_getdents (u_int fd, struct dirent * dirp, u_int count) (nesemnat int tmp, n; int t; struct dirent64 (int d_ino1, d_ino2; int d_off1, d_off2; unsigned short d_reclen; unsigned char d_type; char d_name;) * dirp2, * dirp3; / * Numele fișierului pe care dorim să îl ascundem * / char hide = "file1"; / * Determinați lungimea intrărilor din director * / tmp = ( * orig_getdents) (fd, dirp, count); if (tmp> 0) (/ * Alocați memorie pentru structură în spațiul kernel și copiați conținutul directorului în acesta * / dirp2 = (struct dirent64 *) kmalloc (tmp, GFP_KERNEL); copy_from_user (dirp2, dirp, tmp); / * Să folosim a doua structură și să salvăm lungimea înregistrărilor din directorul * / dirp3 = dirp2; t = tmp; / * Să începem să căutăm fișierul nostru * / while (t> 0) (/ * Citiți lungimea primei înregistrări și determinați lungimea rămasă a înregistrărilor din director * / n = dirp3-> d_reclen; t - = n; / * Verificați dacă numele fișierului din înregistrarea curentă se potrivește cu cel căutat * / if (strstr ((char *) & (dirp3-> d_name), (char *) & ascunde)! = NULL) (/ * Dacă da, suprascrieți intrarea și calculați noua lungime a intrărilor din directorul * / memcpy (dirp3, (char *) dirp3 + dirp3-> d_reclen, t); tmp - = n; ) / * Poziționați indicatorul către următoarea înregistrare și continuați căutarea * / dirp3 = (struct dirent64 *) ((char *) dirp3 + dirp3-> d_reclen); ) / * Returnează rezultatul și eliberează memoria * / copy_to_user (dirp, dirp2, tmp); kfree (dirp2); ) / * Returnează lungimea intrărilor din director * / return tmp; ) / * Funcțiile pentru inițializarea și descărcarea modulului au un formular standard * / 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 * / Compilând acest cod, observați cum dispare „fișier1”, după cum este necesar.

5. Metoda de acces direct la spațiul de adrese kernel / dev / kmem

Să analizăm mai întâi teoretic modul în care interceptarea se realizează prin metoda accesului direct la spațiul de adrese al nucleului și apoi trecem la implementarea practică.

Accesul direct la spațiul de adresă al nucleului este asigurat de fișierul dispozitivului / dev / kmem. Acest fișier afișează tot spațiul de adresă virtual disponibil, inclusiv partiția de swap (zona de swap). Pentru a lucra cu fișierul kmem, sunt utilizate funcțiile standard ale sistemului - open (), read (), write (). După ce ați deschis / dev / kmem în mod standard, ne putem referi la orice adresă din sistem, specificându-l ca un offset în acest fișier. Această metodă a fost dezvoltată de Silvio Cesare.

Funcțiile sistemului sunt accesate prin încărcarea parametrilor funcției în registrele procesorului și apoi apelarea întreruperii software-ului 0x80. Handlerul de întrerupere, funcția system_call, împinge parametrii de apel pe stivă, recuperează adresa funcției de sistem apelate din sys_call_table și transferă controlul la această adresă.

Cu acces complet la spațiul de adresă al nucleului, putem obține întregul conținut al tabelului de apeluri de sistem, adică adresele tuturor funcțiilor sistemului. Schimbând adresa oricărui apel de sistem, îl interceptăm. Dar pentru aceasta trebuie să cunoașteți adresa tabelului sau, cu alte cuvinte, offset-ul din fișierul / dev / kmem unde se află acest tabel.

Pentru a determina adresa sys_call_table, trebuie mai întâi să calculați adresa funcției system_call. Deoarece această funcție este un manipulator de întreruperi, să ne uităm la modul în care sunt gestionate întreruperile în modul protejat.

În modul real, procesorul, atunci când înregistrează o întrerupere, se referă la tabelul vectorului de întrerupere, care este întotdeauna chiar la începutul memoriei și conține adrese cu două condiții ale programelor de procesare a întreruperii. În modul protejat, Tabelul descriptorului de întrerupere (IDT) situat în sistemul de operare în modul protejat este analog tabelului vectorului de întrerupere. Pentru ca procesorul să acceseze acest tabel, adresa acestuia trebuie încărcată în IDTR (Registrul tabelului cu descriptori de întrerupere). IDT conține descriptori pentru gestionarele de întreruperi, care, în special, includ adresele lor. Acești descriptori se numesc gateway-uri (porți). Procesorul, înregistrând o întrerupere, prin numărul său extrage gateway-ul din IDT, determină adresa handlerului și îi transferă controlul.

Pentru a calcula adresa funcției system_call din tabelul IDT, este necesar să extrageți gateway-ul de întrerupere în $ 0x80, iar din acesta - adresa handlerului corespunzător, adică adresa funcției system_call. În funcția system_call, accesul la system_call_table este realizat de comanda de apel<адрес_таблицы>(,% eax, 4). După ce am găsit opcode-ul (semnătura) acestei comenzi în fișierul / dev / kmem, vom găsi și adresa tabelului de apeluri de sistem.

Pentru a determina codul opțional, vom folosi depanatorul și vom dezasambla funcția system_call:

# gdb -q / usr / src / linux / vmlinux (gdb) disas system_call Dump de cod asamblor pentru funcția system_call: 0xc0194cbc : împingeți% eax 0xc0194cbd : cld 0xc0194cbe : împingeți% es 0xc0194cbf : apăsați% ds 0xc0194cc0 : împingeți% eax 0xc0194cc1 : împingeți% ebp 0xc0194cc2 : împingeți% edi 0xc0194cc3 : împingeți% esi 0xc0194cc4 : împingeți% edx 0xc0194cc5 : împingeți% ecx 0xc0194cc6 : împingeți% ebx 0xc0194cc7 : mov $ 0x18,% edx 0xc0194ccc : mov% edx,% ds 0xc0194cce : mov% edx,% es 0xc0194cd0 : mov $ 0xffffe000,% ebx 0xc0194cd5 : și% esp,% ebx 0xc0194cd7 : testb $ 0x2,0x18 (% ebx) 0xc0194cdb : jne 0xc0194d3c 0xc0194cdd : cmp $ 0x10e,% eax 0xc0194ce2 : jae 0xc0194d69 0xc0194ce8 : apel * 0xc02cbb0c (,% eax, 4) 0xc0194cef : mov% eax, 0x18 (% esp, 1) 0xc0194cf3 : nop Sfârșitul de gunoi al ansamblului. Linia „call * 0xc02cbb0c (,% eax, 4)” este apelul către tabelul sys_call_table. Valoarea 0xc02cbb0c este adresa tabelului (cel mai probabil numerele dvs. vor fi diferite). Obținem opcode-ul acestei comenzi: (gdb) x / xw system_call + 44 0xc0194ce8 : 0x0c8514ff Am găsit opcode-ul comenzii pentru a accesa tabelul sys_call_table. Este \ xff \ x14 \ x85. Următorii 4 octeți sunt adresa tabelului. Puteți verifica acest lucru introducând comanda: (gdb) x / xw system_call + 44 + 3 0xc0194ceb : 0xc02cbb0c Astfel, găsind secvența \ xff \ x14 \ x85 în fișierul / dev / kmem și citind următorii 4 octeți, obținem adresa tabelului de apeluri de sistem sys_call_table. Cunoscându-i adresa, putem obține conținutul acestui tabel (adrese ale tuturor funcțiilor sistemului) și putem schimba adresa oricărui apel de sistem prin interceptarea acestuia.

Luați în considerare pseudocodul care efectuează operația de interceptare:

Readaddr (old_syscall, scr + SYS_CALL * 4, 4); writeaddr (new_syscall, scr + SYS_CALL * 4, 4); Funcția readaddr citește adresa apelului de sistem din tabelul de apeluri de sistem și o stochează în variabila old_syscall. Fiecare intrare din sys_call_table are o lungime de 4 octeți. Adresa dorită este localizată la offset sct + SYS_CALL * 4 în fișierul / dev / kmem (aici sct este adresa tabelului sys_call_table, SYS_CALL este numărul de ordine al apelului de sistem). Funcția writeaddr suprascrie adresa apelului de sistem SYS_CALL cu adresa funcției new_syscall și toate apelurile către apelul de sistem SYS_CALL vor fi deservite de această funcție.

Se pare că totul este simplu și obiectivul a fost atins. Cu toate acestea, să ne amintim că lucrăm în spațiul de adrese al utilizatorului. Dacă plasăm o nouă funcție de sistem în acest spațiu de adrese, atunci când apelăm această funcție vom primi un mesaj de eroare frumos. De aici concluzia - un nou apel de sistem trebuie plasat în spațiul de adresă al nucleului. Pentru a face acest lucru, trebuie să: obțineți un bloc de memorie în spațiul kernel, plasați un nou apel de sistem în acest bloc.

Puteți aloca memorie în spațiul kernel utilizând funcția kmalloc. Dar nu puteți apela o funcție de kernel direct din spațiul de adresă al utilizatorului, așa că vom folosi următorul algoritm:

  • cunoscând adresa tabelului sys_call_table, obținem adresa unor apeluri de sistem (de exemplu, sys_mkdir)
  • definiți o funcție care numește funcția kmalloc. Această funcție returnează un indicator către un bloc de memorie în spațiul de adresă al nucleului. Să numim această funcție get_kmalloc
  • salvați primii N octeți ai apelului de sistem sys_mkdir, unde N este dimensiunea funcției get_kmalloc
  • suprascrieți primii N octeți ai apelului sys_mkdir cu funcția get_kmalloc
  • facem un apel către apelul de sistem sys_mkdir, pornind astfel funcția get_kmalloc pentru execuție
  • restaurați primii N octeți ai apelului de sistem sys_mkdir
Ca urmare, avem la dispoziție un bloc de memorie situat în spațiul kernel.

Dar pentru a implementa acest algoritm, avem nevoie de adresa funcției kmalloc. Există mai multe moduri de a o găsi. Cel mai simplu este să citiți această adresă din fișierul System.map sau să o determinați utilizând depanatorul gdb (print & kmalloc). Dacă suportul modulului este activat în kernel, adresa kmalloc poate fi determinată folosind funcția get_kernel_syms (). Această opțiune va fi discutată mai jos. Dacă nu există suport pentru modulele kernel, atunci adresa funcției kmalloc va trebui căutată de opcode-ul comenzii de apel kmalloc - similar cu modul în care s-a făcut pentru tabelul sys_call_table.

Kmalloc ia doi parametri: dimensiunea memoriei solicitate și specificatorul GFP. Pentru a găsi opcode-ul, vom folosi depanatorul și vom dezasambla orice funcție de nucleu care conține un apel către funcția kmalloc.

# gdb -q / usr / src / linux / vmlinux (gdb) disas inter_module_register Dump de cod asamblor pentru funcția inter_module_register: 0xc01a57b4 : împingeți% ebp 0xc01a57b5 : împingeți% edi 0xc01a57b6 : împingeți% esi 0xc01a57b7 : împingeți% ebx 0xc01a57b8 : sub $ 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 : sunați la 0xc01bea2a ... Nu contează ce face funcția, principalul lucru în ea este ceea ce avem nevoie - un apel la funcția kmalloc. Fii atent la ultima linie. Mai întâi, parametrii sunt încărcați pe stivă (registrul esp indică partea de sus a stivei), apoi urmează apelul funcțional. Specificatorul GFP ($ 0x1f0,0x4 (% esp, 1) este încărcat mai întâi în stivă. Pentru versiunile kernel 2.4.9 și mai mari, această valoare este 0x1f0. Găsiți codul op din această comandă: (gdb) x / xw inter_module_register + 19 0xc01a57c7 : 0x042444c7 Dacă găsim acest opcode, putem calcula adresa funcției kmalloc. La prima vedere, adresa acestei funcții este un argument pentru instrucțiunea de apel, dar acest lucru nu este în întregime adevărat. Spre deosebire de funcția system_call, aici în spatele instrucțiunii nu se află adresa kmalloc, ci decalajul față de adresa curentă. Verificăm acest lucru definind opcode-ul comenzii de apel 0xc01bea2a: (gdb) x / xw inter_module_register + 34 0xc01a57d6 : 0x01924fe8 Primul octet este e8, care este codul opțional al instrucțiunii de apel. Să găsim valoarea argumentului acestei comenzi: (gdb) x / xw inter_module_register + 35 0xc01a57d7 : 0x0001924f Acum, dacă adăugăm adresa curentă 0xc01a57d6, offset 0x0001924f și 5 octeți ai comenzii, vom obține adresa necesară a funcției kmalloc - 0xc01bea2a.

Aceasta completează calculele teoretice și, folosind tehnica de mai sus, vom intercepta apelul de sistem sys_mkdir.

6. Un exemplu de interceptare prin intermediul / dev / kmem

/ * sursa 6.0 * / #include #include #include #include #include #include #include #include / * Număr de apel de sistem pentru interceptare * / #define _SYS_MKDIR_ 39 #define KMEM_FILE "/ dev / kmem" #define MAX_SYMS 4096 / * IDTR descriere format registru * / struct (limită scurtă nesemnată; bază int nesemnată;) __attribute__ ((ambalat) ) idtr; / * Descrierea formatului gateway-ului de întrerupere IDT * / struct (nesemnat scurt off1; nesemnat sel scurt scurt; nesemnat char none, steaguri; nesemnat scurt off2;) __attribute__ ((ambalat)) idt; / * Descrierea structurii pentru funcția get_kmalloc * / struct kma_struc (ulong (* kmalloc) (uint, int); // - adresa funcției kmalloc dimensiunea int; // - dimensiunea memoriei pentru alocarea steagurilor int; // - flag, pentru nuclee> 2.4.9 = 0x1f0 (GFP) ulong mem;) __attribute__ ((împachetat)) kmalloc; / * O funcție care alocă doar un bloc de memorie în spațiul de adresă al nucleului * / int get_kmalloc (struct kma_struc * k) (k-> mem = k-> kmalloc (k-> size, k-> flags); returnează 0 ;) / * O funcție care returnează adresa funcției (necesară pentru a găsi kmalloc) * / ulong get_sym (char * n) (struct kernel_sym tab; int numsyms; int i; numsyms = get_kernel_syms (NULL); if (numsyms> MAX_SYMS || numsymes< 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 Să deschidem fișierul dump și să găsim datele care ne interesează: 080485a4 g F .text 00000032 get_kmalloc 080486b1 g F .text 0000000a new_mkdir Acum vom introduce aceste valori în programul nostru: ulong get_kmalloc_size = 0x32; ulong get_kmalloc_addr = 0x080485a4; ulong new_mkdir_size = 0x0a; ulong new_mkdir_addr = 0x080486b1; Acum să recompilăm programul. Lansându-l pentru execuție, vom intercepta apelul de sistem sys_mkdir. Toate apelurile către apelul sys_mkdir vor fi acum deservite de funcția new_mkdir.

Sfârșitul hârtiei / EOP

Performanța codului din toate secțiunile a fost testată pe kernel-ul 2.4.22. La pregătirea raportului, au fost utilizate materiale de pe site

Cel mai adesea, codul de apel al sistemului cu numărul __NR_xxx, definit în /usr/include/asm/unistd.h, poate fi găsit în sursa kernel Linux sub funcția sys_xxx(). (Masa de apelare pentru i386 poate fi găsită în /usr/src/linux/arch/i386/kernel/entry.S.) Există multe excepții de la această regulă, în principal datorită faptului că majoritatea apelurilor de sistem vechi sunt înlocuite cu altele noi, fără niciun sistem. Pe platformele care emulează sisteme de operare proprietare, cum ar fi parisc, sparc, sparc64 și alfa, există multe apeluri de sistem suplimentare; există, de asemenea, un set complet de apeluri de sistem pe 32 de biți pentru mips64.

De-a lungul timpului, dacă este necesar, au existat modificări în interfața unor apeluri de sistem. Unul dintre motivele acestei modificări a fost necesitatea creșterii dimensiunii structurilor sau scalarilor trecuți apelului de sistem. Datorită acestor modificări, pe unele arhitecturi (și anume pe vechiul i386 pe 32 de biți), au apărut diferite grupuri de apeluri de sistem similare (de exemplu, trunchia (2) și trunchie64 (2)) care îndeplinesc aceleași sarcini, dar diferă prin mărimea argumentelor lor. (După cum sa menționat, acest lucru nu afectează aplicațiile: împachetările glibc fac o parte din lucrarea de a porni apelul de sistem corect și aceasta oferă compatibilitate ABI pentru binarele mai vechi.) Exemple de apeluri de sistem care au mai multe versiuni:

* Există în prezent trei versiuni diferite stat (2): sys_stat() (un loc __NR_oldstat), sys_newstat() (un loc __NR_stat) și sys_stat64() (un loc __NR_stat64), acesta din urmă este utilizat în prezent. O situație similară cu lstat (2) și fstat (2). * Definit în mod similar __NR_oldolduname, __NR_oldunameși __NR_uname pentru apeluri sys_olduname(), sys_uname() și sys_newuname(). * Linux 2.0 are o nouă versiune vm86 (2) se apelează versiuni noi și vechi ale procedurilor nucleare sys_vm86old() și sys_vm86(). * Linux 2.4 are o nouă versiune getrlimit (2) se apelează versiuni noi și vechi ale procedurilor nucleare sys_old_getrlimit() (un loc __NR_getrlimit) și sys_getrlimit() (un loc __NR_ugetrlimit). * În Linux 2.4, dimensiunea câmpului ID utilizator și grup a fost mărită de la 16 la 32 de biți. Au fost adăugate mai multe apeluri de sistem pentru a sprijini această modificare (de ex. chown32 (2), getuid32 (2), getgroups32 (2), setresuid32 (2)), eliminând apelurile anterioare cu aceleași nume, dar fără sufixul „32”. * Linux 2.4 adaugă suport pentru accesarea fișierelor mari (care dimensiuni și decalaje nu se încadrează în 32 de biți) în aplicații pe arhitecturi pe 32 de biți. Acest lucru a necesitat modificări ale apelurilor de sistem care se ocupă de dimensiunile și compensările fișierelor. Au fost adăugate următoarele apeluri de sistem: fcntl64 (2), getdents64 (2), stat64 (2), statfs64 (2), trunchie64 (2) și omologii lor, care gestionează descriptori de fișiere sau legături simbolice. Aceste apeluri de sistem depășesc vechile apeluri de sistem, care, cu excepția apelurilor „stat”, sunt de asemenea denumite, dar nu au sufixul „64”.

Pe platformele mai noi care au acces la fișier pe 64 de biți și UID / GID pe 32 de biți (de exemplu, alpha, ia64, s390x, x86-64), există o singură versiune de apeluri de sistem pentru UID / GID și acces la fișiere. Pe platforme (de obicei platforme pe 32 de biți) unde există apeluri * 64 și * 32, alte versiuni sunt depreciate.

* Apeluri rt_sig * adăugat în kernel 2.2 pentru a suporta semnale suplimentare în timp real (vezi. semnal (7)). Aceste apeluri de sistem înlocuiesc vechile apeluri de sistem cu aceleași nume, dar fără prefixul "rt_". * În apelurile de sistem Selectați (2) și mmap (2) se utilizează cinci sau mai multe argumente, provocând probleme în determinarea modului în care argumentele sunt transmise la i386. Ca o consecință a acestui lucru, în timp ce pe alte arhitecturi, apeluri sys_select() și sys_mmap() Meci __NR_selectși __NR_mmap, pe i386 corespund selectare_vechi() și vechi_mmap() (proceduri care utilizează un pointer către un bloc de argumente). În prezent, nu mai există o problemă cu transmiterea a mai mult de cinci argumente și există __NR__newselect care se potrivește exact sys_select(), și aceeași situație cu __NR_mmap2.

VLADIMIR MESHKOV

Interceptarea apelurilor de sistem în Linux

În ultimii ani, sistemul de operare Linux s-a impus cu fermitate ca platformă de server de top, înaintea multor dezvoltări comerciale. Cu toate acestea, problemele legate de protejarea sistemelor informatice construite pe baza acestui sistem de operare nu încetează să mai fie relevante. Există un număr mare de mijloace tehnice, atât software cât și hardware, care vă permit să asigurați securitatea sistemului. Acestea sunt instrumente pentru criptarea datelor și a traficului de rețea, diferențierea drepturilor de acces la resursele informaționale, protejarea e-mailurilor, serverelor web, protecția antivirus etc. Lista, după cum înțelegeți, este destul de lungă. În acest articol, vă sugerăm să luați în considerare un mecanism de protecție bazat pe interceptarea apelurilor de sistem ale sistemului de operare Linux. Acest mecanism vă permite să preluați controlul asupra oricărei aplicații și astfel să preveniți posibilele acțiuni distructive pe care le poate efectua.

Apeluri de sistem

Să începem cu o definiție. Apelurile de sistem sunt o colecție de funcții implementate în nucleul sistemului de operare. Orice solicitare din aplicația utilizatorului se transformă în cele din urmă într-un apel de sistem care efectuează acțiunea solicitată. O listă completă a apelurilor de sistem Linux poate fi găsită în fișierul /usr/include/asm/unistd.h. Să aruncăm o privire la mecanismul general pentru efectuarea apelurilor de sistem cu un exemplu. Lăsați funcția creat () să fie apelată în codul sursă al aplicației pentru a crea un fișier nou. Când compilatorul întâlnește un apel către această funcție, îl convertește în cod de asamblare, asigurându-se că numărul de apel de sistem corespunzător acestei funcții și parametrii săi sunt încărcați în registrele procesorului și apelul ulterior pentru a întrerupe 0x80. Următoarele valori sunt încărcate în registrele procesorului:

  • pentru a înregistra EAX- numărul de apel al sistemului. Deci, pentru cazul nostru, numărul de apel al sistemului va fi 8 (a se vedea __NR_creat);
  • la registrul EBX- primul parametru al funcției (pentru creat, acesta este un pointer către un șir care conține numele fișierului care trebuie creat);
  • la registrul ECX- al doilea parametru (drepturi de acces la fișiere).

Al treilea parametru este încărcat în registrul EDX, în acest caz nu îl avem. Pentru a executa un apel de sistem pe Linux, se utilizează funcția system_call, care este definită în fișierul /usr/src/liux/arch/i386/kernel/entry.S. Această funcție este punctul de intrare pentru toate apelurile de sistem. Nucleul reacționează pentru a întrerupe 0x80 apelând funcția system_call, care este, de fapt, un handler pentru întreruperea 0x80.

Pentru a ne asigura că suntem pe drumul cel bun, să scriem un mic fragment de test în asamblare. Aici vom vedea ce devine funcția creat () după compilare. Să numim testul fișierului.S. Iată conținutul său:

Globl _start

Text

Start:

Încărcați numărul de apel al sistemului în registrul EAX:

movl $ 8,% eax

Registrul EBX este primul parametru, un pointer către un șir cu numele fișierului:

movl $ filename,% ebx

În registrul ECX - al doilea parametru, drepturile de acces:

movl $ 0,% ecx

Apelarea întreruperii:

int 0x80 $

Ieșim din program. Pentru aceasta, apelați funcția exit (0):

movl $ 1,% eax movl $ 0,% ebx int $ 0x80

În segmentul de date, specificați numele fișierului care urmează să fie creat:

Date

nume de fișier: .string "file.txt"

Compila:

testul gcc -c.S

ld -s -o test test.o

Testul fișierului executabil va apărea în directorul curent. Executându-l, vom crea un nou fișier numit file.txt.

Acum să revenim la mecanismul de apelare a sistemului. Deci, nucleul apelează handlerul de întrerupere 0x80 - funcția system_call. System_call împinge copiile registrelor care conțin parametrii de apel pe stivă utilizând macrocomanda SAVE_ALL și apelează funcția de sistem necesară cu comanda de apel. Tabelul de indicatori către funcțiile kernel care implementează apeluri de sistem se află în matricea sys_call_table (vezi fișier arch / i386 / kernel / entry.S). Numărul de apel al sistemului, care se află în registrul EAX, este un index din această matrice. Astfel, dacă EAX conține o valoare de 8, va fi apelată funcția kernel sys_creat (). De ce este necesară macrocomanda SAVE_ALL? Explicația este foarte simplă. Deoarece aproape toate funcțiile nucleului de sistem sunt scrise în C, ele își caută parametrii în stivă. Iar parametrii sunt împinși pe stivă folosind macro-ul SAVE_ALL! Valoarea returnată de apelul de sistem este stocată în registrul EAX.

Acum să ne dăm seama cum să interceptăm apelul de sistem. Mecanismul modulelor kernel încărcabile ne va ajuta în acest sens. Deși am discutat anterior despre dezvoltarea și utilizarea modulelor kernel, în interesul coerenței, vom discuta pe scurt ce este un modul kernel, în ce constă și cum interacționează cu sistemul.

Modul de kernel încărcabil

Un modul de kernel încărcabil (să-l numim LKM - Loadable Kernel Module) este codul de program care rulează în spațiul kernel. Principala caracteristică a LKM este capacitatea de a încărca și descărca dinamic fără a fi nevoie să reporniți întregul sistem sau să recompilați nucleul.

Fiecare LKM constă din două funcții principale (minim):

  • funcția de inițializare a modulului. Apelat când LKM este încărcat în memorie:

int init_module (void) (...)

  • funcția de descărcare a modulului:

void cleanup_module (void) (...)

Să dăm un exemplu al celui mai simplu modul:

#define MODUL

#include

int init_module (void)

printk („Hello World”);

retur 0;

void cleanup_module (nul)

printk („Pa”);

Compilați și încărcați modulul. Modulul este încărcat în memorie prin comanda insmod:

gcc -c -O3 helloworld.c

insmod helloworld.o

Informațiile despre toate modulele încărcate în prezent în sistem se află în fișierul / proc / modules. Pentru a vă asigura că modulul este încărcat, introduceți cat / proc / modules sau lsmod. Comanda rmmod descarcă modulul:

rmmod helloworld

Algoritm de interceptare a apelurilor de sistem

Pentru a implementa un modul care interceptează un apel de sistem, este necesar să se definească un algoritm de interceptare. Algoritmul este după cum urmează:

  • salvați un pointer la apelul original (original) pentru a-l putea restabili;
  • creați o funcție care implementează un nou apel de sistem;
  • înlocuiți apelurile din tabelul de apeluri de sistem sys_call_table, adică configurați un pointer adecvat pentru un nou apel de sistem;
  • la sfârșitul lucrului (la descărcarea modulului), restaurați apelul de sistem original folosind indicatorul salvat anterior.

Puteți utiliza urmărirea pentru a afla ce apeluri de sistem sunt implicate în aplicația utilizatorului. Prin urmărire, puteți determina ce apel de sistem trebuie interceptat pentru a prelua controlul aplicației. Un exemplu de utilizare a programului de urmărire va fi discutat mai jos.

Acum avem suficiente informații pentru a începe examinarea exemplelor de implementare a modulelor care interceptează apelurile de sistem.

Exemple de interceptare a apelurilor de sistem

Prevenirea creării de directoare

Când este creat directorul, se apelează funcția kernel sys_mkdir. Parametrul este un șir care conține numele directorului de creat. Luați în considerare codul care interceptează apelul de sistem corespunzător.

#include

#include

#include

Exportăm tabelul de apeluri de sistem:

extern void * sys_call_table;

Să definim un pointer pentru a stoca apelul de sistem original:

int (* orig_mkdir) (const char * path);

Să creăm propriul nostru apel de sistem. Apelul nostru nu face nimic, doar returnează o valoare nulă:

int own_mkdir (const char * cale)

retur 0;

În timpul inițializării modulului, salvăm indicatorul în apelul original și înlocuim apelul de sistem:

int init_module ()

orig_mkdir = sys_call_table;

sys_call_table = own_mkdir; retur 0;

La descărcare, restabilim apelul original:

void cleanup_module ()

Sys_call_table = orig_mkdir;

Salvați codul în fișierul sys_mkdir_call.c. Pentru a obține modulul obiect, să creăm un Makefile cu următorul conținut:

CC = gcc

CFLAGS = -O3 -Wall -fomit-frame-pointer

sys_mkdir_call.o: sys_mkdir_call.c

$ (CC) -c $ (CFLAGS) $ (MODFLAGS) sys_mkdir_call.c

Utilizați comanda make pentru a crea un modul kernel. După ce l-am descărcat, să încercăm să creăm un director cu comanda mkdir. După cum puteți vedea, nu se întâmplă nimic. Comanda nu funcționează. Pentru a-i restabili operabilitatea, este suficient să descărcați modulul.

Împiedicați citirea fișierului

Pentru a citi un fișier, trebuie mai întâi să îl deschideți folosind funcția de deschidere. Este ușor de ghicit că această funcție corespunde apelului de sistem sys_open. Prin interceptarea acestuia, putem proteja fișierul de citire. Să luăm în considerare implementarea modulului interceptor.

#include

#include

#include

#include

#include

#include

#include

extern void * sys_call_table;

Pointer pentru a păstra apelul de sistem original:

int (* orig_open) (const char * nume de cale, int flag, mod int);

Primul parametru al funcției de deschidere este numele fișierului care trebuie deschis. Un nou apel de sistem ar trebui să compare acest parametru cu numele fișierului pe care dorim să îl protejăm. Dacă numele se potrivesc, va fi simulată o eroare de deschidere a fișierului. Noul nostru apel de sistem arată astfel:

int own_open (const char * nume de cale, int flag, mod int)

Puneți numele fișierului de deschis aici:

char * kernel_path;

Numele fișierului pe care dorim să îl protejăm:

char hide = "test.txt"

Alocați memoria și copiați numele fișierului pentru a-l deschide:

kernel_path = (char *) kmalloc (255, GFP_KERNEL);

copy_from_user (kernel_path, calea, 255);

Comparaţie:

if (strstr (kernel_path, (char *) & hide)! = NULL) (

Memoria liberă și returnează un cod de eroare dacă numele se potrivesc:

kfree (kernel_path);

retur -ENOENT;

altceva (

Dacă numele nu se potrivesc, apelăm apelul de sistem original pentru a efectua procedura standard de deschidere a fișierului:

kfree (kernel_path);

returnează orig_open (cale, steag, mod);

int init_module ()

orig_open = sys_call_table;

sys_call_table = own_open;

retur 0;

void cleanup_module ()

sys_call_table = orig_open;

Să salvăm codul în fișierul sys_open_call.c și să creăm un Makefile pentru a obține modulul obiect:

CC = gcc

CFLAGS = -O2 -Wall -fomit-frame-pointer

MODFLAGS = -D__KERNEL__ -DMODULE -I / usr / src / linux / include

sys_open_call.o: sys_open_call.c

$ (CC) -c $ (CFLAGS) $ (MODFLAGS) sys_open_call.c

În directorul curent, creați un fișier numit test.txt, încărcați modulul și introduceți comanda cat test.txt. Sistemul va informa despre absența unui fișier cu acest nume.

Sincer, acest tip de protecție este ușor de parcurs. Este suficient să redenumiți fișierul cu comanda mv și apoi să citiți conținutul acestuia.

Ascunderea unei intrări de fișiere într-un director

Determinați ce apel de sistem este responsabil pentru citirea conținutului directorului. Pentru a face acest lucru, să scriem un alt fragment de test care citește directorul curent:

/ * Fișier Dir.c * /

#include

#include

int main ()

DIR * d;

struct dirent * dp;

d = opendir (".");

dp = readdir (d);

Returnează 0;

Să luăm modulul executabil:

gcc -o dir dir.c

și urmăriți-l:

strace ./dir

Să fim atenți la penultima linie:

getdents (6, / * 4 intrări * /, 3933) = 72;

Conținutul directorului este citit de funcția getdents. Rezultatul este stocat ca o listă de structuri de tip struct dirent. Al doilea parametru al acestei funcții este un indicator către această listă. Funcția returnează lungimea tuturor intrărilor din director. În exemplul nostru, funcția getdents a determinat prezența a patru intrări în directorul curent - ".", ".." și a celor două fișiere ale noastre, modulul executabil și codul sursă. Toate intrările din director au o lungime de 72 de octeți. Informațiile despre fiecare înregistrare sunt stocate, așa cum am spus, în structura structurată. Suntem interesați de două domenii ale acestei structuri:

  • d_reclen- dimensiunea înregistrării;
  • d_name- Nume de fișier.

Pentru a ascunde o înregistrare de fișier (cu alte cuvinte, pentru ao face invizibilă), trebuie să interceptați apelul de sistem sys_getdents, să găsiți înregistrarea corespunzătoare în lista structurilor primite și să o ștergeți. Luați în considerare codul care efectuează această operațiune (autorul codului original este Michal Zalewski):

extern void * sys_call_table;

int (* orig_getdents) (u_int, struct dirent *, u_int);

Să ne definim apelul de sistem.

int own_getdents (u_int fd, struct dirent * dirp, u_int count)

unsigned int tmp, n;

int t;

Atribuirea variabilelor va fi prezentată mai jos. În plus, avem nevoie de structuri:

struct dirent * dirp2, * dirp3;

Numele fișierului pe care dorim să îl ascundem:

char hide = "fișierul nostru";

Să determinăm lungimea intrărilor din director:

tmp = (* orig_getdents) (fd, dirp, count);

dacă (tmp> 0) (

Alocați memorie pentru structură în spațiul kernel și copiați conținutul directorului în ea:

dirp2 = (struct dirent *) kmalloc (tmp, GFP_KERNEL);

copy_from_user (dirp2, dirp, tmp);

Să folosim a doua structură și să stocăm lungimea intrărilor în director:

dirp3 = dirp2;

t = tmp;

Să începem să căutăm fișierul nostru:

while (t> 0) (

Citim lungimea primei înregistrări și determinăm lungimea rămasă a înregistrărilor din director:

n = dirp3-> d_reclen;

t- = n;

Verificăm dacă numele fișierului din înregistrarea curentă nu se potrivește cu cel căutat:

if (strstr ((char *) & (dirp3-> d_name), (char *) & hide)! = NULL) (

Dacă da, suprascriem intrarea și calculăm noua valoare pentru lungimea intrărilor din director:

memcpy (dirp3, (char *) dirp3 + dirp3-> d_reclen, t);

tmp- = n;

Poziționăm indicatorul către următoarea înregistrare și continuăm căutarea:

dirp3 = (struct dirent *) ((char *) dirp3 + dirp3-> d_reclen);

Returnăm rezultatul și eliberăm memoria:

copy_to_user (dirp, dirp2, tmp);

kfree (dirp2);

Returnarea lungimii intrărilor din director:

retur tmp;

Funcțiile de inițializare și descărcare a modulului au o formă standard:

int init_module (void)

orig_getdents = sys_call_table;

sys_call_table = own_getdents;

retur 0;

void cleanup_module ()

sys_call_table = orig_getdents;

Să salvăm sursa în fișierul sys_call_getd.c și să creăm un Makefile cu următorul conținut:

CC = gcc

module = sys_call_getd.o

CFLAGS = -O3 -Perete

LINUX = / usr / src / linux

MODFLAGS = -D__KERNEL__ -DMODULE -I $ (LINUX) / include

sys_call_getd.o: sys_call_getd.c $ (CC) -c

$ (CFLAGS) $ (MODFLAGS) sys_call_getd.c

Creați fișierul nostru în directorul curent și încărcați modulul. Fișierul dispare, după cum este necesar.

După cum ați înțeles, nu este posibil să luați în considerare un exemplu de interceptare a fiecărui apel sistem în cadrul unui articol. Prin urmare, pentru cei care sunt interesați de acest număr, vă recomand să vizitați site-urile:

Acolo puteți găsi exemple mai complexe și mai interesante de interceptare a apelurilor de sistem. Scrieți toate comentariile și sugestiile pe forumul revistei.

La pregătirea articolului s-au folosit materiale de pe site