Trudne przypadki: buffer overflow
• tech • 612 słów • 3 minuty czytania
Buffer overflow, czyli przepełnienie bufora jest jednym z najczęściej wykrywanych błędów w różnej maści oprogramowaniu. Błąd ten polega na zapisaniu do wyznaczonego obszaru pamięci większej ilości danych, niż zostało to przewidziane przez twórcę. Powoduje to nadpisanie sąsiadujących z buforem komórek pamięci, co w ostateczności prowadzi do różnych nieprzewidywanych zachowań aplikacji.
W najlepszym wypadku spowoduje to “wywalenie” się całego programu, a w najgorszym umożliwi przejecie kontroli nad maszyną przez włamywacza, który odpowiednio wykorzysta zaistniałą sytuację.
Nie będziemy się tu zbytnio rozwodzić nad typowymi błędami przepełnienia, które w dosyć prosty sposób można znaleźć i wyeliminować. W sieci znajduje się wiele ciekawych materiałów dotyczących tego tematu. Polecić mogę artykuł “Po prostu Buffer Overflow” autorstwa h07, który może rzucić nieco więcej światła tym osobom, którzy nie za bardzo orientują się w temacie takich błędów i sposobów ich wykorzystania.
Tutaj chciałbym się podzielić przypadkiem dosyć specyficznym, który może sprawić trochę więcej problemów w czasie działania i próbach znalezienia jego wystąpienia w kodzie, czyli tytułowe trudne przypadki, trudne do wykrycia.
Z jednym z tych specyficznych przypadków miałem styczność w czasie tworzenia mojego ostatniego patcha do wxWidgets, dodającego wsparcie dla themes w owner-drawn menu. Tam właśnie miałem przyjemność pomęczyć się z tym paskudnym błędem, którego tutaj chce szerzej przedstawić.
Zaczynając historię od początku, istnieje fragment działającego do tej pory kodu, który pobiera jakaś sobie wartość:
bool value;
GetValue(&value);
Funkcja GetValue
jest jakąś tam sobie funkcją WinAPI o następującym prototypie:
BOOL GetValue(BOOL* value);
Po małych zmianach, powyższy kod został przeniesiony do wnętrza pewnej struktury:
struct A {
// ...
bool value;
};
I nadal wszystko działało poprawnie.
Niestety, ale tylko do czasu, aż zmieniono nieco zawartość struktury A
. Dodano kolejne pole typu boolowskiego do struktury - value2
, zaraz za naszym wspomnianym value
. I dopiero wtedy zaczęły się dziać dziwne rzeczy. Obie zmienne bool
zaczęły zawierać różne wartości, niekoniecznie takie jakie były spodziewane. Szczególnie odczyt wartości przez funkcję GetValue
modyfikował nie tylko zmienną value
, ale także value2
.
Wpływ na takie zachowanie ma zjawisko structure alignment, czyli wyrównywanie pól struktury do granicy słowa (dwusłowa). Więcej informacji o tym zjawisku można znaleźć choćby na wikipedii lub msdnie. Przy standardowych ustawieniach kompilatora, adresy obu zmiennych boolowskich będą wskazywały na sąsiednie bajty pamięci. Oczywiście to stwierdzenie nie było dla mnie żadną nowością i nadal nic mi nie mówiło o tym dlaczego to nie działa.
Dopiero po upewnieniu się że wyrównanie działa tak jak powinno i kompilator nic nie “miesza”, wpadła mi do głowy myśl, aby sprawdzić co tak naprawdę robi funkcja GetValue
. Po poznaniu jej nagłówka wszystko stało się jasne. Funkcja przyjmuje jako argument wskaźnik do zmiennej typu BOOL
, który w rzeczywistości w WinAPI jest liczbą całkowitą:
typedef int BOOL;
#define FALSE 0
#define TRUE 1
A zatem podając adres do naszej zmiennej value
, modyfikacji ulegają 32-bity (x86) począwszy od komórki, na którą wskazuje podany adres (0x0018FF38
). Przypisanie dowolnej wartości do zmiennej value
przez funkcję GetValue
w rzeczywistości wyzeruje wszystkie bity także w value2
, co interpretowane będzie jako wartość false
. Czyli mamy klasyczny przypadek buffer overflow, tyle że trochę trudny do wykrycia na pierwszy rzut oka.
Trochę czasu zajęło mi dojście do tego rozwiązania, po drodze sprawdzając wiele razy kod i przeprowadzając różne testy.
Problem był o tyle trudny do znalezienia, że kod ten działał prawidłowo do tej pory i był używany przez bibliotekę. Dlatego nawet nie przeszła mi przez głowę myśl, aby przy przenoszeniu tego fragmentu kodu sprawdzić, czy zastanowić się nad prawidłowością wywołania danej funkcji.
Nasuwa się również pytanie, skoro kod ten działał do tej pory, mimo iż zawierał błąd przepełnienia bufora, co tak naprawdę po drodze zostawało nadpisywane i czym to skutkowało przy używaniu biblioteki? Mogło to mieć wpływ na zupełnie inną cześć biblioteki albo programu, a nikt nie był tego świadomy.
Komentarze (2)
Błąd ciekawy z gatunku wrednych.
A czy kompilator nie rzucił warniga przy rzutowaniu
bool*
naBOOL*
?Poziom warningów był chyba zbyt niski w projekcie ;)
No i kod był używany do tej pory, więc zaufałem i zbytnio nie zwróciłem uwagi na jego bebechy przy przenoszeniu.