Ograniczenia z poziomu powłoki
Napisał: Patryk Krawaczyński
16/06/2009 w Bezpieczeństwo Brak komentarzy. (artykuł nr 74, ilość słów: 2376)
P
owłoka zapewnia interfejs pomiędzy jądrem, a użytkownikiem. Można ją opisać jako interpreter. Interpretuje ona polecenia wprowadzane przez użytkownika i przekazuje je do jądra systemu. Interfejs powłoki jest bardzo prosty. Zwykle składa się z monitu, po którym wprowadza się polecenie i następnie potwierdzenie klawiszem [Enter]. Polecenia wpisujemy w wierszu, który jest często nazywany wierszem poleceń. Po dłuższym użytkowaniu Linuksa można przekonać się, że polecenia wprowadzane w wierszu mogą być bardzo skomplikowane, a jej interpretacyjne zdolności dają możliwość stosowania wielu wymyślnych sztuczek.
Na przykład powłoka zawiera zestaw znaków specjalnych umożliwiających generowanie nazw plików, może przekierowywać wejście i wyjście, może również wykonywać operacje w tle, pozwalając na wykonywanie w tym czasie innych zadań. Dodatkowo powłoka robi więcej niż tylko interpretowanie poleceń – udostępnia środowisko do konfigurowania systemu i oprogramowania. Powłoka posiada również swój własny język programowania, który pozwala na pisanie programów wykonujących polecenia Linuksa w różnorodny sposób. Język programowania powłoki ma wiele cech normalnych języków programowania, na przykład pętle i odgałęzienia. Możemy nawet tworzyć skomplikowane programy powłoki tak wydajne jak aplikacje.
Przez lata wypracowano kilka różnych rodzajów powłok. Obecnie istnieją trzy główne powłoki: Bourne’a, Korna i C. Powłoka Bourne’a została stworzona w Bell Labs na potrzemy Systemu V. Powłoka C została stworzona dla wersji BSD Uniks, natomiast powłoka Korna jest późniejszym rozwinięciem powłoki Bourne’a. Aktualnie wersje Uniksa, z Linuksem włącznie, udostępniają wszystkie te trzy powłoki (czasami nawet więcej), pozwalając na wybranie tej, która nam odpowiada. Jednak Linux korzysta z wersji rozszerzonych lub public domain tych powłok: Bourne Again (BASH), powłoki TC (TCSH) i powłoki Public Domain Korn (PDKSH). My zajmiemy się wstępną konfiguracją powłoki BASH, ze względu na fakt, iż jest ona powłoką domyślną oraz jest ona najczęściej spotykaną powłoką w systemach Linuksowych jako główna powłoka (warto wspomnieć, że w powłoce BASH dla Linuksa wbudowane są wszystkie zaawansowane właściwości powłok Korna i C, a także powłoki TCSH). Do pracy potrzebujemy tylko jednej powłoki, lecz z niej możemy się przełączać w miarę potrzeby na inne. Poniższe konfiguracje głównie będą tyczyły się wprowadzenia ograniczeń systemowych dla użytkowników, korzystających z powłoki zapewniając nam większą kontrolę nad ich poczynaniami w naszym systemie. Pierwszą z nich będzie zabezpieczenie historii poleceń wydawanych przez użytkownikow.
Podczas używania powłoki bash informacje o wszystkich wydawanych przez nas komendach są zapisywane w pliku .bash_history w naszym macierzystym katalogu (jest to standardowa nazwa pliku historii). W przypadku historii poleceń wydawanych przez administratora plik ten powinien być jak najkrótszy lub w ogóle nie istnieć, ze względu na możliwość odczytania z niego niektórych właściwości systemu. Jednak w przypadku użytkownika lepiej jest, gdy historia poleceń jest tak długa, że administrator może zweryfikować poczynania użytkownika w systemie. Kolejną opcję jaką możemy określić to czas bezczynności konsoli, po którym nastąpi automatyczne zerwanie sesji. Wielokrotnie odchodząc od terminala użytkownicy zapominają o wylogowaniu się, czy chociaż zablokowaniu aktualnej sesji. W takich sytuacjach określenie czasu w przeciągu, którego to nie zostanie wydana żadna komenda lub jakakolwiek aktywność przy terminalu, a sesja zostanie automatycznie przerwana jest bardzo pomocne. W celu stworzenia osobnych ustawień dla administratora jak i użytkowników systemów musimy dokonać dwóch wpisów do pliku konfiguracyjnego powłoki bash – /etc/profile:
if [ `id -u` = "0" ]; then HISTSIZE=10 HISTFILESIZE=10 TMOUT=300 export HISTSIZE HISTFILESIZE TMOUT fi
Warunek ten sprawdza, czy logująca się osoba to administrator (user id = 0), jeśli tak to nakłada na nią ograniczenia w postaci ograniczenia długości pliku historii do 10 ostatnich wydanych komend oraz określa czas bezczynności sesji do 5 minut. Podobnie możemy wprowadzić ograniczenia dla użytkowników systemu, lecz zamiast w warunku umieszczając poszczególne ich identyfikatory (co by stało się czasochłonne oraz wymagało dużej ilości wpisów) możemy zglobalizować wpis poprzez weryfikację ich numeru grupy. Zakładając, że wszyscy zwykli użytkownicy należą do grupy “users” o id równym 100, to warunek powinien wyglądać następująco (jeśli posiadamy inne grupy np. “webmasters” to dla nich także powinniśmy dokonać osobnego wpisu):
if [ `id -g` = "100" ]; then HISTSIZE=250 HISTFILESIZE=250 TMOUT=300 export HISTSIZE HISTFILESIZE TMOUT fi
Lecz jest to dopiero pierwszy krok do udoskonalenia systemu zapisu historii. Ponieważ bardziej doświadczony użytkownik może wyżej wymienione wartości zredukować np. do 1 poprzez wydanie komendy “export HISTFILESIZE=1” czy zmienić nazwę pliku, do którego będzie ona zapisywana – “export HISTFILE=.history“. By temu zapobiec musimy nanieść restrykcję na opcję, które mają posiadać wartości ustalone tylko i wyłącznie przez nas, a ich wartość ma pozostać stała. W naszym przypadku są to HISTSIZE, HISTFILE, HISTFILESIZE oraz TMOUT. Oczywiście możemy dodać jeszcze inne istotne opcje, np. w przykładzie poniżej są to nazwa do jakiego ma być zapisywana historia oraz login użytkownika. W celu nałożenia restrykcji wpisy te powinny się znaleźć w pliku /etc/profile:
typeset -r HISTSIZE typeset -r HISTFILESIZE typeset -r HISTFILE typeset -r HISTNAME typeset -r TMOUT typeset -r USER typeset -r LOGNAME
W przypadku kiedy uznamy, że pliki z naszą historią poleceń nie powinny być dostępne dla nikogo oraz likwidowane w momencie opuszczania przez nas systemu, wystarczy, że w pliku .bash_logout dodamy składnię: clear && rm -f /root/.bash_history. Jeśli podczas naszych sesji korzystamy z Midnight Commandera to nad wyżej wymienioną komendą dodajemy polecenie: rm -f /root/.mc/history.
Ktoś by mógł się spytać: Po co określać wielkość pliku historii użytkowników skoro oni także mogą sobie dopisać powyższe komendy do swoich plików wykonujących komendy podczas opuszczania systemu? Dlatego samo określenie ilości komend jakie mają być przechowywane w pliku historii poszczególnych użytkowników nie wystarczy. Dodatkowo musimy spowodować, by nie mogli oni modyfikować danych plików historii. Efekt ten uzyskamy dzięki atrybutowi “a” (append – tryb dopisywania). Plik posiadający ustawiony taki atrybut nie będzie możliwy do edycji przez zwykłego użytkownika. Zapis efektów edycji danego pliku z tym atrybutem przez użytkownika będzie tylko możliwy w innym pliku, a ze względu na to, że czynności logowania historii są przeprowadzane przez proces systemowy w pliku tym nadal będą zapisywane poczynania użytkownika. Lecz takie działanie pociąga za sobą konsekwencje powodujące ciągłe narastanie pliku historii. Lecz problem ten można rozwiązać poprzez napisanie skryptu, który zostanie będzie uruchamiany w odpowiednich odcinkach czasu, lub zaraz po sprawdzeniu historii użytkowników w poszukiwaniu podejrzanych wpisów. Więc po dodaniu do systemu nowego użytkownika np. o loginie agresor powinniśmy od razu zmodyfikować atrybuty jego pliku historii, czyli stworzyć go zanim nastąpi pierwsze logowanie: touch /home/agresor/.bash_history ; przypisać mu odpowiedniego właściciela i grupę: chown agresor:users /home/agresor/.bash_history ; i na końcu nadać sam atrybut: chattr +a /home/agresor/.bash_history. Analogicznie postępujemy z resztą nowych lub starych użytkowników. Lub jeśli używamy skryptu “adduser” dołączonego do Slackware autorstwa Suart’a Winter’a do dodawania użytkowników możemy dodać poniższy warunek na jego końcu, przed informacją o pomyślnym skonfigurowaniu konta (echo “Account seutp complete.”):
if [ "$SHL" = "-s /bin/bash" ]; then cd $HME touch .bash_history chown $LOGIN: .bash_history $chmod 600 .bash_history chattr +a .bash_history fi
Powyższy warunek sprawdza czy powłoką dodawanego użytkownika jest bash shell. Jeśli tak to przechodzi do jego domowego katalogu, wcześniej zdefiniowanego pod postacią zmiennej $HME tworzy plik .bash_history, przyznaje mu właściciela pod postacią aktualnie dodawanego użytkownika (np. agresor:users) oraz ogranicza jego edycję przez zwykłego użytkownika poprzez nadanie atrybutu “append”. Warto wspomnieć, że użytkownik po wydaniu komendy: chattr -a .bash_history – zostanie poinformowany, o barku praw do odjęcia danego atrybutu od pliku. Kolejnym krokiem jest napisanie skryptu, który będzie oczyszczał plik historii, który jest zapisywany w trybie przyrostowym. Skrypt ten nazwijmy apdhist.sh. Tworzymy pusty plik: touch apdhist.sh, a następnie przy pomocy ulubionego edytora wpisujemy do niego następujące wartości:
#!/bin/bash cd / for login in /home/*/.bash_history do chattr -a $login; echo -n > $login; chattr +a $login; done
Jak widzimy skrypt nasz w pierwszym kroku zdejmuje wszystkim użytkownikom znajdującym się w katalogu /home artybut append z pliku historii poleceń, następnie dzięki wpisaniu pustej linii likwiduje jego objętość oraz ponownie zabezpiecza go przed edycją użytkowników. Jak już wspomniałem – skrypt ten możemy uruchamiać w systematycznych odcinkach czasu (np. co miesiąc przez program crond) lub zaraz po sprawdzeniu wpisów w danych plikach – w tym celu należy się upewnić czy przypadkiem nie zniszczymy swoich dowodów przeciwko potencjalnemu intruzowi.
W przypadku systemu Linux występuje wiele atrybutów, które są dziedziczone od procesu nadrzędnego podczas tworzenia procesu potomnego przy użyciu polecenia fork, i które są utrzymywane w czasie wykonywania. Dodatkowo pewnym ograniczeniom podlegają również zasoby wykorzystywane przez dany proces. Fork jest jedną z najważniejszych funkcji systemowych. Jej zadaniem jest powołanie do życia nowego procesu na zasadzie relacji rodzic – dziecko. Proces, w którego kodzie znajdzie się wywołanie funkcji, staje się ojcem nowego procesu. Wykorzystanie funkcji fork w programie, a w konsekwencji powstanie nowego procesu następuje w wyniku wykonania kodu:
pid = fork ();
Po wykonaniu tej instrukcji w systemie będą już istnieć dwa osobne procesy – ich odróżnienie w programie jest możliwe dzięki wartości zwracanej przez funkcję fork – procesowi macierzystemu zwrócony zostanie identyfikator syna, procesowi potomnemu – zero. Gdy system uzna, że nowego procesu nie da się utworzyć (na przykład z powodu braku pamięci lub przekroczenia limitów na liczbę istniejących procesów), funkcja fork zwróci wartość – 1, i oczywiście, nie powstanie żadnej nowy proces. Celem ustanawiania tych limitów jest chęć ograniczenia zjawiska “uciekających procesów”, które w przypadku wystąpienia błędu programu lub użytkownika powodują wykorzystanie wszystkich dostępnych zasobów krytycznych dla działania systemu.
Dlatego innym mechanizmem ograniczeń powłoki jest ulimit. Przydaje się on szczególnie wtedy, gdy jeden lub więcej użytkowników, przypadkowo lub umyślne, może zużyć wszystkie zasoby systemowe, wyraźnie zmniejszając wydajność lub powodując natychmiastową awarię systemu. Dlatego często niedostrzegalnym sposobem radzenia sobie z pożeraczami zasobów jest zastosowanie funkcji ulimit interpretera bash (do ustawienia limitów służy wywołanie systemowe setrlimit(), natomiast to ich pobrania getrlimit(), a wywołanie systemowe o nazwie getrusage() wyświetla informacje o bieżącym wykorzystaniu różnych limitów). Oto wynik komendy ulimit -a, listujący ograniczenia w stosunku do użytkownika:
core file size (blocks, -c) unlimited data seg size (kbytes, -d) unlimited file size (blocks, -f) unlimited max locked memory (kbytes, -l) unlimited max memory size (kbytes, -m) unlimited open files (-n) 1024 pipe size (512 bytes, -p) 8 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 4089 virtual memory (kbytes, -v) unlimited
Widzimy tutaj, że prawie wszystkie ograniczenia posiadają brak limitów. Tyczy się to szczególnie rozmiaru pliku, wielkości zrzutu pamięci, ilości pamięci czy nadmierna wielkość ilości procesów. Ta standardowa konfiguracja aż się prosi o zmianę. Np. typowym sposobem na uzyskanie praw administratora jest zmuszenie programu setuid / setgid do zrzucenia pamięci (po awaryjnym zatrzymaniu biegu programu) w określonym miejscu systemu. Agresor może za pomocą tej metody nadpisać dowolne pliki w dowolnym miejscu na serwerze, łącznie z /etc/passwd, dzięki czemu może stworzyć użytkownika z prawami administratora. Dzisiejsze serwery dysponują gigabajtami pamięci operacyjnej (wykluczając te domowe) i zrzuty pamięci mogłyby potencjalnie wypełnić cały dysk, co dla pozostałych użytkowników tego samego systemu plików stanowiłoby atak typu Denial of Service (DoS). Można temu zapobiec w prosty sposób za pomocą polecenia ulimit, które ogranicza zużycie zasobów przez dowolnego użytkownika m.in, przez narzucenie maksymalnego rozmiaru zrzutu pamięci. Należy ustawić tę wielkość na 0 za pomocą następującego polecenia: ulimit -c 0 Tak samo aby zapobiec tworzeniu ogromnych plików przez proces (i jego procesy potomne – czyli wywołane przez niego), wystarczy wydać polecenie: ulimit -f 1024 – co ograniczy im jednorazowe tworzenie pliku o pojemności bliskiej 1 MB. By się o tym przekonać wystarczy wpisać polecenie z poziomu zwykłego użytkownika:
ulimit -f 1024 yes 'Linus Torvalds udostępnia jądro systemu Linux (1991) \ - Linux 2.6 (2005)' > test.txt File size limit exceeded
Należy pamiętać, że nic nie uniemożliwi utworzeniu wielu plików przez użytkownika, choćby ich wielkość była ograniczona poleceniem ulimit, ale by temu zapobiec można przecież wprowadzić limity dyskowe (mechanizm quota). Podobne polecenie ulimit może ograniczyć maksymalną liczbę procesów potomnych uruchamianych przez jednego użytkownika. Warto wspomnieć, że limitowi zostaną poddane procesy jednego użytkownika na wszystkich terminalach (łącznie z procesami działającymi w tle). W tym przypadku ograniczymy liczbę procesów do 100: ulimit -u 100. W celu sprawdzenia poprawności tego limitu wykorzystamy mały skrypt o nazwie “procesy” z wywoływaniem rekurencyjnym procesów:
#!/bin/bash export RUN=$((RUN + 1)) echo $RUN... $0
Po jego wywołaniu – z poziomu zwykłego użytkownika oczywiście – powinniśmy otrzymać następujący wynik:
1... 2... 3... 4... 5... aż do: 99... ./procesy: fork: Resource temporarily unavaiable
W ten sposób każdy użytkownik będzie hamowany przed nadmierną rozrzutnością. Jedną z bardzo przydatnych opcji polecenia ulimit jest maksymalny rozmiar pamięci wirtualnej. Po osiągnięciu tej wartości procesy zakończą pracę z błędem segmentacji (co nie jest idealne, ale zapobiegnie załamaniu systemu w razie wyczerpania pamięci RAM i partycji wymiany). Jeśli istnieje szczególnie groźny proces, który odznacza się szczególnym drzewem procesów (np. Apache + PHP + SSL) można użyć polecenia ulimit jako “hamulca awaryjnego”. Oczywiście limit taki możemy wykorzystać także na użytkownikach np. dając im po 5 MB z pamięci RAM i pliku wymiany: ulimit -v 5120 oraz ulimit -m 5120. Tak samo możemy zredukować ilość czasu zużycia całego procesora dla użytkownika (wartość podajemy w sekundach). Jeśli chodzi o zabezpieczenie przed ponownym wznowieniem większych wartości limitów – to problem ten został rozwiązany w samym mechanizmie ulimit, ponieważ użytkownicy mogą zmniejszyć swoje limity, ale nie mogą ich zwiększyć. Oznacza to, że limity ustalone poleceniem ulimit w pliku /etc/profile nie mogą być zmienione później przez innych użytkowników niż root:
ulimit -u unlimited bash: ulimit: cannot modify limit: Operation not permitted
Jak zastosować dane ograniczenia dla grupy użytkowników zostało rozwiązane powyżej wraz z plikiem historii. wystarczy, że dopiszemy parę linijek po wartości export. Ustawienie limitów systemowych może zostać odebrane przez użytkowników jako kolejne koło tortury, ale znacznie jest takie wrażenie od katastrofy, jaką mogą spowodować rozszalałe stada procesów tych samych użytkowników. Przypomnę tylko, że w interpreterze poleceń tcsh analogicznym poleceniem do ulimit jest limit.
Bonus:W celu wykorzystania powłoki TCSH i skonfigurowania jej w celu wykonywania automatycznej operacji wylogowania się z systemu należy określić w zmiennej: autologout liczbę minut, po których ma nastąpić automatyczne wylogowanie.
set autologout=10 set savehist=250
W tym przykładzie okres bezczynności, po którym zostanie wykonane wylogowanie został ustawiony na 10 minut. Drugi wpis dotyczy historii zdarzeń systemowych, która zostanie ograniczona do ilości 250 linii zachowywanych na dysku twardym. Powyższe wpisy – w celu ustanowienia ich przy starcie systemu należy umieścić w pliku: /etc/csh.cshrc.
Więcej informacji: man bash, man chattr