Familia 'Dirty': anatomia a trei vulnerabilități care au zguduit Linux

Unele bug-uri trăiesc ani de zile în cod fără să facă niciun rău. Altele, odată descoperite, rescriu istoria securității. Vulnerabilitățile din familia 'Dirty' aparțin celei de-a doua categorii: sunt elegante în simplitatea lor conceptuală, devastatoare ca impact și extrem de revelatoare despre cum funcționează cu adevărat un sistem de operare modern. Dacă ești developer și vrei să înțelegi de ce contează securitatea la nivel de sistem, aceste trei cazuri sunt un curs accelerat perfect.
Dirty COW (CVE-2016-5195): o cursă de nouă ani în inima kernel-ului
Dirty COW a fost dezvăluită în octombrie 2016, dar bug-ul exista în kernel-ul Linux de aproape nouă ani – din versiunea 2.6.22, lansată în 2007. Denumirea vine de la mecanismul pe care îl exploatează: Copy-On-Write (COW), o optimizare fundamentală a memoriei virtuale. Povestea ei este un exemplu perfect despre cum o condiție de cursă (race condition) poate transforma o funcție de optimizare inofensivă într-o armă de escaladare a privilegiilor.
Ce este Copy-On-Write și de ce există?
Când un proces creează un proces copil prin apelul de sistem fork(), copierea întregului spațiu de memorie ar fi costisitoare și, de multe ori, inutilă. Copy-On-Write rezolvă această problemă elegant: inițial, procesul părinte și cel copil împart aceleași pagini de memorie, marcate ca read-only. Abia în momentul în care unul dintre ele încearcă să scrie, kernel-ul creează o copie privată a paginii respective – de unde și numele. Această strategie economisește resurse semnificative și accelerează crearea proceselor.
Condiția de cursă: când doi pași devin trei
Dirty COW exploata o secvență de operații în subsistemul de gestionare a memoriei virtuale care implica doi pași: mai întâi, kernel-ul verifica dacă pagina poate fi scrisă (sau dacă trebuie creată o copie COW), apoi efectua scrierea. Problema: între acești doi pași, un alt thread putea interveni și elimina copia privată, forțând kernel-ul să revină la pagina originală – cea read-only. Prin repetarea rapidă a acestei secvențe în paralel, un atacator putea, în mod determinist, să câștige cursa și să scrie în pagini de memorie în care nu ar fi trebuit să aibă acces. Rezultatul: un utilizator neprivilegiat putea suprascrie fișiere read-only precum /etc/passwd sau binare setuid, obținând acces root. Un atacator local devenea, în câteva secunde, administrator de sistem.
Patch-ul și lecția
Fix-ul a fost livrat de Linus Torvalds personal, la doar câteva zile după dezvăluire. Soluția a presupus adăugarea unui mecanism de blocare suplimentar în funcția get_user_pages(), astfel încât secvența verificare-scriere să devină atomică – imposibil de întrerupt. Lecția tehnică fundamentală: oricând ai o operație care presupune doi pași separați (verifică, apoi acționează), trebuie să te întrebi cine ar putea interveni între cei doi pași. Acest principiu se aplică la fel de bine în codul de aplicație, nu doar în kernel.
Dirty Pipe (CVE-2022-0847): când buffere-le de pipe devin o ușă din spate
Descoperită și raportată de Max Kellermann în februarie 2022, Dirty Pipe a primit imediat comparații cu Dirty COW – și pe bună dreptate. Afecta kernel-ul Linux din versiunea 5.8 și permitea, din nou, unui utilizator neprivilegiat să suprascrie conținutul unor fișiere read-only. Dar mecanismul era complet diferit și, dacă e posibil, și mai elegant din punct de vedere tehnic.
Pipe-uri și splice: cum funcționează transferul de date în kernel
Un pipe în Unix este un canal de comunicare unidirecțional între procese. Intern, kernel-ul gestionează pipe-urile printr-un set de buffere circulare, fiecare buffer asociat unei pagini de memorie. Apelul de sistem splice() permite transferul de date direct între un descriptor de fișier și un pipe – fără a copia datele prin spațiul utilizator, o optimizare importantă pentru performanță. Fiecare buffer de pipe are un câmp de flags care indică, printre altele, dacă pagina asociată poate fi modificată sau dacă este partajată cu alte structuri (de exemplu, cache-ul de pagini al unui fișier).
Bug-ul: un flag neinițializat cu consecințe grave
Problema era surprinzător de simplă la origine: în funcția de pregătire a unui nou buffer de pipe, câmpul flags nu era inițializat corect. Prin urmare, putea moșteni valori reziduale din memorie, inclusiv flagul PIPE_BUF_FLAG_CAN_MERGE – care semnala kernel-ului că datele noi pot fi scrise direct în pagina existentă a buffer-ului, fără a crea o copie. Exploatarea era directă: un atacator crea un pipe, îl umplea și îl golea strategic pentru a manipula starea flags-urilor, apoi folosea splice() pentru a lega o pagină din cache-ul unui fișier read-only de pipe. Cu flagul PIPE_BUF_FLAG_CAN_MERGE activ pe acel buffer, scrierea ulterioară în pipe modifica direct pagina din cache – adică conținutul fișierului – ocolind complet verificările de permisiuni. Fișierele SUID, configurațiile critice, orice fișier citibil putea fi alterat.
Patch-ul: o linie de cod care a contat enorm
Fix-ul a fost, în esență, să te asiguri că flagul PIPE_BUF_FLAG_CAN_MERGE este explicit resetat atunci când un buffer de pipe este (re)folosit în contexte unde pagina provine din afara pipe-ului. O corecție minimală ca dimensiune, colosală ca impact. Lecția directă pentru orice developer: inițializați întotdeauna explicit variabilele și structurile de date. Valorile reziduale din memorie sunt o sursă constantă de bug-uri subtile și, uneori, de vulnerabilități critice. Nu presupuneți că memoria este curată.
PwnKit / Polkit (CVE-2021-4034): doisprezece ani ascuns în plain sight
Dacă Dirty COW și Dirty Pipe sunt vulnerabilități de kernel, PwnKit joacă într-o ligă ușor diferită: este o vulnerabilitate în pkexec, un utilitar din spațiul utilizator care face parte din Polkit (fostul PolicyKit). Descoperită de echipa Qualys și dezvăluită în ianuarie 2022, vulnerabilitatea exista în pkexec din prima sa versiune, din mai 2009 – deci peste douăsprezece ani în care orice sistem Linux cu Polkit instalat (practic orice distribuție majoră) era expus.
Ce face pkexec și de ce are privilegii ridicate
Polkit este un framework de autorizare care permite proceselor neprivilegiate să comunice cu procese privilegiate în mod controlat. pkexec este echivalentul sudo din ecosistemul Polkit: permite executarea unui program cu drepturi elevate, după verificarea politicilor de autorizare. Deoarece trebuie să poată schimba identitatea procesului la root, pkexec este un binar de tip setuid-root – adică rulează cu privilegii root indiferent de utilizatorul care îl lansează. Exact această caracteristică îl face o țintă valoroasă.
Vulnerabilitatea: confuzia dintre argc și argv
Bug-ul rezida în modul în care pkexec procesa argumentele din linia de comandă. În mod normal, argv[0] conține numele programului, argv[1] primul argument real și așa mai departe, iar argc indică numărul total de argumente. Codul din pkexec presupunea că argc este întotdeauna cel puțin 1. Dar în Linux, un proces poate fi lansat cu argc=0 – fără niciun argument – printr-un apel exec() special construit. În această situație, codul din pkexec accesa argv[1] pentru a citi primul argument, dar deoarece argc era 0, argv[1] nu exista în mod legitim. Dincolo de limita array-ului argv se afla envp – array-ul variabilelor de mediu. Prin manipularea variabilelor de mediu ale procesului, un atacator putea face pkexec să citească și să scrie date dintr-o locație controlată, injectând o variabilă de mediu periculoasă (cum ar fi LD_PRELOAD sau GCONV_PATH) care era apoi folosită de procesul cu privilegii root pentru a executa cod arbitrar.
Patch-ul și amploarea problemei
Patch-ul a adăugat o verificare explicită pentru argc == 0 la începutul funcției principale din pkexec, tratând această situație ca o eroare fatală. Simplu, direct, eficient. Dar amploarea impactului a fost remarcabilă: Ubuntu, Debian, Fedora, CentOS, Red Hat Enterprise Linux, openSUSE – toate erau vulnerabile. Qualys a confirmat exploatabilitatea pe toate distribuțiile majore testate. Faptul că bug-ul a stat ascuns doisprezece ani, în cod open-source auditat, este un memento puternic că securitatea prin obscuritate nu funcționează – dar nici simpla expunere publică a codului nu garantează că toți ochii văd tot.
Lecții transferabile pentru oricine scrie software
Cele trei vulnerabilități, deși distincte tehnic, converg spre un set de principii care se aplică la orice nivel al stivei software – de la kernel la aplicație web. Iată ce ar trebui să rețină orice developer:
- Principle of Least Privilege: niciun proces, utilizator sau componentă nu ar trebui să aibă mai multe drepturi decât are nevoie pentru a-și îndeplini funcția. pkexec rula ca root pentru că trebuia – dar suprafața de atac creată de binarele setuid este imensă și trebuie minimizată.
- Inițializarea explicită a memoriei: Dirty Pipe a apărut dintr-un câmp neinițializat. În C și C++, dar și în alte limbaje low-level, memoria neinițializată este o sursă constantă de bug-uri. Folosiți utilitare de analiză statică și sanitizere (AddressSanitizer, MemorySanitizer) pentru a detecta aceste probleme devreme.
- Atomicitate pentru operații check-then-act: dacă verifici o condiție și apoi acționezi pe baza ei, asigură-te că nimic nu poate schimba starea între cele două momente. Mutex-urile, operațiile atomice și tranzacțiile există tocmai pentru asta – și se aplică la fel în codul de aplicație, la accesul la fișiere sau la baze de date.
- Validarea tuturor input-urilor, inclusiv cele implicite: pkexec a eșuat să valideze o precondiție de bază (argc >= 1). Validați întotdeauna și starea mediului de execuție, nu doar datele pe care utilizatorul le introduce explicit.
- Patch-urile sunt critice și urgente: toate cele trei vulnerabilități au primit patch rapid după dezvăluire. Organizațiile care au întârziat aplicarea actualizărilor au rămas expuse. Un proces de patch management bine definit nu este opțional – este parte din igiena de bază a securității.
- Auditurile de securitate nu sunt suficiente singure: Dirty COW a stat 9 ani, PwnKit 12 ani în cod public. Chiar și cu revizuiri de cod, fuzzing și analiză statică, bug-urile subtile trec neobservate. Defence in depth – straturi multiple de protecție – rămâne singura strategie realistă.
Cele mai periculoase vulnerabilități nu sunt cele mai complexe. Sunt cele care se ascund în presupozițiile pe care nimeni nu le mai chestionează – un flag neinițializat, un array accesat fără să fie verificat, doi pași care par unul singur.