Baza de calcul de încredere: de ce contează dimensiunea
Discuțiile despre securitate se concentrează adesea pe buguri individuale:
- un buffer overflow
- o problemă de confused deputy
- un parser neverificat
- o cale de escaladare a privilegiilor
Aceste buguri contează, dar sunt simptome ale unei întrebări mai profunde:
Cât cod trebuie să fie corect pentru ca sistemul să rămână sigur?
Acest cod este baza de calcul de încredere, de obicei abreviată TCB.
Dimensiunea și forma TCB determină cât cod trebuie să fie de încredere, auditat, testat și înțeles. Un sistem poate avea abstracții puternice pe hârtie, dar dacă ele depind de o cantitate mare de cod privilegiat care trebuie să se comporte perfect, argumentul de securitate devine mult mai slab.
Această postare explică ce este TCB, de ce dimensiunea ei afectează suprafața de atac și cum EriX încearcă să mențină codul de încredere mic și explicit.
Ce este TCB?⌗
Baza de calcul de încredere este mulțimea componentelor a căror corectitudine este necesară pentru ca proprietățile de securitate ale sistemului să rămână valabile.
Dacă o componentă din TCB este compromisă, garanțiile de securitate ale sistemului pot să nu mai fie adevărate.
Într-un sistem de operare, TCB include adesea:
- bootloaderul
- kernelul
- servicii de sistem privilegiate
- logica de autentificare și autorizare
- parsere pentru date de boot de încredere
- cod de verificare criptografică
- cod care distribuie sau transferă autoritate
TCB exactă depinde de designul sistemului.
Într-un kernel monolitic tradițional, o mare parte din kernel este de obicei în TCB, deoarece multe subsisteme rulează cu privilegii complete de kernel. Un bug într-un driver, sistem de fișiere sau stack de rețea poate deveni o compromitere a kernelului.
Într-un sistem microkernel, TCB-ul kernelului poate fi mai mic, dar sistemul de încredere total include în continuare componentele care distribuie autoritate și aplică politica.
Această distincție contează.
Microkernelurile reduc cantitatea de cod care rulează cu autoritate completă asupra mașinii. Ele nu fac automat toate serviciile din spațiul utilizator neîncrezătoare implicit.
De încredere nu înseamnă sigur⌗
Cuvântul “de încredere” este ușor de înțeles greșit.
O componentă de încredere nu este o componentă despre care știm că este corectă.
Este o componentă de a cărei corectitudine depinde sistemul.
Aceasta este o definiție mai puțin confortabilă, dar utilă.
Dacă un serviciu este de încredere pentru distribuirea capabilităților, un bug în acel serviciu poate acorda autoritate incorect. Dacă un parser este de încredere pentru validarea unui executabil înainte de boot, un bug în parser poate submina lanțul de boot. Dacă un handler syscall al kernelului este de încredere pentru validarea drepturilor de endpoint, o verificare lipsă poate rupe izolarea.
Încrederea nu este laudă.
Încrederea este risc.
Scopul nu este să etichetăm cât mai mult cod ca fiind de încredere. Scopul este să facem setul de încredere cât mai mic, îngust și auditabil.
De ce contează dimensiunea⌗
Dimensiunea TCB contează din mai multe motive.
1. Mai mult cod înseamnă mai multe buguri⌗
Tot software-ul are buguri.
Pe măsură ce crește cantitatea de cod de încredere, crește și probabilitatea de buguri relevante pentru securitate. Acest lucru este valabil mai ales pentru cod care:
- parsează intrări neîncrezătoare
- gestionează memorie
- tratează concurența
- interpretează permisiuni
- traduce un model de autoritate în altul
Sistemele de operare conțin toate aceste tipare.
Reducerea dimensiunii TCB nu elimină bugurile, dar reduce cantitatea de cod în care un bug poate compromite întregul sistem.
2. Mai multe interfețe înseamnă suprafață de atac mai mare⌗
Suprafața de atac nu înseamnă doar linii de cod.
Înseamnă și puncte de intrare.
Fiecare interfață către cod de încredere este un loc unde un atacator poate furniza input:
- argumente syscall
- mesaje IPC
- metadate de imagine de boot
- headere ELF
- structuri de sisteme de fișiere
- descriptori de dispozitive
- evenimente de întrerupere
- tabele furnizate de firmware
Fiecare interfață are nevoie de validare.
O componentă de încredere mică, dar cu o interfață prost proiectată, poate fi periculoasă. Dar pe măsură ce numărul interfețelor de încredere crește, crește și povara validării.
3. Mai multă stare face raționamentul mai greu⌗
Eșecurile de securitate apar adesea nu pentru că lipsește o singură verificare, ci pentru că starea se schimbă într-o ordine neașteptată.
De exemplu:
- o capabilitate este copiată înainte ca drepturile să fie reduse
- un serviciu pornește înainte ca pachetul lui de startup să fie validat complet
- un slot local vechi este tratat ca dovadă de autoritate
- un dispozitiv este considerat prezent înainte ca descoperirea să se termine
- un proces păstrează autoritate după o cale de lansare eșuată
Cu cât un sistem are mai multă stare mutabilă de încredere, cu atât este mai greu de demonstrat că fiecare tranziție păstrează invariantele intenționate.
De aceea EriX pune accent pe startup determinist, înregistrări explicite de transfer și comportament fail-closed.
4. Mai mult privilegiu înseamnă rază de impact mai mare⌗
Același bug are consecințe diferite în funcție de locul unde apare.
Un bug de parsing într-o unealtă neprivilegiată poate prăbuși acea unealtă.
Un bug de parsing în bootloader poate compromite întregul sistem înainte ca kernelul să pornească.
Un bug într-un driver din spațiul utilizator cu acces doar la un interval I/O specific este serios, dar este diferit de un bug într-un driver care rulează în kernel cu autoritate completă asupra mașinii.
Reducerea TCB înseamnă și reducerea razei de impact a bugurilor individuale.
Kernelul este doar o parte din TCB⌗
Este tentant să spunem:
TCB este kernelul.
De obicei este prea simplu.
Kernelul este central, dar un sistem sigur depinde și de codul care pregătește kernelul, pornește prima sarcină din spațiul utilizator, definește formatele de autoritate și distribuie capabilități.
În EriX, kernelul este explicit în TCB.
El deține tabelele de capabilități din spațiul kernel, starea de planificare și resursele mașinii. Validează handoff-ul de la bootloader la kernel, creează sarcina root, gestionează obiecte de bază ale kernelului și expune puncte de intrare trap, syscall și întrerupere specifice arhitecturii.
Dacă kernelul nu aplică verificările de capabilități, drepturile de endpoint, limitele spațiilor de adrese sau ciclurile de viață ale obiectelor, modelul de izolare al sistemului eșuează.
Dar kernelul nu este toată povestea.
Codul de boot este și el de încredere⌗
Bootloaderul rulează înainte de kernel.
Asta îl face critic pentru securitate.
În EriX, bootloaderul este responsabil pentru încărcarea și verificarea unui
boot.img semnat, parsarea imaginilor de kernel și servicii, construirea unei
structuri de handoff deterministe și transferul controlului către kernel.
Acest lucru pune bootloaderul în TCB.
În timpul bootului, el deține autoritate furnizată de firmware și controlează săritura finală către kernel. Trebuie să trateze mediul de boot ca neîncrezător, să valideze structura și integritatea criptografică a imaginii, să respingă binare ELF malformate și să eșueze închis la ambiguitate.
Dacă bootloaderul acceptă o imagine modificată sau construiește un handoff inconsistent, kernelul poate începe execuția dintr-o bază compromisă.
De aceea codul de boot trebuie să fie mic, strict și plictisitor.
Parserele pot fi frontiere de TCB⌗
Parserele sunt adesea subestimate în securitatea sistemelor.
Ele stau exact în locul unde octeții neîncrezători devin structură de încredere.
EriX tratează mai multe crate-uri de parsing și ABI ca fiind în TCB sau adiacente TCB:
lib-bootimgparsează și verifică structura, hashurile și semnăturileboot.imglib-elfvalidează binare ELF64 înainte ca bootloaderul să aibă încredere în segmentele de încărcarelib-handoffvalidează structuri de handoff versionate între etape de bootlib-ipcdefinește și validează layouturi de mesaje IPClib-capabidefinește tipuri de capabilități, drepturi, constante de slot și descriptori de transfer
Aceste biblioteci pot să nu dețină capabilități runtime.
Asta nu le face irelevante pentru TCB.
Dacă lib-bootimg acceptă o imagine de boot modificată, bootloaderul poate avea
încredere în cod pe care ar trebui să îl respingă. Dacă lib-elf acceptă un
executabil malformat, lanțul de boot poate încărca octeți greșiți sau se poate
baza pe intervale de segment invalide. Dacă lib-ipc decodează greșit un mesaj,
o operație poate fi interpretată incorect. Dacă lib-capabi definește o
politică de rol prea largă, un serviciu poate primi autoritate pe care nu ar
trebui să o aibă niciodată.
Codul pur poate fi tot cod de încredere.
Proprietatea importantă este că aceste biblioteci sunt înguste. Nu fac I/O, nu dețin politica sistemului și evită autoritatea ambientală. Rolul lor este să parseze, să valideze și să respingă.
Serviciile din spațiul utilizator pot fi componente de încredere⌗
Mutarea politicii în afara kernelului nu face politica să dispară.
O mută în servicii din spațiul utilizator, unde poate fi izolată, constrânsă și auditată separat.
În EriX, rootd este prima autoritate din spațiul utilizator care poartă
politică. Validează handoff-ul de la kernel la root, parsează configurația de
boot, execută DAG-ul de startup și transferă capabilități de privilegiu minim
către serviciile necesare.
rootd are privilegii mari.
Dar nu este kernelul.
Această distincție este importantă. rootd este de încredere pentru politica
inițială și distribuția de capabilități, dar nu implementează obiecte de kernel
și nu deține direct autoritatea mașinii. Rolul lui este să distribuie autoritate
conform contractelor explicite de startup.
Alte servicii stau și ele în frontiere de încredere specifice:
procdeste de încredere pentru orchestrarea ciclului de viață al proceselordevicedeste de încredere pentru politica driverelor și livrarea capabilităților de drivervfsdeste de încredere ca frontieră a spațiului de nume public al sistemului de fișiere- furnizorii privați de sisteme de fișiere sunt de încredere doar pentru rolul lor de furnizor
Asta nu face aceste servicii neimportante.
Face autoritatea lor mai îngustă decât autoritatea completă a kernelului.
Suprafața de atac într-un design monolitic⌗
Într-un kernel monolitic, multe subsisteme împart un singur spațiu de adrese privilegiat.
Acest lucru poate simplifica căile rapide, dar creează și o suprafață de atac largă.
O vulnerabilitate în orice subsistem din kernel poate deveni o vulnerabilitate a kernelului:
- metadate de sistem de fișiere malformate
- tratare defectuoasă a pachetelor de rețea
- cod de driver nesigur
- descriptori hardware neașteptați
- condiții de cursă în starea partajată a kernelului
Atacatorul are nevoie de o singură cale către cod privilegiat.
Asta nu înseamnă că kernelurile monolitice nu pot fi sigure. Pot fi proiectate, întărite, fuzzate, sandboxate și auditate extensiv.
Dar arhitectura începe cu o suprafață privilegiată mare.
Argumentul de securitate trebuie apoi să explice cum este controlată acea suprafață mare.
Suprafața de atac într-un design microkernel⌗
Un microkernel schimbă forma suprafeței de atac.
Kernelul încă expune interfețe critice:
- syscalls
- livrare IPC
- planificare
- operații pe spații de adrese
- operații cu capabilități
- tratarea întreruperilor
Aceste interfețe trebuie să fie corecte.
Dar serviciile de nivel mai înalt nu rulează automat cu privilegiu complet de kernel. Dacă un furnizor de sistem de fișiere tratează medii malformate, scopul este ca bugul să rămână în autoritatea acelui furnizor. Dacă un driver eșuează, scopul este să eșueze doar cu autoritatea de dispozitiv primită explicit.
Aceasta transformă o suprafață privilegiată mare în mai multe suprafețe de autoritate mai mici.
Nu este automat mai simplu.
Funcționează doar dacă frontierele sunt stricte și autoritatea transmisă prin ele este îngustă.
Cum minimizează EriX TCB⌗
EriX reduce dimensiunea TCB și suprafața de atac prin mai multe alegeri de design.
1. Un kernel minim în politică⌗
Kernelul EriX este responsabil pentru mecanisme, nu pentru politica sistemului.
Gestionează:
- obiecte de bază ale kernelului
- semantica capabilităților
- planificarea și execuția taskurilor
- primitive pentru spații de adrese
- IPC și dispatch de endpointuri
- puncte de intrare pentru întreruperi și excepții
Nu deține:
- politica de startup a serviciilor
- politica de orchestrare a proceselor
- politica spațiului de nume al sistemelor de fișiere
- politica de activare a driverelor
- politica de alocare a memoriei de nivel înalt
Acest lucru păstrează kernelul concentrat pe aplicarea izolării, nu pe forma întregului sistem.
2. Capabilități explicite în locul autorității ambientale⌗
EriX modelează autoritatea prin capabilități.
O componentă poate acționa doar dacă deține o capabilitate cu drepturile cerute. Kernelul validează referințele de capabilitate la utilizare, aplică drepturile endpointurilor la dispatch syscall și tratează transferurile ca evenimente explicite.
Astfel se evită dependența de nume globale sau numere convenționale de slot ca permisiune.
Cunoașterea unui număr de slot nu este autoritate.
Deținerea capabilității corecte în CSpace local este autoritate.
Această distincție este centrală pentru reducerea TCB: codul de încredere nu trebuie să deducă permisiuni din stare globală când autoritatea este purtată explicit.
3. Endpointuri înguste de control kernel⌗
Designurile mai vechi concentrează adesea controlul în spatele unor interfețe privilegiate largi.
EriX merge în direcția opusă.
Runtime-ul actual folosește familii înguste de endpointuri de control kernel pentru sarcini specifice:
- timp
- întreruperi
- hotplug
- configurație PCI
- consolă și framebuffer
- I/O COM1
- I/O i8042
- retyping de memorie
- mapare VSpace
- rezolvarea faulturilor pagerului
- control de procese
- citiri ACPI
Bootul normal de runtime nu dă serviciilor un endpoint root larg pentru toate operațiile kernelului.
În schimb, fiecare serviciu primește familia de endpoint de care are nevoie.
timed primește controlul timpului. irqd primește controlul întreruperilor.
drv-serial primește I/O specific COM1. drv-i8042 primește I/O specific
i8042. drv-acpi primește autoritate de citire ACPI.
Aceasta îngustează atât interfața de încredere, cât și daunele produse de utilizare greșită.
4. Contracte exacte de startup⌗
EriX tratează transferul capabilităților de startup ca un contract, nu ca o sugestie.
Envelopele de startup descriu ce capabilități trebuie să primească un serviciu, unde trebuie să apară și ce drepturi trebuie să poarte.
Serviciile validează sloturile locale reale primite înainte de a declara readiness. Transferurile de endpointuri sunt verificate după slot sursă, slot destinație, drepturi și tip de endpoint așteptat. Transferurile necunoscute, greșite sau suplimentare sunt respinse.
Aceasta previne un bug comun de autoritate:
“Un slot este ocupat, deci trebuie să fie obiectul corect.”
În EriX, ocuparea slotului singură nu este dovadă de autoritate.
Capabilitatea trebuie să corespundă formei de autoritate declarate.
5. Crearea proceselor în etape⌗
Crearea proceselor este o operație de risc ridicat deoarece combină execuție, spații de adrese, endpointuri și autoritate inițială.
EriX rutează crearea proceselor la runtime prin creare de copii în etape, nu prin operația directă moștenită.
Fluxul pe etape este explicit:
- creează un copil în etapă
- primește un grant de instalare și un alias de endpoint al copilului
- instalează pachetul declarat de capabilități de startup
- pornește procesul doar după ce popularea este completă
Kernelul refuză pornirea procesului cât timp granturi de instalare vii încă țintesc acea etapă de copil.
Astfel procesele parțial populate nu devin executabile cu stare de autoritate ambiguă.
6. Autoritate de driver specifică rolului⌗
Driverele sunt o sursă majoră de risc în sistemele de operare.
EriX nu tratează tot controlul hardware ca o singură permisiune largă.
Autoritatea driverelor este specifică rolului:
drv-serialprimește autoritate I/O doar pentru COM1drv-i8042primește autoritate I/O doar pentru i8042drv-acpiprimește autoritate de citire ACPIprobedprimește autoritate de citire a configurației PCIdrv-virtio-blockprimește un frame de dispozitiv validat, nu autoritate generală de memorie
deviced gestionează politica driverelor, dar nu dă fiecărui driver o
capabilitate generică de control al dispozitivelor. Folosește instalare
explicită de capabilități de startup și suprafețe de autoritate specifice
rolului.
Aceasta limitează impactul bugurilor de driver asupra TCB.
7. Memoria de dispozitiv este un tip separat de capabilitate⌗
Memoria de dispozitiv este periculoasă deoarece poate afecta direct starea hardware.
EriX distinge memoria de dispozitiv de RAM obișnuită cu
CAP_TYPE_DEVICE_FRAME.
Calea de stocare poate deriva un frame MMIO bazat pe BAR pentru deviced, iar
deviced poate instala doar acel frame derivat în pachetul de startup al unui
driver.
Frame-ul de dispozitiv nu este tratat ca memorie obișnuită alocabilă.
Acest lucru contează deoarece confundarea memoriei de dispozitiv cu frame-uri normale ar lărgi modelul de autoritate și ar face mai dificil raționamentul despre siguranța memoriei.
8. Dependențe clean-room⌗
Dependențele pot mări TCB în tăcere.
Dacă codul de încredere depinde de o bibliotecă externă generală, sistemul poate moșteni:
- căi de cod de care nu are nevoie
- presupuneri care nu se potrivesc OS-ului
- comportament de parsing prea permisiv
- risc de update și supply chain
EriX evită crate-urile terțe și implementează bibliotecile critice în proiect.
Aceasta crește munca de implementare, dar menține vizibilă frontiera de încredere. Când un parser, crate ABI sau helper criptografic face parte din calea de boot sau autoritate, face parte din suprafața de review a sistemului.
9. Comportament fail-closed⌗
O TCB mică nu este suficientă dacă eșecurile sunt ambigue.
EriX încearcă să facă eșecurile sensibile pentru securitate explicite și terminale:
- handoff de boot malformat oprește bootul
- versiunile nesuportate sunt respinse
- autoritatea de startup invalidă împiedică readiness
- tipurile greșite de endpoint eșuează validarea
- eșecul unui serviciu necesar oprește progresia
- eșecurile după start declanșează teardown fail-closed
Eșecul închis este important deoarece euristicile de recuperare devin adesea politică ascunsă.
Politica ascunsă extinde comportamentul de încredere al sistemului.
Mai mic nu înseamnă trivial⌗
Reducerea TCB nu face designul sistemului ușor.
Adesea îl face mai explicit.
În loc să pună toată logica într-un singur spațiu de adrese privilegiat, sistemul trebuie să definească:
- ce componentă deține fiecare decizie
- ce autoritate primește fiecare componentă
- cum este transferată autoritatea
- cum sunt raportate eșecurile
- cum este curățat un startup parțial
- cum sunt invalidate sau eliminate capabilitățile vechi
Este mai multă muncă la început.
Dar produce un sistem în care argumentul de securitate este mai ușor de inspectat.
Întrebarea devine:
Ce componentă trebuie să fie de încredere pentru această proprietate specifică?
Este o întrebare mai bună decât:
Este corect întregul kernel?
O vedere practică a TCB în EriX⌗
O vedere practică a TCB în EriX arată stratificat.
La bază este lanțul de boot:
- bootloader
- verificarea imaginii de boot
- calea read/verify din
lib-bootimg - parsing ELF
- validare handoff
Apoi kernelul:
- obiecte de capabilități
- aplicarea CSpace și VSpace
- IPC și drepturi de endpoint
- planificare și tratare trap
- livrare întreruperi
Apoi spațiul utilizator de încredere timpuriu:
rootdpentru politica de startup și distribuția de capabilitățiprocdpentru ciclul de viață al proceselordevicedpentru politica driverelor- biblioteci selectate de ABI și validare partajate între servicii
Apoi servicii de încredere mai înguste:
- medierea namespace-ului sistemului de fișiere în
vfsd - furnizori backend privați
- servicii de input, consolă, logging, block, timp și întreruperi
Nu toate aceste componente sunt la fel de privilegiate.
Acesta este scopul.
EriX încearcă să evite o singură lume plată de încredere. Fiecare componentă ar trebui să fie de încredere doar pentru rolul său documentat și doar cu capabilitățile primite explicit.
Privind înainte⌗
Discuția despre TCB duce natural la limbajul de implementare.
Dacă codul de încredere trebuie să fie mic, explicit și auditabil, atunci
siguranța memoriei contează. Contează și frontierele unsafe, corectitudinea
parserelor, layoutul datelor și disciplina necesară aproape de hardware.
Următoarea postare va examina de ce EriX este scris în principal în Rust, ce rezolvă și ce nu rezolvă Rust pentru dezvoltarea de kernel și cum se compară cu abordarea tradițională în C pentru programarea de sisteme.