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







