Terapia zamiast łat

Luki w oprogramowaniu można niwelować na poziomie systemu operacyjnego. Najlepsze efekty przynosi jednak kontrola kodu przed lub w trakcie kompilacji.

Luki w oprogramowaniu można niwelować na poziomie systemu operacyjnego. Najlepsze efekty przynosi jednak kontrola kodu przed lub w trakcie kompilacji.

Zagrożenia związane z atakami sieciowymi czy wirusami zostały w większości firm zauważone i w dużej mierze zażegnane. Tymczasem na "niewidzialne", choć z pewnością istniejące, luki w oprogramowaniu dobrego rozwiązania na razie nie ma. Być może dlatego że problem leży w dużej mierze po stronie wytwórców oprogramowania, a nie administratorów - przynajmniej tam, gdzie ci ostatni nie mają dostępu do kodu źródłowego.

Na czym tak naprawdę polega problem z lukami w oprogramowaniu i co można zrobić, by mu zapobiec?

Luka przez duże C

Większość języków programowania nie była projektowana z myślą o bezpieczeństwie kodu. Jeden z najpopularniejszych języków programowania - język C jest tak naprawdę estetyczniejszą i bardziej czytelną implementacją języka maszynowego (assemblera). Widać to w każdym jego detalu - działaniach na wskaźnikach, łańcuchach, operatorach itd. Ma to oczywiste zalety. Programy pisane w C szybko pracują i są relatywnie mało "pamięciożerne" ze względu na prostą konstrukcję kodu wynikowego. Są też zwykle bardzo wydajne i w związku z tym właśnie, choć coraz więcej programistów korzysta z języków nowszej generacji jak Java czy C#, w pewnych zastosowaniach język C nadal jest niezastąpiony.

Programista piszący w języku C musi pamiętać o organizacji pamięci podczas działania programu, starannej obsłudze błędów i wyjątków. Każda pomyłka lub niedopatrzenie może spowodować lukę powodującą w najlepszym przypadku niestabilne działanie programu, a w najgorszym - naruszenie bezpieczeństwa przetwarzanych przez niego danych.

Są jednak języki programowania, których mankamenty języka C w ogóle nie dotyczą. Przykładem jest Ada - język o składni zbliżonej do Pascala z bardzo ścisłą kontrolą typów już na poziomie kompilatora. Tworząc program za pomocą języka Ada, wielu błędów po prostu nie da się popełnić, ze względu na to że już projektując struktury danych, programista będzie zmuszony zrobić to w sposób bezpieczny, np. z góry określając maksymalną długość bufora znakowego. Z kolei Perl praktycznie nie absorbuje programisty operacjami na buforach pamięci, zajmując się automatycznie ich przydzielaniem i zmianą ich rozmiaru w zależności od potrzeb.

Niestety, ani Windows, ani Linux, ani większości pozostałych systemów nie napisano ani w Adzie, ani w Perlu, lecz w C i C++. W językach tych powstaje także większość aplikacji dla Windows i Linux.

Jak powstaje dziura

Najczęściej popełnianym błędem jest nadpisanie bufora (buffer overrun), czyli sytuacja, kiedy program próbuje skopiować do bufora dane o większej objętości, niż zarezerwowany dla nich obszar pamięci. Wykorzystanie przepełnienia bufora polega na zapisaniu do niego danych dłuższych od przewidzianego przez programistę rozmiaru tak, by dane te nadpisały stos (kolejkę danych przewidzianych do przetworzenia), w tym także adresy powrotów z funkcji (adresy pamięci przewidziane dla wyników przetwarzania). Atakujący może np. nadpisać adres powrotu z bieżącej funkcji tak, aby wskazywał na jego kod (np. kod wirusa lub konia trojańskiego), znajdujący się w wysłanym przez niego bloku danych.

Program operujący bezpośrednio na pamięci wykorzystuje wiele zmiennych będących liczbami całkowitymi, określającymi wielkość bufora, ilość wczytanych danych itd. Zmienne te są deklarowane jako liczby całkowite (integer) ze znakiem (signed) lub bez (unsigned). Każdy z tych typów ma wyrażone w bitach długości minimalne i maksymalne.

W jednym i drugim przypadku możliwe jest, że w wyniku niewystarczającej kontroli ze strony programisty zmienna, wbrew jego intencjom, zmieni typ - z oznaczonego na nieoznaczony lub odwrotnie. Większość kompilatorów C i C++ zezwala na mieszanie typów ze znakiem i bez znaku - funkcja zdeklarowana jako przyjmująca argument bez znaku przyjmie także liczbę ze znakiem. Wywoła to, co prawda, ostrzeżenie kompilatora, ale mimo to taki kod zostanie skompilowany. Co gorsza, dla wygody programisty kompilator zaproponuje w takim przypadku automatyczną konwersję typu. Wtedy liczba ujemna -10000 typu short int (krótka liczba całkowita) zmieni się w 55536, liczbę ponad pięć razy większą i to dodatnią, co może mieć katastrofalne skutki.

Na próchnicę - rekompilacja

Rozwiązania zmniejszające skutki błędów w programach były konieczne, ponieważ duża część działającego oprogramowania wykorzystuje stary kod, którego nie da się lub nie opłaca zmieniać. Rekompilacja kodu pisanego bez dbania o bezpieczeństwo przynosi z reguły tylko częściową poprawę, jednak nawet wtedy ma ona sens. Wykorzystanie jednego z wymienionych w tym artykule zabezpieczeń uchroniło wiele serwerów, mimo że działało na nich np. niezabezpieczone oprogramowanie BIND (DNS) lub Sendmail (serwer pocztowy).

Stosowanie wielu zabezpieczeń ma sens, nawet jeśli każde z nich nie jest stuprocentowo skuteczne. W wielu przypadkach znalezienie błędu umożliwiającego przepełnienie bufora to dopiero pierwszy krok do właściwego ataku, polegającego na wykorzystaniu programu-ofiary do uruchomienia złośliwego kodu. Samo załadowanie tego kodu może się okazać dla atakującego niemożliwe, np. dzięki wprowadzonemu przez zabezpieczenia kompilatora limitowi długości pobieranych danych.

W celu komercyjnej reprodukcji treści Computerworld należy zakupić licencję. Skontaktuj się z naszym partnerem, YGS Group, pod adresem [email protected]

TOP 200