Tradycyjny model uprawnień uniksowych

Podstawowa zasada Uniksa brzmi: (prawie) wszystko jest plikiem. Dlatego w modelu uprawnień tego systemu zasadniczą role odgrywają właśnie uprawnienia do plików - użytkownicy nie mogą dokonywać na nich żadnych operacji jeżeli nie dano im takiego prawa.

Wyjątkiem jest jeden specjalny użytkownik zwany na ogół super userem (ang. użytkownikiem nadrzędnym) lub rootem (ang. korzeniem) - jego nie dotyczą żadne ograniczenia i może (w danym systemie) robić wszystko. Oznacza to, że jądro w żaden sposób nie broni się przez poczynaniami roota - nawet przed odczytywaniem i zapisywaniem przez niego całej pamięci (na przykład przez /dev/mem i /dev/kmem) albo przed mazaniem po podmontowanych dyskach. (Oczywiście zakłada się, że ktoś kto posiada takie uprawnienia posiada również odpowiednią wiedzę jak z nich korzystać, ale to nie zawsze jest prawdą... Poza tym jeżeli ktoś zdobędzie uprawnienia roota, nawet na chwilę, to nic nie jest w stanie powstrzymać go przed zniszczeniem lub modyfikacją systemu.) Obecność tylko jednego specjalnego użytkownika jest cechą charakterystyczną Uniksów (i pochodnych) - do tego stopnia, że widać to w ich nazwie (w przeciwieństwie na przykład do starszego Multicsa, w którym użytkowników specjalnych mogło być więcej).

Użytkownicy i grupy

Każdemu użytkownikowi systemu przypisany jest unikalny numer UID - od user id (ang. identyfikator użytkownika). Root ma numer 0. Oczywiście każdy użytkownik ma również nazwę używaną przy logowaniu oraz do wyświetlania, na przykład, właściciela jakiegoś pliku, ale system wewnętrznie używa właśnie UID-ów. (W zasadzie do jednego UIDa może odnosić się kilka nazw. Wtedy jednak nie będzie można nadać różnych praw posiadającym je użytkownikom.)

Z każdym plikiem w systemie związany jest UID właściciela (ang. owner). Używany jest on do kontroli uprawnień. Dzięki niemu użytkownik może stworzyć plik, który tylko on będzie mógł czytac albo plik, który każdy będzie mógł czytać, ale tylko on będzie mógł modyfikować. Tak prosta kontrola wystarcza w wielu przypadkach.

Czasami jednak użytkownik chce współdzielić plik z niektórymi innymi użytkownikami. Na przykład wykładowca może chcieć udostępnić plik z wynikami egzaminu użytkownikom będącym jego studentami, ale nikomu innemu. Tradycyjnie system uniksowy nie pozwala wiązać z plikiem kilku UIDów i dla każdego z nich oddzielnie określać uprawnienia. Zamiast tego stosuje się grupy. Podobnie jak użytkownik, każda grupa ma unikalny numer, tym razem nazywany GID - od gropup id (ang. identyfikator grupy). Użytkownicy mogą być członkami grup (ale grupy nie mogą należeć do grup). Każdy użytkownik może należeć do wielu grup (ale ma jedną główną - domyślną grupę). Wracając do naszego przykładu - wykładowca może stworzyć (a raczej poprosić administratora o stworzenie) grupę zawierającą studentów i związać GID tej grupy z plikiem z wynikami jako grupę właściciela. Wtedy będzie miał możliwość nadania praw do czytania użytkownikom w tej grupie i nikomu więcej.

Przykładowo na jednym z komputerów mam użytkownika należącego do następujących grup:

$ id
uid=1000(kangur) gid=100(users) grupy=10(wheel),18(audio),35(games),100(users)

