Time of check to time of use
Napisał: Patryk Krawaczyński
29/05/2016 w Ataki Internetowe Brak komentarzy. (artykuł nr 528, ilość słów: 1129)
W
rozwoju oprogramowania termin time of check to time of use (TOCTTOU lub TOCTOU) jest klasą błędów typu race conditions spowodowanych zmianami w systemie, które występują pomiędzy czynnościami: sprawdzeniem warunku (np. poświadczeniem bezpieczeństwa), a wykorzystaniem wyników tego sprawdzenia. Przykład: wyobraźmy sobie aplikację webową, która pozwala użytkownikom na edycję wybranych stron oraz administratorom na blokowanie takich modyfikacji. Użytkownik wysyła żądanie do aplikacji o edycję strony i otrzymuje formularz umożliwiający mu przeprowadzenie tej operacji. Po edycji treści, ale przed wysłaniem formularza do aplikacji przez użytkownika – administrator blokuje stronę, co powinno zapobiec jej edycji. Ze względu na fakt, że proces edycji już się zaczął, gdy użytkownik wyślę zawartość formularza (razem z zmienioną treścią) to zostanie ona zaakceptowania. Dlaczego? Gdy użytkownik rozpoczął edycję aplikacja sprawdziła jego uprawnienia i rzeczywiście mógł edytować stronę. Zezwolenie zostało cofnięte później, już po sprawdzeniu autoryzacji dlatego modyfikacje zostały dopuszczone.
Możemy zejść nieco niżej – do języka SQL, który napędza większość aplikacji webowych. Aplikacja przed dodaniem adresu e-mail użytkownika do tabeli z adresami wykonuje zapytanie SELECT
, w celu sprawdzenia, czy już taka wartość nie istnieje. Jeśli zapytanie nie zwróci żadnych wierszy instrukcja INSERT
podąży za wynikiem umieszczając w tabeli nowy wpis. Jedna droga do eksploracji tego schematu postępowania prowadzi atakującego do (prze)uchwycenia proponowanego adresu e-mail i wysłaniu prawie równoległego żądania INSERT
z tą samą wartością. Jeśli żądanie atakującego zostanie wykonane szybciej to prawidłowe zapytanie dostanie błąd odmowy obsługi, ponieważ naruszy integralność klucza głównego. Gdy zapytanie SELECT
zostało wykonane (time to check) i zwróciło pusty zestaw wyników podając, że adres e-mail nie jest jeszcze zarejestrowany zakładało, że stan ten pozostanie nienaruszony do czasu aż INSERT
(time of use) nie zostanie wykonany. W ten sposób atakujący wpłynął na pracę i priorytet procesu poprzez zakłócenie pracy powtarzalnych się po sobie żądań.
Słabość TOCTTOU może występować zawsze wtedy, gdy atakujący ma wpływ na stan obiektu / zasobu pomiędzy sprawdzeniem jego stanu, a jego użyciem. Występować to może w przypadku systemu plików, pamięci, a nawet zmiennych w wielowątkowych programach.
We wczesnych latach 90’tych narzędzie mail w systemie Unix BSD 4.3 posiadało podatność związaną z plikami tymczasowymi, ponieważ do ich tworzenia użyto funkcji mktemp(). Wystarczyło, że osoba atakująca w jednym “oknie” uruchamiała wiele razy program generując nazwy plików tymczasowych, a w drugim tworzyła dowiązania symboliczne próbując zgadnąć kolejne ich nazwy. Atak udawało się często przeprowadzić w mniej niż minutę.
Spójrzmy na przykład odnoszący się do programu napisanego w C:
if (access("file", W_OK) != 0) { exit(1); } fd = open("file", O_WRONLY); write(fd, buffer, sizeof(buffer));
Powyższy program z ustawionym bitem suid posiada błąd TOCTTOU. Mimo, że wywołanie access() sprawdza, czy użytkownik, który wywołał program (czyli sprawdzany jest rzeczywisty identyfikator użytkownika, a nie efektywny) ma możliwość zapisu do pliku to atakujący może wykorzystać lukę między instrukcjami access i open(), aby przykładowo nadpisać wpisy w pliku haseł systemu:
if (access("file", W_OK) != 0) { exit(1); } symlink("/etc/passwd", "file"); fd = open("file", O_WRONLY); write(fd, buffer, sizeof(buffer));
Wywołanie access() zachowuje się zgodnie z oczekiwaniami i zwraca wartość 0, jeśli użytkownik uruchamiający program posiada niezbędne prawa do zapisu pliku ("file"
) oraz -1 jeśli ich nie ma. Niestety, ponieważ access i open operują na nazwie pliku – nie daje nam to gwarancji, że zmienna w tej postaci nadal odwołuje się do tego samego pliku na dysku pomiędzy tymi wywołaniami. Jeśli agresor podmieni plik zaraz po wykonaniu warunku access na link symboliczny do innego pliku (tutaj /etc/passwd
) to program użyje uprawnień administratora, aby wykonać daną operację na pliku docelowym (nawet jeśli jest to plik, do którego atakujący nie posiada wystarczających uprawnień). Przez oszukanie programu do wykonania operacji, które normalnie byłyby niedopuszczalne – napastnik zdobył podwyższone uprawnienia w systemie. Ten typ podatności nie ogranicza się tylko do programów z uprawnieniami root, ale każdej aplikacji, która jest w stanie wykonać operacje w imieniu atakującego. W konsekwencji takich działań napastnik jest w stanie: uzyskać nieautoryzowany dostęp do zasobów, wykonać na nich niepożądane zmiany lub usunąć pliki zawierające wrażliwe dane np. logi.
W generycznym ataku TOCTTOU wymaga precyzyjnego zgrania w celu zapewnienia, że działania atakującego przeplatają się dokładnie z ofiary. Adwersarz musi wykonać swój krok po każdym kroku operacji ofiary – technika ta zwana jest single stepping i znana jest z debugowania programów, które w tym scenariuszu wykonują jedną linię kodu krok po kroku. Techniki zwane labiryntami systemu plików oraz algorytmicznych złożoności prowadzą do single steppingu programów ofiary (“kontrolują” / opóźniają czas wykonywania) poprzez manipulację stanem pamięci podręcznej systemu operacyjnego. Labirynty systemu plików zmuszają ofiarę do odczytywania katalogów z pominięciem pamięci cache. W ten sposób system każe czekać atakowanemu, gdy sam jest zmuszony do odczytu danych bezpośrednio z dysku. W ataku algorytmicznych złożoności ofiara jest zmuszona poświęcić całą ilość zaplanowanego czasu na pojedyncze wywołanie systemowe, które musi przemierzyć całą tablicę mieszającą jądra służącą do cacheowania nazw plików. Dochodzi do takiej sytuacji ponieważ agresor tworzy dużą liczbę plików, których nazwy są wrzucane przez funkcję haszującą do wspomnianej tablicy.
Jak uniknąć sytuacji TOCTOU? W przypadku powyższego przykładu programu C pierwszą rzeczą w jaką należy zainwestować to zamienić operowanie na nazwie pliku na jego deskryptor lub uchwyt. Mając do czynienia z deskryptorami lub wskaźnikami plików mamy pewność, że obiekty, na których działamy nie zmienią się za naszymi plecami, gdy zaczynamy na nich wykonywać pierwsze operacje. Ponadto powinniśmy odpuścić sobie własne sprawdzanie plików (access()) – zostawmy to warstwie systemu plików. Lepiej zaimplementować dobrą obsługę wyjątków zamiast takiego sprawdzania – zgodnie z filozofią EAFP – “Łatwiej jest prosić o przebaczenie niż o pozwolenie” (ang. It is easier to ask for forgiveness then permission). Dla programów binarnych z bitem setuid lepszym rozwiązaniem jest też użycie seteuid() do zmiany efektywnego użytkownika, a następnie wykonanie wywołania open().
Pomimo swojej prostoty tego typu sytuacje wyścigu są trudne do uniknięcia i wyeliminowania. W kontekście systemu plików podstawowym wyzwaniem jest zapewnienie, że nie powinien być on zmieniony pomiędzy dwoma wywołaniami systemowymi. Jednym z rozwiązań jest wprowadzenie transakcji, które mogą zapewnić abstrakcję kontroli współbieżności dla systemu operacyjnego. W systemie Ubuntu (od wersji 10.10) podjęto próbę wykluczenia ataku bazującego na tworzeniu linków (twardych i symbolicznych) poprzez restrykcję ich zachowania w katalogach globalnego zapisu z ustawionym bitem sticky. Jeśli właściciel linku i obiektu docelowego różnią się – nie można podążać za takim linkiem.
Więcej informacji: A Tour of TOCTTOUs, Building Secure Software: Race Conditions, TOCTTOU Vulnerabilities in UNIX-Style File Systems: An Anatomical Study, Portably Solving File TOCTTOU Races with Hardness Amplification, Time of check to time of use