Coldwind Gynvael, Juszczyk Mateusz - Programistyczne potkniecia.pdf

(675 KB) Pobierz
1
Spis treści
Wstęp............................................................................3
Arytmetyka i obliczenia.................................................4
Przekroczenie zakresu liczb całkowitych...........................4
Obcięcie zakresu liczb całkowitych...................................8
Głębokie ukrycie.........................................................12
Użycie niezainicjalizowanej pamięci...............................13
Kolizje w tablicach haszujących.....................................20
Wyszukiwanie wszystkiego...........................................22
Wyścigi w systemie.....................................................24
Deserializacja niezaufanych danych...............................27
Logika aplikacji...........................................................31
Oscylatory walutowe...................................................31
Dziwactwa..................................................................33
Nierówne równości......................................................33
Wartości (nie)losowe...................................................35
Nazwy lików..............................................................37
p
Wycieki danych...........................................................12
Stabilność i niezawodność...........................................20
Wielowątkowość i wielozadaniowość..........................24
Dane wejściowe..........................................................27
Kryptografia................................................................35
Przenośność kodu.......................................................37
Zakończenie................................................................39
Bibliografia.................................................................40
2
Wstęp
Historia tworzenia i testowania oprogramowania jest ściśle związana
z historią elektronicznych maszyn liczących, której początki sięgają lat ‘30
i ‘40 poprzedniego wieku, kiedy to sławne postaci komputerowego świata
takie jak Alan Turing, John von Neumann czy Tommy Flowers tworzyli pod-
waliny współczesnej informatyki. Od czasu powstania pierwszej programo-
walnej maszyny software stał się nieodłączną częścią każdego komputera,
którego rozwój niedługo później wyewoluował do niezwykle rozległej i skom-
plikowanej dziedziny nauki w swoich podstawach odrębnej od
hardware,
czyli fizycznie istniejącego sprzętu, na którym jest wykonywany. Ze względu
na ludzką omylność oraz fakt, że programowanie jest pod wieloma względa-
mi procesem trudnym, wymagającym koncentracji, wiedzy oraz doświadcze-
nia, błędy (tzw. bugi) wszelkich rodzajów towarzyszyły i towarzyszą po dziś
dzień twórcom oprogramowania, stając się wręcz integralną i akceptowaną
częścią ich pracy.
Błędy znajdujące się w programach komputerowych można dzielić na wie-
le różnych kategorii: błędy w składni lub logice aplikacji, błędy wpływające
na stabilność lub bezpieczeństwo systemu, błędy ujawniające się w trakcie
pisania kodu, po tygodniach, latach, a nawet dekadach! W historii ostatnich
lat znajdziemy błędy które w żaden znaczący sposób nie wpłynęły na losy
świata jak również takie, których skutki były tragiczne, a firmy i państwowe
organizacje obciążyły stratami rzędu milionów, a nawet miliardów dolarów.
Naturalnym wydaje się, że nie sposób całkowicie wyeliminować wszystkie
usterki w programach sterującymi aparatami, komórkami, komputerami do-
mowymi, reaktorami jądrowymi czy sztucznymi satelitami; możemy jednak
minimalizować liczbę popełnianych pomyłek oraz naprawiać błędy, zanim
znajdą się one w kodzie produkcyjnym. W niniejszym artykule przedstawia-
my przykłady kilkunastu najciekawszych, zdaniem autorów, rodzin błędów
i programistycznych potknięć, wraz z wyjaśnieniem ich natury, możliwości
unikania oraz powiązanymi przykładami z życia. Zapraszamy do lektury!
3
Arytmetyka i obliczenia
Przekroczenie zakresu liczb całkowitych
Zaczniemy od bardzo prostego błędu, który spotyka się w wielu językach
programowania - przekroczenia zakresu liczb całkowitych, lub częściej
integer
overflow (dosłownie przepełnienie liczby całkowitej). Przykłady kodu
zawierającego błąd tego typu w różnych językach programowania przedsta-
wione są poniżej:
// C, C++
unsigned short
dlugosc_tekstu
=
OdbierzDlugoscTekstu(socket);
char
*bufor = (char*)malloc(dlugosc_tekstu
+ 1);
OdbierzTekst(bufor, dlugosc_tekstu,
socket);
bufor[dlugosc_tekstu]
=
‘\0’;
// ActionScript
var
szerokosc:int
=
PobierzInt(plik);
var
wysokosc:int
=
PobierzInt(plik);
var
wielkosc:int
=
szerokosc * wysokosc;
W niektórych językach programowania takich jak C, C++, Objective-C,
ActionScript czy Java, zmienne typu całkowitego mają ograniczony roz-
miar (zazwyczaj wyrażany w bitach; np. typ int w ActionScript ma wielkość
32 bitów), a co za tym idzie, ograniczony zakres wartości które można
w nich przechować. W zasadzie dowolna książka traktująca o danym języku
programowania rozpoczyna opis dostępnych typów zmiennych od podania
zakresu wartości, które można w nich przechować; w przypadku wspomnia-
nego typu int w ActionScript będą to wartości od -2
31
do 2
31
-1 (czyli od około
minus dwóch miliardów, do około dwóch miliardów).
Powstaje więc pytanie - co się stanie, jeśli program w toku wykonywania
przekroczy dozwolony zakres zmiennej, w rezultacie wykonanej właśnie
operacji arytmetycznej? Możliwości jest oczywiście kilka i zależą one za-
równo od konkretnej technologii, jej wersji, typu zmiennej, jak i systemu,
czy architektury procesora na którym dany program jest uruchamiany.
W zdecydowanej większości przypadków do czynienia mamy z tzw. arytme-
tyką “modulo” która, rozpatrując kwestię z niskopoziomowej perspektywy,
4
polega na “przycięciu” wyniku operacji do dolnych N-bitów, które mieszczą
się w zakresie danego typu (dla typów liczb naturalnych jest to równoważne
z wykonaniem dzielenia modulo przez maksymalną wartość, którą można
pomieścić w zmiennej powiększoną o jeden).
Rozważmy ten problem na podanym powyżej przykładzie z ActionScript -
jeśli funkcja PobierzInt zwróciłaby w obu przypadkach wartość 70 000, to
wynikiem mnożenia byłoby oczywiście 4 900 000 000 (niecałe pięć miliar-
dów). Binarnie (w systemie dwójkowym) można tę liczbę zapisać jako:
1 0010 0100 0001 0000 0001 0001 0000 0000
Niestety, liczba całkowita tego rzędu wymaga do wyrażenia 33 bitów, a za-
deklarowana w przykładzie zmienna wielkosc ma rozmiar jedynie 32 bitów.
Z tego powodu najbardziej znacząca część wyniku zostanie odrzucona:
1
0010 0100 0001 0000 0001 0001 0000 0000
W efekcie otrzymujemy znacznie mniejszą liczbę - 605 032 704 - która nijak
ma się do oczekiwanego wyniku.
Błędy tego typu występują w oprogramowaniu niezwykle często, jednak je-
dynie w nielicznych przypadkach objawiają się podczas codziennej pracy -
z reguły programy w normalnych warunkach nie mają “okazji” operować na
danych wystarczająco obszernych, by doprowadzić do przepełnienia zmien-
nej typu wybranego przez programistę. Błąd tego typu może zostać jednak
celowo wywołany przez osobę o niecnych zamiarach, która, będąc w sta-
nie spreparować nieprzewidziane przez autora warunki działania programu,
może doprowadzić do wykonania kontrolowanego przez nią kodu w kon-
tekście podatnej aplikacji (czyli do przejęcia nad nią kontroli). W związku
z tym przekroczenie zakresu zmiennej, szczególnie w przypadku języków
C oraz C++ (choć nie tylko), może prowadzić do poważnych problemów
związanych z bezpieczeństwem aplikacji, a w konsekwencji całego systemu
komputerowego.
Za przykład może posłużyć tutaj
exploit
zaprezentowany przez hakera o pseudonimie
PinkiePie
na konferencji PacSec w Tokio w listopadzie 2013. PinkiePie, wykorzystu-
jąc właśnie błąd przepełnienia zmiennej typu całkowitego w przeglądarce Google
Chrome (a następnie kilka kolejnych błędów), był w stanie wykonać dowolny kod na
smarfonach Nexus 4 oraz Samsung Galaxy S4, tym samym zdobywając nagrodę w
wysokości 50,000 USD
[1].
CIEKAWOSTKA
5
Zgłoś jeśli naruszono regulamin