Linux

Linux ma znacznie większą powierzchnię ataku, niż myślisz. Co naprawdę chroni kernel przed procesem użytkownika

Linux ma znacznie większą powierzchnię ataku, niż myślisz. Co naprawdę chroni kernel przed procesem użytkownika

Linux często uchodzi za system, w którym bezpieczeństwo można sprowadzić do prostego modelu:

użytkownik → uprawnienia → proces → kernel

To już dawno przestało wystarczać.

Współczesny Linux posiada ogromną liczbę interfejsów, przez które przestrzeń użytkownika komunikuje się z jądrem. Są wśród nich syscall’e, /proc, /sys, debugfs, perf, mechanizmy tracingu, namespaces, capabilities, eBPF, urządzenia znakowe, sockety, interfejsy sieciowe oraz dziesiątki subsystemów kernela.

Każdy taki interfejs jest potencjalnym wejściem do kodu wykonywanego z uprawnieniami kernela.

I tutaj zaczyna się prawdziwa historia bezpieczeństwa Linuksa.

Nie chodzi wyłącznie o to, czy użytkownik jest root.

Chodzi o to:

ile kodu kernela może osiągnąć proces, który nie jest rootem?

Kernel nie jest magiczną czarną skrzynką

Proces użytkownika działa w przestrzeni adresowej odseparowanej od kernela.

To jedna z podstawowych granic bezpieczeństwa systemu.

Proces nie powinien móc po prostu zapisać czegoś do pamięci kernela:

                USER SPACE
        ┌──────────────────────┐
        │ application          │
        │ libraries            │
        │ runtime              │
        │ shell                │
        └──────────┬───────────┘
                   │
             syscalls / APIs
                   │
        ┌──────────▼───────────┐
        │       KERNEL         │
        │                      │
        │ VFS                  │
        │ networking           │
        │ drivers              │
        │ memory management    │
        │ filesystems          │
        │ namespaces           │
        │ security             │
        └──────────┬───────────┘
                   │
        ┌──────────▼───────────┐
        │ hardware             │
        └──────────────────────┘

Problem polega na tym, że kernel musi udostępniać użytkownikowi mechanizmy pozwalające korzystać z systemu.

Program musi otworzyć plik.

Musi utworzyć socket.

Musi przydzielić pamięć.

Musi utworzyć proces.

Musi komunikować się z urządzeniem.

Musi korzystać z sieci.

Musi montować system plików, jeśli posiada odpowiednie uprawnienia.

Każda taka operacja oznacza wejście do kodu kernela.

Dlatego powierzchnia ataku Linuxa nie jest równa liczbie uruchomionych aplikacji.

Jest znacznie większa.

To między innymi liczba dostępnych ścieżek prowadzących z userspace do kodu kernela.

Syscall nie jest tylko „funkcją systemową”

Każdy syscall jest punktem wejścia.

Przykładowo:

openat()
read()
write()
mmap()
clone()
execve()
socket()
ioctl()
mount()
ptrace()
bpf()
perf_event_open()

Nie wszystkie są dostępne dla każdego procesu w takim samym zakresie.

Nie wszystkie są równie niebezpieczne.

Ale każdy z nich może prowadzić do dużych fragmentów kodu kernela.

W praktyce oznacza to prostą zależność:

więcej dostępnych syscalli
        ↓
większa liczba osiągalnych ścieżek kernela
        ↓
większa powierzchnia ataku

To właśnie jeden z powodów istnienia seccomp.

Kernel opisuje seccomp jako mechanizm pozwalający ograniczyć liczbę kernel entry points dostępnych dla procesu. Nie naprawia on błędnego syscalla. Po prostu sprawia, że określony proces nie może go wywołać.

To ogromna różnica.

Seccomp nie zabezpiecza kernela. Odcina drogę do niego.

Załóżmy, że aplikacja webowa potrzebuje:

read
write
openat
close
mmap
munmap
socket
connect
recvfrom
sendto

Nie potrzebuje natomiast:

mount
ptrace
bpf
reboot
init_module
kexec_load

Nie ma powodu, żeby proces webowy miał dostęp do całej powierzchni system calli.

Można więc stworzyć model:

                    KERNEL
                       │
        ┌──────────────┼──────────────┐
        │              │              │
      files          network        process
        │              │              │
        └───────┬──────┴──────┬───────┘
                │             │
             seccomp       LSM
                │             │
                └──────┬──────┘
                       │
                 APPLICATION