(jak widać informację o swoim UIDzie i przynależności do grup można uzyskać za pomocą polecenia id). Wynik działania tej komendy oznacza, że mam prawo stawać się rootem (grupa wheel), mam możliwość słuchania muzyki (grupa audio), używania cdromu (grupa cdrom), grania w gry (grupa games) oraz że jestem normalnym użytkownikiem (grupa users - to moja grupa domyślna). Oczywiście opisane tu uprawnienia wynikające z przynależności do określonych grup nie biorą się z powietrza. Po prostu pliki urządzeń (lub inne, na przykład z grami) w tym systemie są własnością odpowiednich grup:

$ ls -al /dev/sound/dsp
crw-------  1 kangur audio 14, 3 sty  9 18:56 /dev/sound/dsp

$ ls -al /usr/games/bin/tuxracer
-rwxr-x---  1 games games 291932 lis 30  2003 /usr/games/bin/tuxracer

$ ls -ald /home/kangur
drwxr-xr-x  120 kangur users 25600 sty  9 23:00 /home/kangur

(to, że kangur jest właścicielem niektórych plików urządzeń jest wynikiem sztuczek dokonywanych po zalogowaniu przez bibliotekę PAM - ale o tym innym razem). Wyjątkiem jest jak widać grupa wheel, która nie jest właścicielem żadnego pliku. Przynależność do tej grupy jest sprawdzana po prostu przez polecenie su, gdy próbuję uzyskać uprawnienia roota.

Tego typu zarządzanie uprawnieniami poprzez grupy jest uniwersalne w świecie Uniksa i przy tym dość wygodne. Do dzisiaj stosuje się je na przykład w systemach *BSD i w niektórych dobrych dystrybucjach Linuksa (na przykład Gentoo).

Uprawnienia procesów

W tradycyjnych Uniksach przyjmuje się, że proces działa na zlecenie użytkownika (jest jego reprezentantem). Dlatego, w momencie startu, z każdym prosesem wiąże się UID i GID odpowiedniego użytkownika (domyślnie tego, który uruchomił program) i wartości te używane są przez cały czas wykonywania procesu do kontroli jego uprawnień. Na przykład na ich podstawie jądro podejmuje decyzję o zezwoleniu na otwarcie pliku, wysłanie sygnału i tak dalej. (Dla uproszczenia zaniedbuję szczegóły związane z effective UIDem, który pozwala na szereg ciekawych operacji - na przykład automatyczną, przeprowadzaną przez jądro, kontrolę dostępu z uprawnieniami innego użytkownika, bardzo użyteczną w przypadku wielu demonów.)

Jak jednak rozwiązać problem na przykład zmiany hasła użytkownika? To normalne, że użytkownik powinien móc zmienić (swoje) hasło, ale oczywiście nie wolno dać mu dostępu do pisania do całego pliku haseł. Ten problem, w tradycyjnym modelu uprawnień, rozwiązano poprzez wprowadzenie programów typu SUID/SGID. Są to normalne programy, tyle, że przy starcie zaczynają się wykonywać z prawami właściciela (grupy właściciela) - typowo roota. Narzędzie systemowe do zmiany haseł - passwd - jest przykładem takiego programu. Może zostać wywołane przez każdego użytkownika, ale na początku wykonuje się z uprawnieniami roota. Następnie dokonuje autoryzacji użytkownika i pozwala mu zmienić hasło. Problem pojawia się wtedy, gdy intruz jest w stanie zmusić SUIDowany program do wykonania jakichś niezamierzonych czynności. Wiele programów typu SUID szybko pozbywa się podwyższonych uprawnień (przykładem może być chociażby su).

Uprawnienia do plików i katalogów

W Uniksie są trzy podstawowe operacje na pliku: czytanie, pisanie i wykonywanie (lub wejście do katalogu w przypadku katalogów). Na liście tej nie ma na przykład zmiany nazwy pliku lub jego usunięcia, ponieważ w Uniksie są to operacje na zawierającym plik katalogu (dokładnie mówiąc nie kasuje się pliku, a jego dowiązanie - nawet sama operacja nazywa się unlink - plik zostaje usunięty dopiero gdy wszystkie dowiązania do niego zostaną zniszczone - coś jak garbage collecting lat siedemdziesiątych...).

