Na przełomie września i października 2014 świat obiegła informacja o ukrytej przez lata luce bezpieczeństwa w powłoce GNU Bourne Again Shell (skr. Bash). Większość serwisów związanych z bezpieczeństwem IT już zdążyła na ten temat napisać i podać odpowiednie metody ochrony, jednak naszą uwagę przykuł edukacyjny aspekt luki – pod tym względem jest to „dobry błąd”, tzn. taki, którego można użyć do wyjaśnienia wielu ciekawych mechanizmów obecnych w systemach typu Unix.
Luka Shellshock (zwana też Shelldoor lub Bashbleed) to usterka bezpieczeństwa w uniksowej powłoce Bash, która pozwala na lokalne (LCE) i zdalne (RCE) wykonywanie dowolnego kodu z uprawnieniami uruchamiającego ją użytkownika. Jest niebezpieczna, ponieważ pośrednio z powłoki korzysta wiele powszechnie dostępnych usług sieciowych, np. serwery WWW wyposażone w interfejsy CGI, serwery DHCP, a nierzadko serwery poczty elektronicznej bądź aplikacje PHP, które posługują się powłoką, aby we właściwym środowisku uruchomić jakiś program.
Przyczyną zagrożenia wystąpieniem usterki jest błąd niewystarczającego filtrowania danych wejściowych pochodzących ze środowiska odziedziczonego po procesie nadrzędnym. Pozwala on na użycie tzw. eksportowanych funkcji powłokowych w celu przemycenia do uruchamianego procesu powłoki dowolnych komend. Wstrzyknięte w ten sposób polecenia zostaną wykonane przez powłokę tuż po uruchomieniu, w momencie wczytywania otrzymanego zestawu zmiennych środowiskowych.
W efekcie potencjalny napastnik może wpływać na działanie usług i modyfikować należące do nich zasoby. Oznacza to w najlepszym przypadku dyskredytację danego serwisu sieciowego, a w najgorszym przejęcie kontroli nad systemem (gdy usługa działa z uprawnieniami administratora lub znalezione będą dodatkowe usterki).
Spróbujmy założyć, że nie wiemy wiele o systemach typu Unix, powłoka kojarzy nam się raczej z ubiorem lub emulsją do malowania budynków, a środowisko z protestami ekologów w Dolinie Rospudy. Aby dogłębnie zrozumieć temat, musimy sobie przypomnieć:
- Czym charakteryzują się systemy typu Unix?
- Czym jest uniksowa powłoka?
- Czym różni się proces od programu i czym jest środowisko?
- Jak Bash obsługuje środowisko i co to są zmienne środowiskowe?
- Jakie są warunki wystąpienia usterki w Bashu?
- Jakie usterki znaleziono przy okazji?
- Jak rozwiązano problemy?
Jeżeli mechanizmy systemu operacyjnego związane z zarządzaniem środowiskiem procesów nie są Ci obce, wiesz co robią funkcje
execve()
ifork()
, a także zdajesz sobie sprawę, że środowisko może zawierać nie tylko zmienne, ale dowolne napisy, przejdź do opisów technicznych samych usterek.
Wprowadzenie
Dawno, dawno temu wszyscy użytkownicy systemów komputerowych byli programistami. Były to czasy środowisk, w których „rozmowę” z systemem operacyjnym użytkownik prowadził z użyciem zintegrowanego edytora kodu i debuggera. Uniksy nie zdążyły jeszcze się spopularyzować, a powszechne dziś relacyjne bazy danych, uruchamiane na osobnych komputerach, istniały jedynie w marzeniach specjalistów od sprzedaży tych ostatnich. Dane były kodem, kod danymi, a programista mógł sięgać po ich struktury bez zastanawiania się, gdzie są przechowywane. Czas mainframe’ów.
Bezpieczeństwo teleinformatyczne jako branża praktycznie nie istniało, systemy nie miały efektywnego podziału na użytkowników o różnych poziomach dostępu, a każdy, kto mógł skorzystać z terminalu lub jego emulatora miał nieograniczony dostęp do usług i przetwarzanych danych w obrębie systemu. Wydawać by się mogło, że z perspektywy IT security mieliśmy do czynienia z – mówiąc kolokwialnie – niezłą porażką. Być może, ale i model ochrony zasobów był nieco inny, a gdyby ewoluował wraz z przyjętym kierunkiem po dziś dzień, to usterek bezpieczeństwa byłoby nawet mniej, a z pewnością miałyby inną charakterystykę.
Myśląc o monolitycznych systemach przeszłości warto pamiętać, że były to bardziej odpowiedniki dzisiejszych maszyn wirtualnych, niż wieloużytkownikowych systemów operacyjnych. Oznacza to, że ochrona była możliwa, ale była to ochrona na poziomie dostępu do usługi lub struktur pamięciowych obsługiwanych przez interpreter języka lub metajęzyka stworzonego na potrzeby projektu.
Unix
Pojawienie się Uniksów wstrząsnęło branżą nowych technologii. Nie z powodu szczególnie zaawansowanych rozwiązań technicznych czy wygody obsługi, ale z powodu architektury, która pozwalała adaptować system do własnych potrzeb i tanim kosztem tworzyć klony dostosowane do konkretnych wymagań. Wiązało się to m.in. z uniezależnieniem systemu od sprzętu, jak również z możliwością współpracy jego nabywcy z wieloma dostawcami oprogramowania, a nie jedną instytucją, która produkuje zarówno system, jak i każdą aplikację działającą pod jego kontrolą.
Trudne do szybkiego zgłębienia przez początkujących języki funkcyjne czy interpretery zorientowane na rozwiązywanie konkretnych problemów biznesowych zastąpiły uniwersalne narzędzia tworzone w języku C i skrypty powłokowe. Te pierwsze były kompilowane do postaci kodu maszynowego, zoptymalizowanego pod kątem architektury sprzętowej. Z kolei architektoniczny podział systemu na małe, współpracujące ze sobą części pozwolił na obniżenie wymagań odnośnie kompetencji obsługi technicznej – administrator był operatorem maszyny, a nie specjalistą od wszystkiego, w tym od zarządzania danymi i programowania.
Unix to w istocie zestaw współpracujących ze sobą komponentów, z których każdy wykonuje jakieś specyficzne zadanie. Architekt systemowy może integrować ze sobą wybrane składniki w celu stworzenia środowiska pod konkretne potrzeby. Przekłada się to również na ostateczną cenę, co nie pozostało bez wpływu, jeżeli chodzi o popularyzowanie się Uniksów. Warto też zaznaczyć, że pod względem promocji sprzedawcy Uniksów byli bardzo skuteczni. Po pewnym czasie gros absolwentów uczelni technicznych wchodziło na rynek pracy z praktycznymi umiejętnościami programowania głównie w języku C i korzystania z uniksowych komend oraz usług. Twórcom Uniksa – Dennisowi Ritchie’emu i Kenowi Thompsonowi z Bell Labs – udało się stworzyć uniwersalny, wieloużytkownikowy i wieloprocesowy, sieciowy system operacyjny z podziałem czasu, na który mogła pozwolić sobie niemal każda placówka.
Do „pogawędek” operatora z systemem użyto specjalnie w tym celu stworzonego oprogramowania. Naturalnie, można by kontynuować programistyczną tradycję i skorzystać na przykład z Lispu – jednego z najznakomitszych języków programowania, popularnego kiedyś, a teraz, za sprawą projektów takich jak Clojure, świętującego drugą młodość. Sprawdziłby się w roli systemowego interfejsu użytkownika, ale pod warunkiem, że ten użytkownik ma już doświadczenie w pracy z nim (jest de facto programistą). Dla kogoś, kto chce po prostu uruchomić czy zatrzymać usługę, skopiować plik bądź podejrzeć pliki raportów zdarzeń (logi), znacznie lepszym narzędziem będzie interpreter skrojony na miarę, którego komend i składni nauczyć można się w kilka godzin.
Powłoka
Każdy system komputerowy, który obsługiwać ma człowiek niebędący programistą,
zasługuje na interfejs komunikacyjny, umożliwiający przeglądanie zasobów tego
komputera, uruchamianie aplikacji, wydawanie komend i oglądanie rezultatów ich
pracy. Wspomniany interfejs może być graficzny (np. apple’owy Finder) lub
tekstowy (np. COMMAND.COM
z systemu MS-DOS, czy właśnie uniksowy
bash
). Z całą jednak pewnością będzie przyjmował informacje z użyciem klawiatury
(i ew. myszki), a rezultaty wykonywanych czynności prezentował na monitorze komputera
(lub zdalnego terminalu). Nazywamy to interaktywną powłoką systemową
(ang. interactive shell), tzn. taką, dzięki której użytkownik może w czasie
rzeczywistym komunikować się z OS-em.
Powłoka systemowa (ang. shell) to program komputerowy jak każdy inny. Jej funkcją jest stworzenie warunków do wykonywania podstawowych czynności użytkowych, takich jak dostęp do plików i katalogów czy możliwość uruchamiania programów.
Powłoki interaktywne są pierwszoplanowym procesem (uruchomionym programem),
który na bieżąco realizuje wydawane komendy i wyświetla ich rezultaty, z kolei
powłoki nieinteraktywne (ang. non-interactive shells) robią te same rzeczy, ale
bez nadzoru ze strony operatora będącego człowiekiem. Powłoka interaktywna
w systemach typu Unix jest uruchamiana na prawach uwierzytelnionego użytkownika przez
oprogramowanie umożliwiające lokalne lub zdalne korzystanie z systemu (np. przez
program login
czy demona sshd
).
Nazwę ścieżkową domyślnej powłoki przypisanej do danego użytkownika znajdziemy
w pliku haseł /etc/passwd
lub innej bazie (np. LDAP czy NSS, jeżeli są
używane). Najczęściej powłoka, która potrafi działać interaktywnie, posiada też
możliwość pracy w trybie nieinteraktywnym.
Współczesne powłoki zawierają rozbudowany interpreter poleceń. Dzięki temu mogą nie tylko przyjmować pojedyncze komendy czy ich sekwencje, ale też interpretować i wykonywać skrypty – proste programy pisane z wykorzystaniem odpowiednich wewnętrznych i zewnętrznych poleceń.
Polecenia wewnętrzne (ang. internal commands) to takie komendy, których działanie
realizowane jest bezpośrednio przez podprogramy powłoki w obrębie jej własnego
programu, a polecenia zewnętrzne (ang. external commands) to inne programy
użytkowe znajdujące się na nośniku, np. znane z Uniksów: ping
, grep
, awk
, sed
czy mc
(Midnight Commander) – są one wywoływane, a w tym czasie powłoka oczekuje
w tle na ich zakończenie i zapamiętuje tzw. kod zakończenia (ang. exit status)
każdego.
Pierwszą powłoką w Uniksie (i w jego prekursorze, systemie Multics) był Thompson shell, który powstał w roku 1971. Napisany został przez – jak wskazuje nazwa – współtwórcę tego systemu. Był to prosty interpreter poleceń, pozbawiony możliwości wykonywania skryptów. Innowacją w stosunku do znanych w tamtych czasach OS-ów było potraktowanie powłoki jako samodzielnego programu użytkowego, a nie części jądra. Z punktu widzenia bezpieczeństwa wydaje się to rozsądnym posunięciem, ponieważ każda luka oznaczałaby przejęcie kontroli nad pracą całego systemu.
Wspominając shella Thompsona warto pamiętać o tym, że to właśnie w nim zastosowano po
raz pierwszy operatory przekierowania strumienia <
i >
oraz operator potoku |
–
konwencję tę zastosowano potem w większości znanych powłok.
Program i proces
Nadmieniliśmy wcześniej, że powłoka systemowa jest programem komputerowym. Oznacza to, że gdzieś na nośniku (np. dysku twardym) znajduje się zbiór danych mający początek, określoną długość, pewną nazwę i inne atrybuty (np. słowo trybu dostępu, które mówi o tym, kto może w jakim trybie – np. zapisu, odczytu czy uruchamiania – mieć do tych danych dostęp). Taki zbiór danych nazywamy plikiem (ang. file).
W systemach typu Unix dąży się do tego, aby wszystko było plikiem, a dokładniej, aby z większością obiektów systemowych dało się komunikować używając plików jako reprezentacji ich wejścia i wyjścia. Żeby było to możliwe, niektóre pliki to tzw. pliki specjalne (ang. special files). Ich zawartość nie znajduje się na dysku, lecz generowana jest dynamicznie i zależy od skojarzonego z plikiem specjalnym podprogramu obsługi (np. jakiegoś urządzenia).
Wracając do definicji programu. Będzie nim zbiór instrukcji maszynowych i danych potrzebnych do ich właściwego wykonania umieszczony w pamięci bądź w tzw. pliku wykonywalnym (ang. executable file). Plik wykonywalny to taki plik, który przeznaczono do uruchamiania i zawierający obraz programu. Czym jest obraz programu? To łańcuch instrukcji zrozumiałych przez CPU, ukształtowany zgodnie z zasadami jednego z formatów plików wykonywalnych (ang. executable formats). Popularne w Uniksach formaty to na przykład a.out (skr. od assembler output) i Executable and Linkable Format (skr. ELF).
Dzięki znanemu formatowi pliku wykonywalnego system wie, czego program potrzebuje,
aby dobrze pracować. Przykładem mogą tu być np. biblioteki współdzielone, do których
aplikacja musi się odwoływać – w odpowiednim, określonym standardem miejscu pliku
wykonywalnego, zawarte są odwołania do potrzebnych bibliotek czy nawet do
interpretera, który poprawnie załaduje i wykona kod (w przypadku formatu ELF segment
PT_INTERP
).
Proces to, jak wspomnieliśmy wcześniej, uruchomiony program, a nieco bardziej precyzyjnie pod względem językowym wykonywanie się programu. Jeżeli więc procesor wykonuje umieszczony w pamięci operacyjnej kod maszynowy, to możemy mówić o procesie uruchamiania.
W perspektywie systemowej procesem nazwiemy nie tylko akt realizowania zadania
przez system, ale także pewną bardziej namacalną jakość: strukturę danych
w pamięci zarządzanej przez jądro, z użyciem której OS kontroluje prawidłowy przebieg
procesu – m.in. dba o sprawiedliwy przydział czasu, rezerwowanie pamięci, dostęp do
zasobów sprzętowych czy do mechanizmów wymiany danych z innymi procesami. Oznacza to,
że każde wczytanie programu do pamięci w celu jego uruchomienia wiąże się
z utworzeniem odpowiedniej struktury kontrolnej (np. w systemie GNU/Linux
task_struct
zdefiniowanej w pliku nagłówkowym sched.h
), zawierającej
np. identyfikatory związane z uprawnieniami (UID, GID, EUID, EGID, SUID, SGID, FSUID,
FSGID), informacje o rodzicu i potomkach, dane dotyczące priorytetyzacji itd.
Sposobem na wczytanie programu z dysku i uruchomienie go jest zwrócenie się do jądra
systemowego z prośbą o wykonanie takiej operacji. W tym celu uruchomiony program
powinien skorzystać z funkcji systemowej (ang. system call) o nazwie execve
,
której deklaracja w języku C wygląda następująco:
Widzimy, że przyjmuje ona trzy argumenty: nazwę ścieżkową pliku do wczytania
(w postaci wskaźnika do łańcucha znakowego zakończonego znakiem o kodzie 0),
argumenty linii komend (w postaci stałego wskaźnika do tablicy łańcuchów
znakowych) i środowisko (również stały wskaźnik do tablicy łańcuchów
znakowych). Tablice przechowujące argumenty i środowisko powinny być zakończone
elementem, który jest wskaźnikiem pustym (NULL
), a każdy z łańcuchów znakowych
(czyli tablic znakowych) podobnie jak w przypadku pierwszego argumentu powinien być
zakończony znakiem o kodzie 0.
Kernel sprawdzi, czy proces ma prawo uruchomić program i spróbuje wczytać go z pliku
do wolnego miejsca w pamięci. Jeżeli ta operacja się powiedzie, bieżący proces
(w rozumieniu struktury utrzymywanej przez jądro systemowe) zostanie skojarzony
z nowo załadowanym kodem maszynowym, który zacznie być wykonywany. Oznacza to, że
z chwilą pomyślnego wywołania execve()
obraz programu realizowanego do tej pory
w ramach bieżącego procesu przestaje istnieć, zwalniane są także związane z nim
struktury pamięciowe (segment kodu, segment danych zainicjowanych, segment danych
niezainicjowanych oraz segmenty stosu i sterty). Dziedziczony jest oczywiście
należący do procesu identyfikator (ang. process identifier, skr. PID).
W związku z powyższym potrzebujemy jeszcze mechanizmu, który pozwoliłby uruchomionemu programowi na „powołanie do życia” dodatkowego procesu, aby to ten drugi wczytał program kończąc własną, dotychczasową pracę. W przeciwnym razie w systemie moglibyśmy mieć uruchomioną tylko jedną aplikację w tym samym czasie – w obrębie pojedynczego procesu zmieniałyby się po prostu obrazy programów i dane.
Na szczęście istnieje odpowiednia funkcja systemowa, a nosi ona nazwę
fork
. Wywołujący ją proces staje się procesem macierzystym (ang. parent
process) względem nowo powołanego, współdzieli z nim też kod i większość
parametrów. Proces potomny (ang. child process) może rozpoznać, że jest właśnie
nim, ponieważ w jego wątku wykonywania fork()
zwróci wartość 0. Nowy proces może
już bezpiecznie wywołać execve()
i wczytać „w siebie” program. Z kolei proces
macierzysty może nadzorować wykonywanie się potomka w oczekiwaniu na jego zakończenie
– właśnie w ten sposób zachowuje się powłoka, gdy wydamy jakieś polecenie zewnętrzne.
Komunikacja międzyprocesowa
Programy komunikujące się wyłącznie z użytkownikiem i okazjonalnie zwracające się do kernela o dostęp do zasobów są nudnymi programami, a gdyby mogły marzyć, to najprawdopodobniej ich jedyną fantazją byłoby się zawiesić. Najlepiej czują się „rozmawiając” ze sobą. Właśnie dlatego architekci systemowi wpadli kiedyś na pomysł zaimplementowania metod komunikacji międzyprocesowej (ang. inter-process communication, skr. IPC), czyli wymiany informacji między uruchomionymi programami.
Wyróżnić możemy komunikację lokalną (w obrębie tego samego systemu operacyjnego) i zdalną (różne systemy, np. połączone przez sieć). Przykładami tej pierwszej są:
- sygnały (ang. signals),
- semafory (ang. semaphores),
- pamięć dzielona (ang. shared memory),
- kolejki komunikatów (ang. message queues),
- łącza komunikacyjne (ang. pipes),
- łącza nazwane (ang. named pipes),
- gniazda dziedziny Uniksa (ang. Unix domain sockets),
- argumenty linii komend (ang. command-line arguments),
- środowisko (ang. environment),
- zwykłe pliki i pliki mapowane w pamięci (ang. memory-mapped files).
Z kolei przykładem zdalnej komunikacji są m.in. gniazda sieciowe (ang. sockets).
Obiekty IPC tworzone są i usuwane przez kernel po wywołaniu specyficznych funkcji systemowych przez oprogramowanie, które ich potrzebuje. Niektóre z nich pozwalają na dwustronną komunikację (gniazda, pamięć dzielona, pliki), a niektóre tylko na przekazanie wiadomości bądź zasygnalizowanie jakiegoś stanu, bez uzyskiwania informacji zwrotnej z użyciem tego samego kanału (sygnały, argumenty, środowisko, komunikaty, pojedyncze łącza komunikacyjne).
Z usterką Shellshock związany jest mechanizm z tej drugiej grupy, a dokładniej środowisko.
Środowisko procesu
Środowisko (ang. environment) to mechanizm komunikacji międzyprocesowej, który pozwala w sposób jednokierunkowy przekazać do wczytywanego programu pewien zbiór informacji w postaci zestawu łańcuchów znakowych. Środowisko może być ustawione przez jeden uruchomiony program podczas ładowania do pamięci innego programu. Gdy ten ostatni zacznie pracę będzie dysponował przekazanym mu środowiskiem.
Środowisko służy zwykle do sterowania zachowaniem programu – oczywiście pod warunkiem, że uruchamiany program zrobi użytek z danych środowiskowych. Można więc zapytać: po co korzystać ze środowiska, jeżeli mamy argumenty linii komend, które również to umożliwiają?
Ważną cechą, która odróżnia komunikację z użyciem środowiska od tej, w której używany
argumentów wywoływania, jest dziedziczenie. Realizowane jest ono automatycznie
przez system podczas wydzielania procesu potomnego, a także na zasadzie często
stosowanej konwencji, kiedy w wywołaniu execve()
, czy odwołujących się do niej
execle()
i execvpe()
, do wczytywanego programu przekazywana jest kopia bieżącego
środowiska. Również często używane do ładowania programów funkcje – jak
np. execl()
, execlp()
, execv()
i execvp()
, system()
czy popen()
–
przekazują zastane środowisko.
Rozmiar przekazywanego środowiska jest ograniczony. W nowszych kernelach Linux zależy
od wartości RLIMIT_STACK
, którą może ustawić administrator dla pewnych grup
procesów i wynosi maksymalnie 1⁄4 rozmiaru stosu (aby stos nie był w całości zajęty
przez środowisko). System dba też o to, aby mimo limitów rozmiar nie był nigdy
mniejszy niż 32 strony pamięciowe (co przy stronie o wielkości 4 KB daje limit na
poziomie 128 KB). Z kolei limit pojedynczego łańcucha znakowego określany jest
stałą kernela MAX_ARG_STRLEN
i wynosi również 32 strony (w jądrze Linux
2.6.23). Maksymalna liczba przekazywanych łańcuchów to z kolei 0x7FFFFFFF
(dziesiętnie 2 147 483 647) – wynika to z zakresu typu int32_t
(32-bitowa wartość
całkowita ze znakiem).
Na poziomie systemowym jedynym wymogiem jest, aby środowisko było tablicą łańcuchów
znakowych (czyli tablicą znakowych tablic) zakończoną wskaźnikiem pustym (NULL
),
a każdy z umieszczonych w niej wskaźników odwoływał się do tablicy znakowej
zakończonej znakiem o kodzie 0. Nie muszą być to nawet zgodne z konwencją pary
nazwa–wartość, lecz dowolne dane, na przykład:
Podczas ładowania nowego programu z użyciem execve()
uruchamianej aplikacji można
przekazać środowisko, czyli wspomnianą tablicę. W systemie GNU/Linux będzie po
skopiowaniu przechowywane w strukturze mm_struct
(pola env_start
i env_end
), do
której pole wskaźnikowe nazwane mm
znajdziemy we wspomnianej wcześniej strukturze
kontrolnej task_struct
określającej wątek zadania wykonywanego przez system.
Spróbujmy stworzyć prosty program, który utworzy odpowiednie środowisko i przekaże je
do wywołanej powłoki /bin/bash
. Powłoce podamy odpowiednią opcję, dzięki której
wyświetli włsne środowisko i zakończy pracę.
Stwórzmy plik podaj-srodowisko.c
o następującej zawartości:
Skompilujmy go używając GCC, a następnie uruchommy:
Naszym oczom powinna ukazać się lista przyporządkowań podobna do poniższej:
calkiem poprawne
srodowisko przekazywane
do uruchamianego programu
A=1
B=2
Co się stało? Wczytaliśmy powłokę, przekazując jej odpowiednio stworzone środowisko,
zawierające dwie pary nazwa–wartość oraz kilka napisów w języku polskim. Powłoce
przekazaliśmy też opcję (-c
) w postaci argumentu, który instruuje ją, że nie ma
oczekiwać na interaktywnie wprowadzane polecenia, lecz wykonać podaną sekwencję
komend (c
od command), którą można czytelniej przedstawić jako:
Osoby zaznajomione z poleceniem wewnętrznym set
powłoki i programami typu env
oraz printenv
mogą zapytać dlaczego nie skorzystaliśmy właśnie z nich. W przypadku
dwóch ostatnich powodem jest fakt, że wywołane byłyby w procesie potomnym,
ponieważ są zewnętrznymi komendami, gdy zaś chodzi o set
, wyświetlone
zostałyby wszystkie zmienne (i funkcje) powłokowe, a nie środowisko, nawet jeżeli
niektóre z tych zmiennych pochodziłyby ze środowiska bądź były przeznaczone do tego,
aby się tam znaleźć. W każdym z przypadków środowisko bieżącego procesu powłoki nie
byłoby dobrze odwzorowane.
Użycie pseudosystemu plikowego procfs pozwala nam na dostęp do pseudopliku
/proc/[PID]/environ
, którego zawartość odzwierciedla środowisko procesu dla
podanego jako [PID]
identyfikatora procesu (ang. process identifier,
skr. PID). Aby poznać własny identyfikator procesu powłoki, korzystamy ze zmiennej
specjalnej $$
.
Uzyskane dane są w „surowej”, nieprzetworzonej formie i konieczna jest zamiana
wszystkich znaków o kodzie zero na znaki nowej linii. Tu z pomocą przychodzi
polecenie zewnętrzne tr
, którym można łatwo tego dokonać.
W rezultacie na ekranie pojawi się dokładnie takie środowisko, jakiego byśmy oczekiwali, zawierające nawet umieszczone przez nas napisy. Czy Bash korzysta z nich, czy też nie, to już inna sprawa. Można to sprawdzić nieco modyfikując nasz program:
W rezultacie otrzymamy:
A=1
B=2
PWD=/home/randomseed
SHLVL=0
Bystre oko wprawnego czytelnika zauważy, że wśród wyświetlonych informacji brakuje niektórych przekazanych łańcuchów. Hańba! Skandal! Kto za to odpowiada?
W poprzednim przykładzie wykluczyliśmy usuwanie naszych fraz ze środowiska przez sam
proces powłoki, pozostaje zatem możliwość ignorowania ich przez Basha. Zauważmy,
że wywołanie /bin/bash -c exec printenv
zastępuje proces powłoki wczytywanym
programem (odpowiada za to wewnętrzne polecenie exec
). Wyświetlona zawartość
nie jest więc odzwierciedleniem środowiska oryginalnie uruchomionego programu
powłoki, ale zastępującego go printenv
.
Skąd więc znajdują się tam wpisy zarówno pochodzące z pierwotnie utworzonego
środowiska (A=1
, B=2
), jak i nowe, nieznanego pochodzenia (PWD=/home/randomseed
,
SHLVL=0
)? Okazuje się, że powłoka skopiowała własne środowisko i dodając do
niego pewne dane, użyła tak skonstruowanego zbioru do zainicjowania środowiska dla
ładowanego programu printenv
.
Wniosek: powłoka ignoruje wpisy w środowisku, jeżeli nie potrafi ich przetworzyć (są dla niej niezrozumiałe), nie są one również kopiowane do środowisk procesów uruchamianych przez powłokę, jednak nie usuwa ich z własnej pamięci środowiska.
Warto zapamiętać, że korzystając z Basha lub podobnej powłoki uniksowej nie
operujemy bezpośrednio na środowisku procesu, nawet gdy w podręcznikach obsługi
powtarzają się słowa dotyczące dodawania lub usuwania ze środowiska pewnych
danych. Używając wewnętrznych poleceń, takich jak set
(typeset
), unset
czy
export
, wpływamy na struktury danych powłoki, lecz nie na obszar pamięci faktycznie
związany ze środowiskiem. Jest on zmieniany i odpowiednio ustawiany leniwie,
tzn. w momencie, gdy jest to konieczne – na przykład, kiedy ładowany jest inny
program (w bieżącym procesie lub nowo powstałym procesie potomnym).
Powyższe można zaobserwować kompilując i uruchamiając następujący kod:
Wiemy już jak środowisko jest przekazywane, warto więc nadmienić o sposobach dostępu do własnego środowiska przez uruchomiony program. Pierwszy sposób wykorzystuje argumenty funkcji głównej:
Drugi zmienną zewnętrzną environ
, którą definiuje standardowa biblioteka języka C:
Zmienne środowiskowe
Zmienna środowiskowa (ang. environment variable) to zmienna (ang. variable), czyli konstrukcja posiadająca symboliczną nazwę oraz wartość, której miejscem przechowywania jest środowisko uruchomionego programu. We wcześniejszych przykładach staraliśmy się unikać tego pojęcia, aby podkreślić, że dla systemu operacyjnego nie jest istotne, czy w środowisku będą przechowywane zmienne czy inne konstrukty.
Bardziej konserwatywnie do środowiska podchodzą programiści powłok, wielu aplikacji czy nawet standardowej biblioteki języka C. Dla nich środowisko to repozytorium par nazwa–wartość, np.:
Zauważmy, że nie mamy tu zdefiniowanych typów danych dla podanych wartości (wszystkie to pierwotnie łańcuchy znakowe). Typizowanie jest więc kwestią konwencji lub detekcji i zależy od otrzymującego informacje procesu.
Wspomniana już standardowa biblioteka języka C definiuje odpowiednie funkcje,
ułatwiające zarządzanie zmiennymi środowiskowymi: getenv()
, setenv()
,
unsetenv()
, putenv()
i clearenv()
. Wszystkie poza ostatnią służą do zarządzania
wyłącznie zmiennymi, które wykrywane są w ten sposób, że muszą zawierać znak
=
. Jeżeli w środowisku umieszczony byłby łańcuch znakowy inny niż określający
zmienną, to nie będzie możliwe jego wybiórcze usunięcie czy nadpisanie (zastąpienie
zmienną o takiej nazwie) z użyciem tych właśnie funkcji.
Środowisko w Bashu
W przypadku Basha mamy do czynienia z ciekawą sytuacją – zaimplementowano tam własne
wersje funkcji
odpowiedzialnych za obsługę zmiennych środowiskowych. Zmiany polegają głównie na tym,
że zapisy i odczyty zmiennych są buforowane, tzn. nie operują na środowisku, lecz
na wewnętrznych strukturach. Z nich dopiero na bieżąco konstruowane są środowiska
przekazywane procesom potomnym lub procesowi zastępującemu powłokę (polecenie
wewnętrzne exec
).
Gdy przyjrzymy się, w jaki sposób odbywa się rozruch systemów typu Unix – pamiętając,
że jedynym sposobem wytworzenia nowego procesu jest rozwidlenie wykonywania bieżącego
z użyciem funkcji fork()
– dostrzeżemy zaletę wynikającą z dziedziczności
środowiska. Skrypty startowe systemu mogą ustawiać pewne zmienne środowiskowe,
przydatne dla wszystkich działających programów, które zostaną potem rozpropagowane
i trafią do uruchamianych skryptów konkretnych usług, a te z kolei mogą dołączać do
środowisk zmienne specyficzne dla nich. Podobnie rzecz się ma z ustawieniami
środowiskowymi aplikacji uruchamianych przez użytkownika – dziedziczą środowiska po
procesach powłoki, które z kolei przyjmują je od usług zapewniających dostęp do
systemu (np. login
czy sshd
) i tak dalej.
Niektóre aplikacje umożliwiają zarządzanie zmiennymi z ich środowiska bezpośrednio przez użytkownika (z użyciem interaktywnego interfejsu, komunikacji międzyprocesowej lub plików konfiguracyjnych). Właściwość ta przydaje się wtedy, gdy trzeba ustawić odpowiednie zmienne, mające wpływ na pracę programów, lecz nie można tego zrobić z poziomu powłoki (będącej procesem rodzicielskim), bo np. program nie jest uruchamiany przez powłokę lub startuje ona zbyt późno (a to jej środowisko chcemy przygotować). Przykładem mogą być tu tabele demona wykonywania cyklicznych zadań Cron, ale też sytuacje, w których chcemy wpłynąć na środowisko procesu (powłoki lub innego programu) w momencie nawiązania zdalnego połączenia SSH (wysyłając środowisko przez sieć).
Zmienne i funkcje
Interpreter powłoki Bash pozwala korzystać ze zmiennych i funkcji. Te pierwsze służą
do przechowywania danych, a drugie do tworzenia podprogramów i operowania na tych
pierwszych. Zmienna w Bashu może mieć ustalony zasięg (ang. scope), który określa
gdzie w treści skryptu może być ona użyta. Domyślnie zmienne są globalne
(ang. global), to znaczy zdefiniowane będą widoczne w całym skrypcie realizowanym
przez bieżący proces. Można to zachowanie zmienić z użyciem modyfikatora zasięgu
local
umieszczanego przed nazwą zmiennej w trakcie jej pierwszego użycia. Taka
zmienna będzie widoczna tylko w obrębie funkcji, w której się pojawiła.
Częstym błędem początkujących jest zakładanie, że zmienne globalne będą współdzielone
z procesami potomnymi powłoki. Chodzi tu konkretnie o polecenia, które z racji
przyjmowania wyjścia z innych poleceń zmuszają Basha do tworzenia dodatkowego wątku
wykonywania. Przykładem może być tu komenda read
, która wczytuje dane pochodzące ze
standardowego wyjścia polecenia echo
:
Po uruchomieniu, zamiast spodziewanego 123
, wartością zmiennej l
będzie napis
pusta
. Dlaczego? Na potrzeby obsługi przekierowanego wyjścia z echo
(z użyciem
operatora potoku, który tworzy łącze komunikacyjne) powłoka utworzyła proces potomny
i to w nim wykonywane było polecenie wewnętrzne read
nadające wartość
l
. Wszystkie dane tego dodatkowego procesu przestały istnieć, gdy zakończył on
pracę, włączając w to zmienną l
, która ma co prawda globalny zasięg, ale w obrębie
danego procesu, do którego danych proces macierzysty nie ma bezpośredniego
dostępu. Rozwiązaniem podobnych problemów może być rezygnacja z przekierowania
i użycie zmiennej przechowującej wynik wcześniej wykonanego polecenia:
Inny sposób to przekierowanie standardowego wyjścia podpowłoki (ang. subshell),
czyli procesu potomnego realizującego osobne polecenie, do standardowego wejścia
komendy read
. W tym przypadku również powstanie proces potomny, lecz nie będzie on
próbował wpływać na zawartość zmiennej l
– będzie to zadaniem procesu
macierzystego:
Więcej przykładów radzenia sobie z subshellami można znaleźć na stronach Bash Hackers Wiki.
Zmienne eksportowane
Istnieje jeszcze jeden rodzaj zmiennych w Bashu, które możemy wyróżnić pod względem zasięgu – zmienne
środowiskowe powłoki, nazywane również zmiennymi eksportowanymi
(ang. exported variables). Są to zmienne o zasięgu globalnym, które zostaną
umieszczone w środowisku każdego procesu potomnego powłoki i każdego programu
zastępującego powłokę (w przypadku użycia wbudowanego polecenia exec
).
Termin „eksportowane” nie jest zbyt popularny, lecz dobrze określa różnicę między rzeczywistymi zmiennymi środowiskowymi (przechowywanymi w środowisku danego procesu), a zmiennymi globalnymi powłoki, które są tylko oznaczone jako te, które powinny się tam znaleźć.
Do oznaczenia zmiennej jako przeznaczonej do umieszczenia w środowisku używa się
modyfikatora export
, który może być zastosowany w dowolnym momencie, np.:
Funkcje środowiskowe
Funkcja środowiskowa (ang. environmental function) to pomysł programistów Basha na propagowanie podprogramów wśród potomnych procesów powłokowych. Przypomina ona znaną z dowcipów świnkę morską, która w rzeczywistości nie jest ani świnką, ani tym bardziej morską. Spójrzmy:
Po wykonaniu skryptu otrzymamy rezultat podobny do poniższego:
Co zrobiliśmy? Stworzyliśmy funkcję Basha, a następnie oznaczyliśmy ją jako
eksportowalną z użyciem export -f
. W środowisku procesu potomnego (printenv
)
pojawiła się jako zmienna środowiskowa o nazwie funkcja
i zawartości dokładnie
takiej, jak ciało naszej funkcji.
Warto zauważyć, że interpreter nie miał kłopotów z przeniesieniem do środowiska znaku =
; jego wewnętrzny parser rozpoznaje wartości zmiennych środowiskowych jako łańcuchy umieszczone między pierwszym wystąpieniem =
a znakiem o kodzie 0, oznaczającym koniec tekstu.
Tego typu zmienne środowiskowe (przenoszące funkcje Basha) propagowane są do wszystkich procesów potomnych, jednak tylko Bash (ew. inne powłoki) są w stanie ich potem użyć. Przypominają się dawne czasy, ale też obecne języki programowania, w których dzięki instrukcji eval
dane mogą stawać się kodem.
Automatyczne definiowanie funkcji
Obecne w środowisku zmienne, które w rzeczywistości zawierają funkcje Basha, są rozpoznawalne przez parser po umieszczonej na początku wartości parze okrągłych nawiasów i otwierającym nawiasie klamrowym. Jeżeli powłoka napotka taki wzorzec, to łańcuch znaków ze środowiska będzie przekazany do interpretera w celu zdefiniowania funkcji w bieżącym procesie. Na przykład:
Uwaga: ten przykład nie zadziała w nowych, poprawionych wydaniach Basha.
W powyższym przykładzie wywołaliśmy w procesie potomnym funkcję funkcja()
, która
nawet nie istniała w procesie macierzystym, lecz została w nim zapisana jako
zmienna przeznaczona do umieszczenia w środowisku. Wywoływana raz jeszcze powłoka
Bash (bash -c funkcja
) sprawdziła otrzymane środowisko i wykryła, że ma do
czynienia z eksportowaną funkcją. Została więc ta ostatnia zdefiniowana pod nazwą
funkcja
, a następnie wywołana (z powodu przekazanej jako argument opcji -c
funkcja
).
Niedoskonałości
Parser środowiska obecny w Bashu nie jest tak doskonały, jak mogłoby się wydawać. Okazuje się, że gdy trafi na odpowiednio skonstruowaną zmienną środowiskową, zawierającą w domyśle funkcję do zdefiniowania, to może też wykonywać dodatkowe komendy. Przypatrzmy się następującemu przykładowi:
Przykład jest podobny do poprzedniego, lecz tym razem oprócz ciała funkcji ujętego
w klamry umieściliśmy po separatorze komend (średniku) polecenie echo
. W efekcie na
ekranie ukazały się dwa napisy:
jestem poza
jestem funkcja
Uwaga: ten przykład nie zadziała w nowych, poprawionych wydaniach Basha.
Okazuje się, że proces interpretowania zmiennej środowiskowej jako kodu definiującego
funkcję nie zatrzymuje się na zamykającym nawiasie klamrowym, lecz trwa do
osiągnięcia końca łańcucha z wartością. Dlatego w momencie startu powłoka wyświetliła
napis jestem poza
(komenda echo
została wydana w momencie odczytu środowiska),
a dopiero potem pojawił się rezultat wywołania funkcji funkcja()
.
Programiści Basha szybko poprawili powyższy błąd, eliminując możliwość dodawania poleceń dopisanych za definicją funkcji. Okazuje się jednak, że nie chroni to przed wszystkimi zagrożeniami…
Luka Shellshock
W dniu 24 września 2014 roku świat IT obiegła sensacyjna informacja: w popularnej powłoce Bash od blisko 15 lat istniała luka (CVE–2014–6271), dzięki której można było lokalnie, a w pewnych warunkach również zdalnie wykonywać dowolne polecenia z uprawnieniami usługi. Jedynym warunkiem jest możliwość kontrolowania środowiska procesu powłoki. Jej odkrywcą okazał się Stéphane Chazelas – kierownik działu IT z brytyjskiej spółki SeeByte Ltd, który pasjonuje się otwartym oprogramowaniem, w szczególności takim, które działa na systemach Unix i GNU/Linux.
Usterka polegała na możliwości przemycania dowolnych komend przez doklejenie ich do zmiennych środowiskowych zawierających definicje funkcji powłokowych. Dokładnie ten mechanizm pokazaliśmy w ostatnim przykładzie, chociaż w Sieci można znaleźć skróconą wersję kodu obrazującego błąd w postaci:
Dlaczego administratorzy zareagowali paniką, a przygotowujący pakiety oprogramowania dystrybutorzy GNU/Linuksa oraz innych Uniksów masowo zaczęli odwiedzać specjalistów ds. zdrowia psychicznego i gabinety masażu? Przecież usterkę można wykorzystać tylko, gdy już kontroluje się środowisko powłoki, a więc – zgodnie z wcześniejszymi przykładami – użytkownik musiałby sabotować sam siebie!
Niestety okazuje się, że przez lata twórcy różnych narzędzi i usług sieciowych zaufali zmiennym środowiskowym jako mechanizmowi dobrze poznanemu i takiemu, który wydaje się zbyt prosty, aby można w nim było znaleźć jakieś poważniejsze słabe punkty. Samo środowisko (z systemowego punktu widzenia) z pewnością takie jest, problem powstaje wtedy, gdy wszędobylski Bash czyni z niego niewłaściwy użytek i zamienia dane w instrukcje.
Dlaczego wszędobylski? Wiele funkcji bibliotecznych i podprogramów skryptowych
najprzeróżniejszych interpreterów, gdy chce uruchomić jakiś proces, to nie ładuje go
bezpośrednio, lecz jako komendę powłoki przeznaczoną do wykonania. Dla przykładu
skrypt webowy może używać wywołanego przez powłokę polecenia convert
do zmiany
rozmiarów obrazków:
Dzięki takiemu podejściu programista nie musi tworzyć osobnych mechanizmów ustawiania
zmiennych przeszukiwania (tj. PATH
czy LD_LIBRARY_PATH
), plików konfiguracyjnych,
które pozwolą uruchomić odpowiednie programy, dbające o przygotowanie środowiska
pracy itd. Wystarczy scedować te nudne i żmudne czynności na powłokę wraz z jej
globalnymi i należącymi do użytkownika plikami konfiguracyjnymi.
Przykłady, gdy środowisko uruchamianego programu jest kontrolowane przez niezaufanego klienta, lub gdy jest on zaufany, ale może niebezpiecznie zwiększyć własne uprawnienia do wykonywania pewnych operacji obejmują:
niektóre serwery poczty elektronicznej, w szczególności czyniące użytek z powłoki podczas doręczania wiadomości do zewnętrznych filtrów (np. Procmaila) czy bazujące na krótkich, elastycznych skryptach (qmail);
serwer WWW Apache z włączonym mechanizmem CGI (moduł
mod_cgi
lubmod_cgid
), jeżeli uruchamiane skrypty stworzone są w Bashu lub wywołują go, np. z użyciem wspomnianych wcześniej funkcjisystem()
czypopen()
;serwer OpenSSH, gdy włączona jest opcja
ForceCommand
, której funkcją jest ograniczanie możliwości wykonywanych poleceń, a także gdy użytkowane są klucze publiczne z klauzulącommand
(wstrzyknięcie komend omija ograniczenie);klienty DHCP, gdy wykonując skrypty przekazują do nich parametry używając zmiennych środowiskowych stworzonych na podstawie danych otrzymanych od serwerów – tu w wielu przypadkach możliwe jest wykonanie szkodliwego kodu z uprawnieniami administratora;
pliki wykonywalne z włączonym ustawianiem identyfikatora użytkownika podczas wykonania (ang. set user ID upon execution, skr. setuid) lub z ustawianiem identyfikatora grupy podczas wykonania (ang. set group ID upon execution, skr. setgid) gdy dziedziczą środowisko – możliwe jest wtedy rozszerzanie uprawnień i wykonywanie poleceń jako inny użytkownik czy nawet administrator, jeżeli aplikacja uruchamia powłokę, np. z użyciem
system()
lubpopen()
.
Przykład podatnego serwera WWW
Spójrzmy w jaki sposób zdalny napastnik może wykorzystać usterkę Shellshock atakując podatny na zagrożenie serwer WWW. Warunkiem koniecznym jest tu przekazywanie nagłówków protokołu HTTP pochodzących od klienta do serwera aplikacyjnego w postaci zmiennych środowiskowych. Serwerem aplikacyjnym może być na przykład skrypt CGI uruchamiany za każdym razem, gdy konieczna jest obsługa żądania.
Załóżmy, że klient ustawił np. nagłówek Cookie
w taki sposób:
Cookie:() { :; }; ping -c 3 randomseed.pl
Po nawiązaniu połączenia HTTP trafi on do serwera WWW, który chcąc przekazać nagłówek aplikacji CGI doda zmienną środowiskową:
HTTP_COOKIE=() { :; }; ping -c 3 ransomseed.pl
Wywołany interpreter (np. PHP) przyjmie środowisko i chcąc przeskalować obrazek wykona:
Użyta tu funkcja PHP o nazwie
system
jest odpowiednikiem tej
samej funkcji ze standardowej biblioteki języka C. Ta ostatnia z kolei nie uruchomi
programu convert
bezpośrednio, lecz wywoła powłokę przekazując jej polecenie
i argumenty. Powłoka odziedziczy środowisko i jeżeli jest to Bash, to zacznie
importować funkcje, których definicje znajdzie w wartościach zmiennych.
W przypadku podatnej na lukę wersji interpretera z uprawnieniami serwera
aplikacyjnego wykonane zostanie polecenie ping -c 3 randomseed.pl
, które wyśle trzy
żądania echo-request
i w ten sposób poinformuje drugą stronę o tym, że serwer
korzysta z zagrożonego wydania Basha.
Pierwsza łata
Programiści Basha szybko wydali odpowiednią łatkę (ang. patch), określoną identyfikatorem bash43-025, która eliminowała usterkę.
Poprawka polegała na wprowadzeniu dwóch trybów przetwarzania do funkcji
parse_and_execute()
– identyfikowanych przez SEVAL_FUNCDEF
i SEVAL_ONECMD
. W przypadku wywołania w tym pierwszym trybie, parser pomija komendy
inne niż definicje funkcji. Z kolei drugi tryb zapobiega uruchamianiu więcej niż
jednej znalezionej komendy. W efekcie przedstawiony wcześniej exploit już nie
zadziała, ponieważ nie będą zinterpretowane polecenia wprowadzone za ciałem funkcji.
Usterka bufora
Ulga użytkowników i administratorów nie trwała długo. Tavis Ormandy, współpracujący z koncernem Google specjalista zajmujący się usterkami w oprogramowaniu, znalazł podobną lukę. W bazie usterek musiano więc zarejestrować kolejną podatność na zagrożenie – otrzymała ona identyfikator CVE–2014–7169.
Szkodliwy kod powłokowy demonstrujący błąd wyglądał następująco:
Efekt wywołania tego polecenia na podatnym interpreterze spowoduje utworzenie pliku
o nazwie echo
, zawierającego rezultat wykonania komendy date
, a następnie
wyświetlenie jego zawartości na ekranie. Użyta metoda wprowadzenia Basha w błąd też
wykorzystuje słabość parsera przy wczytywaniu zawartości środowiska, lecz metoda jest
zgoła inna. Mamy w niej do czynienia z celowym zaburzeniem pracy bufora służącego do
przechowywania wykonywanych poleceń.
Aby wyjaśnić, co dokładnie się dzieje, posłużymy się bardziej przystępnym przykładem:
Wiemy, że podczas uruchamiania nowego procesu powłoki (bash
) analizowane są zmienne
środowiskowe. Zawartość jednej z nich z powodu początkowych nawiasów okrągłych
i otwierającej klamry traktowana jest jak początek definicji eksportowanej funkcji
powłokowej. W tym momencie warto zauważyć, że Bash nie wykonuje poleceń niezwłocznie,
lecz trafiają one wcześniej do specjalnego bufora. Jest on pomocny, ponieważ niektóre
elementy składniowe wymagają analizy konstrukcji sąsiadujących z nimi. Jaskrawym
przykładem jest zapis:
Parser nie realizuje komendy echo
, lecz umieszcza ją w buforze, ponieważ odwrócony
ukośnik na końcu linii informuje, że wyrażenie będzie miało dalszy ciąg.
W naszym przykładzie, podczas analizy środowiska, bufor przedstawia się następująco:
function a a>\
Dla interpretera zrozumiała jest tylko część function a a
(definicja funkcji
zagnieżdżonej) i ona jest od razu przetworzona. W buforze zostaje więc:
>\
Celem pozostawienia sekwencji >\
jest próba wpłynięcia na sposób, w jaki
potraktowane będzie kolejne polecenie (echo date
), które też trafi do
bufora. Teraz, „zanieczyszczony” poprzednią operacją, będzie wyglądał tak:
>\
echo date
Zauważmy, że po odwróconym ukośniku mamy znak nowej linii, która zgodnie z syntaktyką Basha oddziela kolejne polecenia. W normalnych warunkach potraktowany byłby on właśnie jako separator, jednak przemycony backslash sprawia, że powstaje sekwencja unikowa (ang. escape sequence) i specjalne znaczenie nowej linii zostaje unieważnione. W efekcie bufor zawiera wyrażenie równoważne zapisowi:
>echo date
Jest to z kolei gramatyczny odpowiednik wyrażenia:
date > echo
Rezultatem będzie więc wywołanie polecenia date
i przekierowanie wyjścia do pliku
o nazwie echo
.
Kolejny patch
(bash43-026)
eliminuje tę lukę wyłączając w pewnych warunkach zachłanne buforowanie napływających
danych (eol_ungetc_lookahead = 0;
).
Konflikty nazw
Sytuację dobrze podsumował w prowadzonym przez siebie blogu Michał Zalewski (lcamtuf) – pochodzący z Polski i również współpracujący z Google’em badacz bezpieczeństwa IT. Zwrócił on uwagę na to, że dotychczas proponowane poprawki są raptem obejściami, których funkcją jest zapobieganie zdalnemu wykonywaniu kodu (RCE), a nie rozwiązują problemu u źródła.
Zdaniem Zalewskiego, poza oczywistym uszczelnieniem parsera należałoby również wprowadzić wyraźną separację składowanych w środowisku funkcji i zmiennych. Proponowane przez niego podejście wiązałoby się z wydzieleniem osobnej przestrzeni nazw (ang. namespace), czyli umieszczeniu funkcji w środowisku w taki sposób, aby nie były one nigdy pomylone z innymi przechowywanymi tam informacjami, tzn. aby nie występowały konflikty nazw. Zauważmy, że nawet stosując ochronę w postaci poprawek niedopuszczających do wykonania komend Basha w trakcie wczytywania środowiska, wciąż istnieje ryzyko, że pochodząca np. z serwera WWW zmienna środowiskowa będzie miała taką samą etykietę jak zmienna powłokowa sterująca wykonywaniem się skryptu lub wywoływanego przez shella polecenia zewnętrznego.
Lcamtuf zasugerował też wprowadzenie opcji przekazywanej jako argument linii komend, której funkcją byłoby wybiórcze włączanie możliwości eksportowania funkcji. Bez stosownego parametru wykonywanie takich operacji byłoby niedopuszczalne. Problemem okazała się jednak konieczność uwzględnienia wstecznej kompatybilności tak „uzbrojonego” interpretera – niektóre skrypty wykorzystywane przez użytkowników czy narzędzia do testowania dostarczanych pakietów z oprogramowaniem czynią użytek z funkcji powłokowych przekazywanych między procesami.
Użycie osobnych przestrzeni nazw wprowadza stworzony przez Floriana Weimera z Red
Hata patch
bash43-027. Definiuje
on stałą FUNCDEF_PREFIX
zawierającą napis "BASH_FUNC_"
, a następnie ogranicza
interpretowanie wartości zmiennych środowiskowych jako definicji funkcji do takich,
których nazwa rozpoczyna się od tego napisu. Dodaje też stałą FUNCDEF_SUFFIX
,
ustawioną na parę nawiasów okrągłych, dzięki której sprawdzane jest czy nazwa kończy
się właśnie nimi.
Oto przykład eksportowanych funkcji obecnych w środowisku procesu potomnego Basha z zaaplikowaną poprawką:
Problemy z tablicami parsera
Wspomniany wcześniej Weimer zgłosił w międzyczasie dwie poprawki dotyczące błędów
w pliku źródłowym parse.y
, które niezależnie odkryte zostały też przez Todd
Sabina z VMware’a.
Jedna z usterek (CVE–2014–7187) dawała o sobie znać, gdy do pojedynczej komendy dołączono wiele dokumentów miejscowych (ang. here-documents). W efekcie pojawiał się błąd dostępu do danych poza zakresem tablicy (ang. out-of-bounds index), prowadzący do awaryjnego przerwania pracy interpretera.
Podatność można sprawdzić wydając sekwencję poleceń:
Drugą lukę (CVE-2014-7187) można było wykorzystać korzystając z dużej liczby zagnieżdżonych pętli. Przyczyna była podobna jak w pierwszej – dostęp do elementu tablicy poza zakresem, z tym że wywołany błędem pomyłki o jeden (ang. off-by-one).
Podatność na występowanie usterki można sprawdzić uruchamiając poniższy skrypt:
Łata, oznaczona jako bash43-028, eliminuje te problemy przez wprowadzanie liczników wywołań.
Finał
Pierwszego października 2014 wspomniany już wcześniej Michał Zalewski opublikował szczegóły dotyczące trzymanych przez niego dotąd w tajemnicy usterek bezpieczeństwa Basha (CVE–2014–6277 i CVE–2014–6278).
Pierwszy problem dotyczył błędu niezainicjowanego wskaźnika w funkcji
make_redirect()
obsługującej strukturę REDIR
. Jest to usterka podobna do
CVE–2014–7186, wykorzystująca również problemy parsera dokumentów
miejscowych. W zależności od flag kompilacji Basha luki tej można użyć albo do ataków
typu DoS (wywołując awaryjne przerwanie pracy interpretera), albo (w sprzyjających
warunkach) do przemycenia i wykonania dowolnego kodu w postaci przekazywanych
parametrów i wpływania na wartości lokalnych zmiennych.
A sprawdzić podatność na występowanie usterki można posłużyć się poniższym skryptem:
Kolejny błąd, znaleziony przez lcamtufa z użyciem zestawu „magicznych fuzzerów” jego
autorstwa, polega na niepożądanej zmianie zawartości bufora interpretera przez użycie
sekwencji zagnieżdżonych symboli $
. Problem występuje najprawdopodobniej w funkcji
xparse_dolparen()
i zmianach wprowadzonych w Bashu 4.2 (patch level 12).
Podatność na występowanie usterki można sprawdzić uruchamiając poniższy skrypt:
Poprawki i aktualizacje
Większość dystrybucji systemu GNU/Linux wydało już stosowne poprawki i uaktualnienia eliminujące opisywane tu problemy. Nieco w tyle pozostaje koncern Apple, którego systemy Mac OS X z opóźnieniem otrzymują aktualizacje. Użytkownicy spekulują, że chodzi tu prawdopodobnie o licencję: nowy Bash wydawany jest zgodnie z zasadami ponownego wykorzystania zdefiniowanymi w licencji GNU GPL w wersji 3, która jest bardziej restrykcyjna niż GNU GPL w wersji 2, a to właśnie tej ostatniej stara trzymać się koncern Apple, włączając do wydawanych przez siebie systemów pakiety wolnego oprogramowania.
Jeżeli chodzi o dwie ostatnie luki, to nie są one jeszcze w pełni zamknięte (w wersji Basha 4.3 patch level 27), ale przed ich wykorzystaniem chronią poprawki Weimera.