Seccomp odpowiada przede wszystkim na pytanie:

„Do których syscalli ten proces w ogóle może wejść?”

LSM odpowiada na inne pytanie:

„Jeżeli już wszedł do kernela, czy ta konkretna operacja na konkretnym obiekcie jest dozwolona?”

Capabilities odpowiadają jeszcze inaczej:

„Czy proces posiada wymagane uprzywilejowanie?”

Namespaces:

„W jakim zakresie zasobów systemu ten proces w ogóle operuje?”

To są różne mechanizmy.

I właśnie dlatego jeden z nich nigdy nie powinien być traktowany jako pełny zamiennik pozostałych.

Capabilities: root został rozbity na kawałki

Tradycyjny model Unix wyglądał prosto:

UID != 0  → ograniczony
UID == 0  → root

Linux rozbił część tego modelu na capabilities.

Zamiast jednego ogromnego przełącznika istnieją mniejsze uprawnienia.

Przykłady:

CAP_NET_ADMIN
CAP_NET_RAW
CAP_SYS_PTRACE
CAP_SYS_ADMIN
CAP_SYS_MODULE
CAP_SYS_RAWIO
CAP_PERFMON

To znacznie bardziej precyzyjny model.

Ale jest tutaj jeden szczególnie niebezpieczny element:

CAP_SYS_ADMIN

Problem z CAP_SYS_ADMIN nie polega na tym, że jest „rootem”.

Problem polega na jego ogromnym zakresie.

Jeżeli aplikacja dostaje CAP_SYS_ADMIN, administrator często daje jej dostęp do bardzo dużej części uprzywilejowanych operacji.

W kontenerach jest to szczególnie istotne.

Konfiguracja:

--privileged

nie jest niewinnym przełącznikiem „żeby aplikacja działała”.

To może oznaczać bardzo poważne zmniejszenie izolacji.

User namespace: root, który nie jest rootem

Tutaj Linux robi coś, co na pierwszy rzut oka wygląda absurdalnie.

Zwykły użytkownik może utworzyć user namespace i otrzymać w nim zestaw capabilities.

Czyli:

HOST
│
├── UID 1000
│
│   └── USER NAMESPACE
│       └── UID 0
│           ├── CAP_SYS_ADMIN
│           ├── CAP_NET_ADMIN
│           └── ...

Czy użytkownik właśnie został rootem całego systemu?

Nie.

Capabilities obowiązują w określonym namespace.

Kernel specjalnie ogranicza działania, które mogłyby naruszyć initial user namespace.

To niezwykle ważna granica.

Jednocześnie user namespaces zwiększają złożoność kernela.

Kernel musi teraz poprawnie obsługiwać kombinację:

UID
+
capabilities
+
user namespace
+
inne namespaces
+
LSM
+
obiekt kernela

Im więcej takich kombinacji, tym większa złożoność ścieżek bezpieczeństwa.

A złożoność jest jednym z naturalnych wrogów bezpieczeństwa.

/proc nie jest zwykłym katalogiem

Jednym z największych błędów początkujących administratorów jest traktowanie:

/proc

jak zwykłego systemu plików.

Nie jest nim.

procfs jest interfejsem do informacji i mechanizmów związanych z procesami oraz stanem kernela.

Przykładowo:

cat /proc/cpuinfo
cat /proc/meminfo
cat /proc/cmdline
cat /proc/mounts
cat /proc/sys/kernel/hostname

Niektóre informacje są całkowicie nieszkodliwe.

Inne mogą ujawniać szczegóły środowiska.

Jeszcze inne zależą od uprawnień, namespace albo konfiguracji kernela.

Dlatego bezpieczeństwo /proc nie polega na prostym pytaniu:

„Czy użytkownik ma dostęp do /proc?”

Prawidłowe pytanie brzmi:

„Jakie informacje i operacje może zobaczyć lub wykonać ten konkretny proces?”

To dużo bardziej zaawansowany model.

debugfs: interfejs, którego nie należy traktować jak produkcyjnego API

Kernel ma ogromną liczbę mechanizmów diagnostycznych.

Dla administratora i developera są bezcenne.

Dla atakującego mogą być równie interesujące.

Linux Kernel Threat Model wskazuje debugfs jako interfejs, który nie jest przeznaczony do dostępu przez nieuprzywilejowanych użytkowników.