Uniks pozwala określić które z tych trzech (czytanie, pisanie i wykonywanie) podstawowych operacji mogą być wykonywane przez każdą z trzech standardowych klas - właściciela, użytkowników należących do grupy właściciela i przez wszystkich innych. Tradycyjnie jest to zapisywane w ten sposób:

rwxrwxrwx

(pierwsze rwx odnosi się do właściciela, drugie do grupy, a trzecie do innych; litera r oznacza pozwolenie na czytanie, w - na pisanie, a x na wykonywanie; gdy nie ma jakiegoś uprawnienia w tym miejscu jest znak -).

Przykładowo:

$ ls -al test
-rw-r-----  1 kangur users 0 sty 10 02:08 test

(pierwsza - używana jest do innych zastosowań) oznacza, że mogę ten plik czytać i pisać, członkowie grupy users mogą go tylko czytać, a inni nie mogą wykonywać na nim żadnych operacji. Oczywiście procesy roota (UID O) mają dostęp do każdego pliku niezależnie jakie uprawnienia są ustawione.

Wewnętrznie system operacyjny reprezentuje te uprawnienia jako bity w liczbie 16 bitowej. Na opisane powyżej 3x3 uprawnienia potrzeba razem 9 bitów. Oprócz tych podstawowych uprawnień w Uniksach występują również dodatkowe.

Pierwszym z nich jest bit sticky (ang. klejący - nazwa, nie mająca nic wspólnego z dzisiejszym użyciem, pochodzi z czasów, gdy systemy operacyjne nie miały tak rozbudowanych podsystemów pamięci wirtualnej; oznaczała wtedy, że program należy pozostawić - przykleić - w pamięci po wykonaniu). Obecnie bit ten używany jest wyłącznie w odniesieniu do katalogów i oznacza, że usunąć plik z danego katalogu może tylko właściciel tego pliku. (Normalnie to prawa katalogu decydują o możliwości usunięcia pliku. W tym przypadku wykonywane jest dodatkowe sprawdzenie.)

Dwa inne dodatkowe bity to SUID i SGID. Oznaczony nimi plik przy wykonaniu dostanie uprawnienia właściciela (grupy) zamiast użytkownika, który faktycznie go uruchomił.

W większości Uniksów występują także (chociaż implementowane różnie) bity append-only i immutable. Pierwszy z nich pozwala tylko na dopisywanie na koniec pliku (używane czasami do logów), a drugi wogóle zabrania modyfikacji oznaczonego nim pliku. Oczywiście standardowo nic nie przeszkodzi rootowi cofnąć tych uprawnień - wtedy plik można już zapisywać normalnie.

Zalety i wady tradycyjnego modelu uprawnień uniksowych

Opisany powyżej model uprawnień ma dwie zasadnicze zalety. Pierwszą z nich jest prostota. Opis uprawnień pliku zajmuje niewiele miejsca i może być wyświetlany na zwykłym listingu plików (nie ma możliwości, że nie zauważymy jakiś specjalnych uprawnień), a zasady rządzące przyznawaniem lub odmową dostępu są bardzo proste i przejrzyste. Dzięki temu łatwo przeszkolić nowych użytkowników i łatwo stosować je w praktyce.

Drugą, nawet ważniejszą, zaletą jest jego przenośność i uniwersalność (w świecie uniksowym - ale dla niektórych to cały świat). Wszystkie standardy definiujące wspólne interfejsy systemów uniksowych spodziewają się właśnie takiego modelu uprawnień. Również istniejące oprogramowanie bardzo często zakłada obecność tego modelu. Dzieki temu można uzyskać legendarną wręcz przenośność aplikacji ("P: Jak przenieśliście swoje oprogramowanie na Linuksa [z innego Uniksa]? O: make install...").