To ma sens.

Mechanizmy debugowania często powstają po to, żeby pokazać stan wewnętrzny systemu.

A stan wewnętrzny kernela jest dokładnie tym, czego atakujący chciałby się dowiedzieć.

W bezpieczeństwie istnieje prosta zasada:

diagnostyka ≠ automatycznie bezpieczny interfejs

Jeżeli serwer produkcyjny nie potrzebuje określonego mechanizmu debugowania, nie ma powodu wystawiać go bez kontroli.

 

perf: wydajność kontra powierzchnia ataku

perf jest fantastycznym narzędziem.

Administrator może analizować:

  • CPU,
  • cache,
  • scheduler,
  • procesy,
  • funkcje kernela,
  • zdarzenia sprzętowe,
  • wydajność aplikacji.

Ale obserwacja systemu na niskim poziomie oznacza dostęp do bardzo szczegółowych informacji.

Dlatego kernel posiada mechanizmy kontroli dostępu dla perf.

Istnieje także capability:

CAP_PERFMON

przeznaczona właśnie do operacji związanych z performance monitoring i observability.

To dobry przykład szerszego problemu:

narzędzie stworzone dla administratora może jednocześnie zwiększać powierzchnię informacyjną dostępną procesowi.

Nie oznacza to, że perf jest „niebezpieczny”.

Oznacza to, że dostęp do niego powinien odpowiadać modelowi zaufania.

LSM działa głębiej niż klasyczne UNIX permissions

Tradycyjne DAC:

UID
GID
mode bits
ACL

to tylko jedna warstwa.

Linux Security Modules pozwalają dodać kolejne mechanizmy kontroli dostępu.

Najbardziej znane:

SELinux
AppArmor
Smack
Tomoyo

LSM działa poprzez hooki w krytycznych miejscach kernela.

W praktyce może więc dojść do sytuacji:

UNIX permissions → ALLOW
        ↓
capability       → ALLOW
        ↓
LSM              → DENY

I operacja zostaje zablokowana.

To właśnie jest sens Mandatory Access Control.

Nie pytamy tylko:

„Czy użytkownik ma prawo?”

Pytamy:

„Czy ten konkretny proces, w tym konkretnym kontekście, może wykonać tę operację?”

Kernel udostępnia także /sys/kernel/security/lsm, dzięki któremu można sprawdzić aktywne moduły LSM.

Przykład:

cat /sys/kernel/security/lsm

Na różnych dystrybucjach wynik będzie inny.

I to jest ważne.

Nie można zakładać, że każdy Linux ma identyczny model bezpieczeństwa.

Najlepszy model to warstwy

Dobry Linux security architecture nie wygląda tak:

SELinux = bezpieczeństwo

ani:

Seccomp = bezpieczeństwo

ani:

Docker = izolacja

Powinien wyglądać bardziej tak:

                 APPLICATION
                      │
              ┌───────▼───────┐
              │   seccomp     │
              └───────┬───────┘
                      │
              ┌───────▼───────┐
              │ capabilities   │
              └───────┬───────┘
                      │
              ┌───────▼───────┐
              │  namespaces   │
              └───────┬───────┘
                      │
              ┌───────▼───────┐
              │      LSM      │
              └───────┬───────┘
                      │
              ┌───────▼───────┐
              │     KERNEL    │
              └───────────────┘

Każda warstwa rozwiązuje inny problem.

Co się dzieje, gdy jedna warstwa zawiedzie?

To najciekawsza część całego modelu.

Załóżmy:

Aplikacja
   ↓
podatny komponent
   ↓
próba wykorzystania syscalla

Seccomp może zablokować syscall.

Jeżeli syscall jest dozwolony, capabilities mogą ograniczyć operację.

Jeżeli capability istnieje, LSM może odmówić dostępu.

Jeżeli LSM zezwoli, namespace może ograniczyć zakres obiektu.

Jeżeli również ta granica zostanie przekroczona, pozostają mechanizmy samoobrony kernela.

To jest właśnie defense in depth na poziomie kernela.

Nie zakładamy, że jedna kontrola nigdy nie zawiedzie.

Zakładamy, że wcześniej czy później jakaś kontrola zawiedzie — i projektujemy system tak, aby następna nadal działała.

Kernel Self-Protection: ostatnia linia obrony