Tu jednak dochodzimy do wad tego rozwiązania. Po pierwsze sposób opisu uprawnień jest bardzo mało dokładny. W wyniku tego procesy działają z większymi uprawnieniami niż są im potrzebne. Na przykład wiele programów musi (przynajmniej przez chwilę) działać z uprawnieniami roota, tylko dlatego, że chcą otworzyć jakiś niski port TCP lub odczytać jakiś plik. A ponieważ root może wszystko, każdy błąd w takim programie może spowodować przejęcie kontroli nad całym systemem. To właśnie jest powodem powstania większości dodatkowych zabezpieczeń, które postaram się omówić później.

Niewiele lepiej jest z kontami daemonów - na przykład tak popularny i ceniony apache działa (przez większość czasu) na prawach własnego użytkownika (najczęściej apache, www lub httpd). Również na takich prawach (domyślnie - bez suEXECa lub jakiegoś niewygodnego w użyciu patcha) uruchamia wszystkie skrypty (np. CGI, PHP, Perl). Prowadzi to do problemów i możliwości zakłócenia pracy serwera przez pojedynczego złośliwego użytkownika (był to powód wprowadzenia większych zabezpieczeń na rainbowie).

Po drugie, w tradycyjnym modelu uprawnień, nie można dać praw do pliku konkretnemu użytkownikowi, który nie jest w jednej grupie z właścicielem, co nie tylko jest niewygodne, ale również może spowodować zagrożenia. Użytkownik nie może na przykład dać praw do skryptu PHP zawierającego hasła do bazy danych w taki sposób, żeby tylko PHP mogło z niego korzystać. Kończy się to na ogół nadaniem praw do odczytu (a czasem i do zapisu - na przykład do logów strony) dla wszystkich.

Częściowym rozwiązaniem tego problemu jest właśnie suEXEC - część apacha, który zmienia użytkownika w momencie wykonania skryptu na właściciela tego skryptu. Ale wtedy z kolei konto użytkownika nie jest w żaden sposób chronione przed dziurami w wykonywanych skryptach. Załóżmy, że użytkownik posiada dwa pliki skrypt.php i tajny.txt (oba do odczytu i zapisu tylko przez właściciela) oraz że w systemie aktywny jest suEXEC. Jeżeli skrypt.php (wykonujący się na prawach właściciela) zawiera jakiś błąd (niestety częste w przypadku skryptów w PHP), to wykorzystując ten błąd atakujący może odczytać, zmodyfikować lub nawet usunąć tajny.txt bez pozostawiania innych śladów. Dlatego nie polecałbym suEXECa w systemach, w których użytkownicy trzymają na swoich kontach oprócz skryptów inne ważne lub tajne dane.

W tradycyjym modelu uprawnień konieczne jest także istnienie programów typu SUID/SGID. Tymi bitami oznaczone są na przykład su (oczywiste), suexec (też oczywiste), mount (możliwość montowania dyskietek, cdromów i dysków sieciowych bez roota), pppd (możliwość uruchomienia przez zwykłego użytkownika), ping (otwarcie połączenia ICMP), xterm i podobne (konieczność dokonania wpisu w dzienniku logowań) i wiele innych. Nie jest to dobre dla bezpieczeństwa - każdy błąd w takim programie lub bibliotece, której używa, może dać zwykłemu użytkownikowi pełną kontrolę nad systemem. Oczywiście część z tych uprawnień można zabrać (kosztem wygody i narzekań użytkowników), ale ciężko jest zlikwidować wszystkie takie przypadki.

Opisane tu wady (i kilka innych) sprawiły, że od dość dawna zaczęto szukać sposobu rozszerzenia tego modelu i zlikwidowania przynajmniej części potencjalnych problemów.

Prezentacja na Systemy Operacyjne 2004/2005 - Informatyka MIMUW.
Grzegorz Kulewski (O nas)