Linux posiada cały zestaw mechanizmów mających utrudnić wykorzystanie błędów w samym jądrze.

Wśród nich znajdują się między innymi:

  • izolacja pamięci userspace i kernela,
  • SMEP/SMAP na x86,
  • odpowiedniki ochrony pamięci na ARM,
  • ograniczanie zapisu do pamięci kernela,
  • KASLR,
  • kontrola dostępu do informacji o adresach kernela,
  • inicjalizacja pamięci,
  • memory poisoning,
  • ograniczanie interfejsów wejściowych,
  • seccomp,
  • ograniczenia ładowania modułów.

Kernel Security Self-Protection mówi o tym wprost: podstawową metodą ograniczania skutków exploitów jest redukcja obszaru kernela, który może być osiągnięty przez userspace.

To bardzo ważna zmiana sposobu myślenia.

Nie próbujemy wyłącznie sprawić, żeby bugów nie było.

Projektujemy kernel tak, żeby nawet przy istnieniu błędu jego wykorzystanie było trudniejsze.

Najgorszy administrator to administrator z CAP_SYS_ADMIN

To oczywiście uproszczenie.

Ale bardzo dobre jako zasada projektowa.

Jeżeli aplikacja potrzebuje:

CAP_SYS_ADMIN
CAP_SYS_PTRACE
CAP_SYS_RAWIO
CAP_SYS_MODULE
CAP_NET_ADMIN

zatrzymaj się i zapytaj:

dlaczego?

Nie:

„Czy aplikacja działa?”

Tylko:

„Jaki dokładnie kernel operation wymaga tego uprawnienia?”

To fundamentalna różnica między administracją systemem a projektowaniem jego bezpieczeństwa.

Praktyczny audyt powierzchni ataku

Na serwerze warto zacząć od zebrania informacji.

Kernel

uname -a
cat /proc/version

Aktywne LSM

cat /sys/kernel/security/lsm

Capabilities procesu

grep Cap /proc/$$/status

Do czytelnego dekodowania można wykorzystać:

capsh --print

Namespaces

ls -l /proc/$$/ns/

Mounted filesystems

findmnt

Seccomp

grep Seccomp /proc/$$/status

Parametry kernela

sysctl -a

Sprawdzenie debugfs

mount | grep debugfs

Perf

Na systemach, gdzie perf jest zainstalowany:

perf version

Nie chodzi o uruchomienie wszystkich możliwych narzędzi.

Chodzi o zbudowanie mapy:

co istnieje?
       ↓
co jest dostępne?
       ↓
dla kogo?
       ↓
jakie capability są wymagane?
       ↓
czy LSM dodatkowo ogranicza dostęp?
       ↓
czy aplikacja rzeczywiście tego potrzebuje?

To jest prawdziwy kernel attack-surface assessment.

Szczególnie ważne w kontenerach

Kontener nie ma własnego kernela.

To zdanie powinno wisieć nad każdym klastrem produkcyjnym.

Docker może izolować:

filesystem
process tree
network
IPC
hostname
mounts
users

ale wszystkie kontenery nadal korzystają z kernela hosta.

Jeżeli więc podatność znajduje się w kernel subsystemie, granica kontenera nie jest tym samym co granica maszyny wirtualnej.

Dlatego bezpieczeństwo kontenera powinno obejmować:

namespaces
+
capabilities
+
seccomp
+
LSM
+
cgroups
+
read-only filesystem
+
minimal devices

A przede wszystkim:

nie dawaj aplikacji więcej dostępu do kernela niż absolutnie potrzebuje.

--privileged powinno zapalać czerwoną lampkę

Jeżeli kontener wymaga:

--privileged

administrator powinien potraktować to jako wyjątek wymagający uzasadnienia.

Nie jako standardową metodę naprawiania problemów z uprawnieniami.

W środowisku produkcyjnym znacznie lepszy model wygląda tak:

container
   │
   ├── minimal capabilities
   ├── restricted seccomp
   ├── LSM profile
   ├── restricted devices
   ├── read-only mounts
   └── no unnecessary host interfaces

Im mniej interfejsów hosta widzi kontener, tym mniej kodu kernela może potencjalnie osiągnąć.

Najbardziej niedoceniana zasada bezpieczeństwa Linuxa

Administratorzy często pytają:

„Czy ten użytkownik jest rootem?”

Znacznie lepsze pytanie brzmi:

„Jakie ścieżki do kernela są dostępne dla tego procesu?”

To zupełnie inna perspektywa.

Proces może być:

UID 1000

a mimo tego posiadać niepotrzebne capabilities.

Może być:

UID 0

wewnątrz user namespace.

Może mieć dostęp do określonych urządzeń.

Może mieć szeroki zakres syscalli.

Może mieć dostęp do mechanizmów tracingu.

Może działać bez odpowiedniego profilu LSM.

Może korzystać z niepotrzebnych interfejsów kernela.

Sam UID nie opisuje więc całej granicy bezpieczeństwa.

Linux security to zarządzanie powierzchnią kernela

Najważniejsza rzecz, którą warto z tego zapamiętać, jest prosta:

bezpieczeństwo Linuxa nie polega na tym, żeby proces nie był rootem.

Polega na tym, żeby proces nie mógł wykonać operacji, których nie potrzebuje.

To oznacza:

minimalne capabilities
        +
namespaces
        +
seccomp
        +
LSM
        +
ograniczony dostęp do /proc i interfejsów diagnostycznych
        +
kontrola urządzeń
        +
aktualny kernel
        +
kernel self-protection

Każda warstwa ogranicza inną część problemu.

I właśnie dlatego Linux może być bardzo bezpieczną platformą — ale tylko wtedy, gdy administrator traktuje kernel jako ogromną powierzchnię ataku, a nie jako magiczną warstwę, która automatycznie chroni wszystko znajdujące się nad nią.

Podsumowanie

Najgroźniejszym błędem w administracji Linuxem jest myślenie kategoriami:

root / non-root

Współczesny Linux jest znacznie bardziej skomplikowany.

Prawdziwy model bezpieczeństwa wygląda raczej tak:

                 USER PROCESS
                      │
        ┌─────────────┼─────────────┐
        │             │             │
    syscalls      /proc,/sys    devices
        │             │             │
        └─────────────┼─────────────┘
                      │
               capabilities
                      │
                 namespaces
                      │
                   seccomp
                      │
                     LSM
                      │
              KERNEL SELF-PROTECTION
                      │
                   HARDWARE

Atakujący nie musi od razu zostać rootem.

Musi znaleźć ścieżkę przez granicę bezpieczeństwa.

Rolą administratora jest sprawić, żeby takich ścieżek było jak najmniej.

I właśnie dlatego najbardziej dojrzały Linux security zaczyna się nie od pytania:

„Kto jest rootem?”

ale od pytania:

„Który fragment kernela może osiągnąć ten proces — i dlaczego w ogóle mu na to pozwalamy?”

Autor

Marek „Netbe” Lampart — Inżynier Informatyki, praktyk systemów Windows/Linux i cyberbezpieczeństwa.

Źródła techniczne

Linux Kernel Documentation — The Linux Kernel threat model

Linux Kernel Documentation — Kernel Self-Protection

Linux Kernel Documentation — Linux Security Modules

Linux Kernel Documentation — Perf events and tool security

Linux Kernel Documentation — Credentials in Linux

Polecane wpisy
Konteneryzacja i orkiestracja w Linux
Konteneryzacja i orkiestracja w Linux

Konteneryzacja i orkiestracja są dwoma kluczowymi pojęciami związanymi z zarządzaniem aplikacjami w środowisku Linux. Oto ich opisy: [caption id="attachment_6673" align="aligncenter" Czytaj dalej

Konfiguracja L2TP/IPsec VPN – Kompletny przewodnik
Konfiguracja L2TP/IPsec VPN – Kompletny przewodnik

Konfiguracja L2TP/IPsec VPN – Kompletny przewodnik L2TP/IPsec to popularny protokół VPN, który łączy Layer 2 Tunneling Protocol (L2TP) z IPsec Czytaj dalej

Marek "Netbe" Lampart Inżynier informatyki Marek Lampart to doświadczony inżynier informatyki z ponad 25-letnim stażem w zawodzie. Specjalizuje się w systemach Windows i Linux, bezpieczeństwie IT, cyberbezpieczeństwie, administracji serwerami oraz diagnostyce i optymalizacji systemów. Na netbe.pl publikuje praktyczne poradniki, analizy i instrukcje krok po kroku, pomagając administratorom, specjalistom IT oraz zaawansowanym użytkownikom rozwiązywać realne problemy techniczne.