Sign in Join
Wiki copytemporary

Secure programming/pl

English · Français · Polski

Bezpieczne programowanie

Przedmowa

Ta strona wiki jest próbą nauczenia innego podejścia do tworzenia oprogramowania. Strona wykorzystuje bardzo proste przykłady, aby pokazać, że wiele problemów można wykorzystać w celu wykonania ataku bezpieczeństwa na komputer, program lub cały system.

Należy pamiętać, że dokument jest tylko początkiem nauki pisania lepszego i nieco bezpieczniejszego kodu, ale nie stanowi kompletnego przewodnika, jak to zrobić. W rzeczywistości jest to tylko krótki przegląd tego, jak musimy widzieć nasz kod i program oraz jak uniknąć wielu typowych problemów, które tam występują.

Proszę pamiętać, że ten dokument dotyczy edukacji w celu lepszego kodowania, a nie przewodnika po hakowaniu i łamaniu programów.

Możesz być także zainteresowany zasadami The Power of 10, aby uzyskać bardziej zwięzłą poradę.

Nie ufaj danym wejściowym

Podczas tworzenia programu jest prawdopodobne, że będzie on w jakiś sposób wchodził w interakcję z użytkownikiem, nawet jeśli oznacza to jedynie odczytywanie plików w systemie i prezentację danych.

Zwykle w szkołach i na uniwersytetach, gdy zaczyna się pisać programy, osoba ta uczy się, jak otrzymywać dane wejściowe, podczas gdy nauczyciele zwykle mówią tej osobie „załóż, że dane, które otrzymujesz, są prawidłowe”. Wtedy zaczynają się problemy:

Note: Nie możemy ufać żadnym danym wejściowym, których nie możemy kontrolować, ponieważ ich zawartość jest nieznana i może wykorzystywać lukę w naszym oprogramowaniu.

Czytanie z pliku to odczytywanie niezaufanych danych wejściowych, podobnie jak odczytywanie danych wejściowych użytkownika lub akceptowanie danych wejściowych z sieci.

Dlaczego nie mogę ufać danym wejściowym?

Aby zrozumieć, dlaczego dane wejściowe są niebezpieczne, najpierw musimy zrozumieć, czym są dane wejściowe.

Dane wejściowe mogą pochodzić z naciśnięcia klawisza, ruchu myszy lub kliknięcia przycisku myszy, lub z odczytywania i akceptowania informacji z wielu innych sposobów, takich jak strumień danych lub nawet funkcje systemowe. W rzeczywistości wszystko, co Twój program otrzymuje z zewnątrz, to są dane wejściowe.

Nie ma znaczenia, jaki jest rodzaj danych wejściowych: na przykład użytkownik lub inny system może podać nam błędne dane, a przyczyny mogą być celowe lub pomyłka. Nie możesz kontrolować tego wejścia, a głównym powodem jest to, że nie możesz odgadnąć, jakie będą to dane wejściowe.

Wynikiem mogą być puste (NULL) „dane”, które podaje nam użytkownik, liczba wykraczająca poza nasz oczekiwany zakres lub większa ilość znaków niż oczekiwaliśmy, a nawet próba zmiany adresu zmiennej, która akceptuje dane wejściowe od użytkownika. Po prostu nie wiemy, jakie dane wprowadzi użytkownik.

Każde „niebezpieczne” postępowanie z danymi wejściowymi może spowodować pobranie krytycznych informacji, których użytkownik nie może zobaczyć, lub modyfikację danych, do której użytkownik nie jest upoważniony, uszkodzenie danych, a nawet uszkodzenie (zawieszenie) samego programu.

Jakich problemów możemy się spodziewać?

Przy każdym typie błędu prawdopodobnie znajdziesz inny sposób ataku, ale chciałbym podać krótką listę bardzo powszechnych rodzajów ataków, zamiast opisywać wiele ich typów.

Najczęstsze rodzaje ataków to:

Przepełnienie bufora

Gdy otrzymane dane przepełnią ilość pamięci, która została dla nich przydzielona:

pascal
var
  iNums : array [0..9] of integer;
  ...
  FillChar (iNums[-1], 100, #0);
  ...
  for i := -10 to 10 do
  readln (iNums[i]);
  ...

Na tym przykładzie widać, że dla statycznej tablicy iNums daliśmy możliwość przyjęcia tylko 10 liczb, podczas gdy potem próbujemy wprowadzić 21 liczb.

Należy pamiętać, że chociaż kompilator może ostrzegać w prostych przypadkach, to nie zawsze będzie on reagował w bardziej rozbudowanych formach.

Jeśli użytkownik może wprowadzić dane, które są wysyłane do bufora, może on podać pewne wartości, które można zinterpretować jako instrukcje kodu maszynowego, a te zostaną zapisane poza naszym buforem. Komputer mógłby wtedy wykonać ten kod zamiast kodu, który powinien tam być. To jest własnie przepełnienie bufora.

Atak DoS

Denial of Service to nie tylko problem sieciowy, ale może istnieć również na wiele innych sposobów:

pascal
procedure Recurse;
begin
  while (True) do
    begin
      Recurse;
    end;
end;

Ta procedura będzie działać, dopóki system nie wyczerpie zasobów, ponieważ przy każdej rekursji przydziela więcej pamięci stosu i spowoduje, że system przestanie odpowiadać, a nawet ulegnie awarii. Chociaż niektóre systemy – takie jak Linux – będą próbowały dać ci możliwość zaprzestania uruchamiania programu, zajmie ci to dużo czasu.

Należy pamiętać, że jest to tylko statyczny przykład, ale przeprowadziliśmy atak DoS na system uruchamiając ten kod.

Innym znanym atakiem DoS jest brak zwolnienia zasobów systemowych, takich jak pamięć, gniazda, deskryptory plików itp.

Na przykład:

pascal
...
  begin
    while True do
      begin
        Getmem(OurPtr, 10);
        OurPtr := Something;
      end;
  end.

Ten przykład pokazuje alokację pamięci (Getmem jest jak malloc w C: rezerwuje pamięć do jakiegoś użycia), ale kończymy wykonywanie kodu bez zwalniania pamięci pod koniec jej używania.

Wstrzyknięcia

Kiedy użytkownik podaje nam dane wejściowe, a my pracujemy na danym wejściu bezpośrednio bez oczyszczania go, użytkownik może umieścić coś w niektórych tagach SQL lub kodzie (takim jak kod skryptu lub kod maszynowy), co sprawi, że nasz program będzie wykonywał pewne dodatkowe działania (np. usunięcie niektórych rekordów/tabeli, wysłanie użytkownikowi pewnych zastrzeżonych danych, takich jak struktura bazy danych/tabeli, użytkownik bazy danych i hasło, zawartość katalogu lub pliku, a nawet uruchomienie programu na komputerze).

Przykład wstrzyknięcia SQL:

Dane wejściowe użytkownika: Podaj swoje imię i nazwisko: a' OR 1=1

Wewnątrz kodu:

pascal
...
Write('Proszę, wpisać swoje imię: ');
ReadLn(sName);
Query1.SQL.Add('SELECT Password FROM tblUsers WHERE Name='#32 + sName + #32);
...

Przesłanie tej instrukcji SQL jako danych wejściowych użytkownika spowoduje, że nasze zapytanie doda „ OR 1=1” do instrukcji SQL przekazanej do bazy danych: w tym przypadku zawsze skutkuje to prawdą, a użytkownik uzyskał nieautoryzowany dostęp do programu.

Dostęp do Twoich danych i modyfikacja

Nie tylko Twój program będzie miał dostęp do danych, z których korzysta. Jeśli przechowujesz dane w plikach lub (zdalnych) bazach danych, atakujący może uzyskać do nich dostęp za pośrednictwem systemu operacyjnego (i/lub bazy danych i/lub warstwy sieci/protokołu bazy danych).

Szyfrowanie: czy to wystarczy?

Aby przeciwdziałać opisanemu powyżej wątkowi, programiści często stosują szyfrowanie. Szyfrowanie może służyć do zapewnienia:

zarówno dla

Jeśli korzystasz z tych metod, Twoje dane nie są jednak automatycznie bezpieczne.

Istnieje wiele możliwych ataków na zaszyfrowane dane:

Jeśli nie wiesz dokładnie, co robisz (i nie będziesz wiedzieć, chyba że masz przeszkolenie kryptograficzne), użyj (w kolejności malejącej preferencji):

Szyfrowanie jest częścią zestawu możliwych środków bezpieczeństwa; wysiłek/pieniądze na to wydane powinny zostać ocenione w ramach analizy bezpieczeństwa (patrz poniżej).

Mity i założenia

Wiele problemów związanych z bezpieczeństwem wynika z ignorowania ważnych ostrzeżeń i informacji przekazanych przez kompilator oraz z myślenia, że ​​program nie zawiera żadnego możliwego do wykorzystania problemu.

Oto kilka przykładów tego typu problemów:

mity

założenia

Analizuj, aby zrozumieć zagrożenia i bezpieczeństwo

Generalnie programista (lub jego pracodawcy/właściciele firmy) powinien przeprowadzić analizę wszystkich zagrożeń (od fizycznego dostępu/ataku po ataki logiczne i socjotechniczne) dla całego systemu (w tym infrastruktury – OS, bazy danych, sieci a także fizycznej maszyny/okablowanie/budynki/połączenia zewnętrzne), aby przeanalizować, czy nie pozostawiasz otwartej luki w zabezpieczeniach, która jest niedopuszczalna (z perspektywy ryzyka/korzyści).

Zakres analizy powinien zależeć od wartości danych/procesów chronionych przez system. Oczywiście nie ma sensu szaleć, próbując przeanalizować fizyczne bezpieczeństwo swojego domu podczas opracowywania programu hobbystycznego, aby śledzić wyniki brydżowe.

Korzyści z tego rodzaju analiz:

Konkretne rozwiązania

Skoro znamy już pewne problemy, które możemy napotkać podczas tworzenia programów, powinniśmy dowiedzieć się, jak je naprawić. Wszystkie problemy, które widzieliśmy powyżej, przejawiają się w dwóch typach: założenia i brak starannego programowania. A żeby nauczyć się je naprawiać, najpierw musimy nauczyć się myśleć w inny sposób niż dotychczas.

Przepełnienie

Aby naprawić przepełnienie danych, takie jak bufory i inne rodzaje danych wejściowych, musimy przede wszystkim zidentyfikować rodzaj danych, z którymi musimy pracować.

Przepełnienie bufora

Jeśli wrócimy do naszego przykładu:

pascal
var
  iNums: array [0..9] of Integer;
  ...
  FillChar(iNums[-1], 100, #0);
  ...
  for i := -10 to 10 do
    ReadLn(iNums[i]);
  ...

Widzimy tutaj zakres, który został przepełniony naszymi wartościami, nawet nie sprawdzając, czy numer indeksu jest poprawny.

W tablicach dynamicznych/otwartych w Pascalu możemy poznać limity przydzielonej pamięci. Więc wszystko, co musimy zrobić, to sprawdzić, czy rozmiar nie jest za mały lub za duży dla naszego bufora i ograniczyć akceptację do rozmiaru, jaki sobie życzymy.

Zatem przykład należy zmienić na:

pascal
var
  iNums: array [0..9] of Integer;
  
  ...
  FillChar (iNums[Low(iNums)], High(iNums), #0);
  ...
  
  for i := Low(iNums) to High(iNums) do
    ReadLn(iNums[i]);
  ...

Ale czekaj, coś jest jeszcze nie tak!

Readln przyjmie nieograniczoną ilość znaków i nikt nam nie obiecuje, że będzie to liczba całkowita lub nawet z zakresu, który możemy obsłużyć.

Przepełnienie liczby

Podczas gdy łańcuch w Pascalu jest czystą tablicą (hrmm hrmm.. niezupełnie, przynajmniej nie w FPC, ale załóżmy przez chwilę, że jest, OK?), więc readln spróbuje znaleźć i zobaczyć, jakie są jego limity i czy nie spróbujemy przekroczyć zakresu, który nadaliśmy temu typowi, ale liczby nie są takie same.

Liczby mają ograniczenia, komputer/kompilator ma wiele ograniczeń dotyczących pamięci i liczb. Może dać tylko „małą” ilość pamięci na liczby (liczby zmiennoprzecinkowe i całkowite). I w większości przypadków nie potrzebujemy uzywać dużego zakresu liczb (jak zmienna logiczna, która zwykle potrzebuje tylko dwóch liczb).

W powyższym przykładzie możemy mieć przepełnienie bufora, które spowoduje błąd sprawdzania zakresu, który da nam złą liczbę (problemy z przypomnieniem Carry Flag (flaga przeniesienia)... nie będę ich tutaj wyjaśniać), a także mamy efekt DoS, ponieważ nasz program zatrzyma się w tym momencie.

Więc co możemy zrobić, aby to naprawić?

Przede wszystkim możemy chcieć pracować ze zmienną łańcuchową, która będzie odpowiadała największej długości liczby +1 (dla znaku minus) lub możemy stworzyć własną procedurę/funkcję readln, która będzie specjalizować się w typie integer.

Dla pierwszej opcji możemy wykonać następujące czynności (skopiowane z dokumentacji FPC):

pascal
Program Example74;

{ Program do demonstracji funkcji Val. }
Var
  I, Code: Integer;
begin
  Val(ParamStr(1), I, Code);
  If Code <> 0 then
    Writeln('Błąd na pozycji ', code, ' : ', Paramstr(1)[Code])
  else
    Writeln('Wartość : ', I);
end.

Tutaj widzimy, jak przekonwertować łańcuch na liczbę całkowitą z bardzo łatwą obsługą błędów. Funkcja StrToInt także może załatwić sprawę, ale wtedy musimy przechwycić wyjątek w przypadku obsługi błędów.

Oto mały przykład małej procedury typu readln dla liczb całkowitych.

pascal
program MyReadln;
uses
  CRT;

procedure MyIntReadLn(var Param: Integer; ParamLength: Integer);
var
  Line: string;
  ch: char;
  Error: Integer;
begin
  Line  := '';
  
  repeat
    ch := ReadKey;
    if (Length (Line) <> ParamLength) then
    begin
      if (ch in ['0'..'9']) then
      begin
        Line := Line + ch;
        write (ch);
      end
      else
      if (ch = '-') and (Length(Line) = 0) then
      begin
        Line := '-';
        write (ch);
      end;
    end;
    
    if (ch = #8) and (Length(Line) <> 0) then // backspace
    begin
      Line := Copy(Line, 1, Length(Line) - 1);
      gotoxy(WhereX - 1, WhereY);
      write(' ');
      gotoxy(WhereX - 1, WhereY);
    end;
  until (ch = #13);
  
  val(Line, Param, Error);
  
  if (Error <> 0) then
    Param := 0;
  
  writeln;
end;

var
  Num : Integer;
begin
  Write('Number: ');
  MyIntReadLn(Num, 2);
  WriteLn('The number is: ', Num);
end.

Pamiętaj, że możesz uczynić go jeszcze lepszym i bardziej wydajnym, jeśli chcesz. To tylko bardzo mały przykład pokazujący, jak to zrobić.

Jakie są zagrożenia bezpieczeństwa w Przepełnieniach?

Przepełnienie pamięci może pozwolić na wykonanie dowolnego kodu procesora, a użytkownicy mogą uruchamiać dowolny typ kodu i nic nie może ich powstrzymać.

Blokada usług DoS

Blokada usług (DoS) to jeden z najtrudniejszych rodzajów ataków, którym należy zapobiec. Powody to:

Jak widać, blokada usług może być prawie wszystkim, co może uniemożliwić systemowi działanie tak, jak powinien, z powodu wykorzystania błędów lub błędnego kodu lub po prostu wadliwego programu, który przechwytuje zasoby systemowe.

W powyższym przykładzie DoS:

pascal
procedure Recurse;
begin
  while (True) do
    begin
      Recurse;
    end;
end;

Stworzyłem również przepełnienie stosu (inny rodzaj przepełnienia bufora), który spowodował, że komputer potrzebował więcej zasobów pamięci, aby kontynuować wykonywanie kodu.

Każdy zasób systemowy, który jest dostępny dla programu, może zostać nadużyty, nie zwracając go z powrotem do systemu, gdy program „już go nie potrzebuje”. Przetrzymywanie zasobów systemowych, takich jak pamięć lub gniazda, blokuje innym programom możliwość wykonywania niektórych z ich działań. W ten sposób większość programów zatrzyma wykonywanie i zgłosi błąd, a niektóre będą się zawieszać i nadal szukać zasobów systemowych.

Należy pamiętać, że niektóre nadużycia zasobów systemowych wynikają z błędu w programowaniu, jak oczekiwanie na bufor 150k, podczas gdy rzeczywisty bufor ma tylko 2 bajty, a gdy program nadal szuka bufora 150k, nowe żądanie tworzy kolejny bufor 150k itd., dopóki system nie będzie już w stanie odpowiedzieć na żadne z żądań (jest to znany typ ataku).

Dobrym obejściem tego błędu jest ograniczenie liczby niepełnych buforów, które można przydzielić jednocześnie. Jeśli bufor nie jest pełny po upływie limitu czasu, powinien być wolny. Jednak to rozwiązanie spowoduje również odmowę usługi, ponieważ komunikacja i tak zostanie zatrzymana w pewnym momencie lub powolne połączenie może spowodować utratę danych.

Wstrzykiwanie

Istnieje wiele sposobów na wstrzyknięcie pewnego rodzaju kodu do naszych programów. Jak widzieliśmy na powyższym przykładzie:

Dane wejściowe użytkownika: Podaj swoje imię i nazwisko: a' LUB 1=1

Wewnątrz kodu:

pascal
...
write('Proszę wpisać swoje imię: ');
readln(sName);
Query1.SQL.Add('SELECT Password FROM tblUsers WHERE Name='#32 + sName + #32);
...

Wstrzyknięcie nastąpiło, ponieważ nie filtrujemy (nie odkażamy) naszego kodu: oznacza to sprawdzenie, czy otrzymujemy dokładnie taki rodzaj wejścia, jakiego szukamy, i nic więcej.

Na przykład możemy sprawdzić, czy sName zawiera spacje. Jeśli tak, nie kontynuuj sprawdzania reszty zmiennej. To pomaga, jeśli nazwa użytkownika może składać się tylko z jednego słowa składającego się z liter, może znaku apostrofu (') i może nawet podkreślenia (_) i na tym koniec. Jeśli wprowadzimy liczbę, powinno to być nielegalne (chyba że chcemy używać „języka hackerskiego” (fonetycznie) lub zezwolić na używanie cyfr.

Istnieje wiele sposobów oczyszczenia danych. Mniej skuteczny (ale często używany) to:

Nieskuteczne odkażanie

pascal
function ValidVar (const S: AnsiString; AllowChars: TCharset): Boolean;
var
  i: Word;
begin
  i := 0;
  Result := True;
  
  while (Result) and (i <= Length(S)) do
  begin
    Inc(i);
    Result := S[i] in AllowChars;
  end;
end;

Funkcja zwraca wartość true, jeśli mamy poprawną strukturę treści podaną przez parametr AllowChars w zmiennej S. Należy pamiętać, że ta funkcja jest tylko próbką koncepcji i może wymagać więcej pracy, aby mogła zostać w pełni wykorzystana.

Innym sposobem na zrobienie tego samego jest użycie wyrażeń regularnych w następujący sposób (jest to próbka koncepcji wzięta z języka Perl. FPC nie ma w pełni obsługiwanego silnika wyrażeń regularnych, który umożliwia modyfikację ciągów):

text
$sName =~ s/[^a-z0-9_\']//gi;

Wyrażenie regularne usuwa wszelkie niepoprawne znaki z łańcucha i zwraca do nas usunięty łańcuch. Zwróć uwagę, że o ile wiem, to wyrażenie regularne będzie działać również w silnikach ereg, ale z minimalnymi korektami (flaga g instruuje Perla, aby zastąpił wszystkie znalezione pasujące wzorce, a i oznacza case insensitivity (niewrażliwość na wielkość liter)).

Teraz, gdy wiemy, że nasze dane wejściowe są prawidłowe, musimy zobaczyć, do czego służy zawartość zmiennej. Jeśli zawartość zmiennej trafia do bazy danych, skryptu cgi lub czegokolwiek innego, co ma własną składnię, musimy zmienić treść zgodnie z niedozwolonymi lub kontrolnymi znakami odpowiedniego języka (np. SQL dla baz danych).

Istnieje wiele sposobów na uniknięcie tego typu zawartości. Załóżmy na razie, że ta treść trafia do zapytania bazy danych. Teraz przede wszystkim musimy upewnić się, że nasz dodatkowy znak ucieczki, nie zwiększy rozmiaru naszych danych, powyżej limitu długości pól w naszej bazie danych. Ponieważ, jeśli tak się stanie, możemy doprowadzić do wstrzykiwania, do utraty danych, blokady usług lub przepełnienia bufora (poważna baza danych zwykle obcina dane, ale czasami nie we właściwym miejscu).

Zwykle jedyną ucieczkę, jaką musimy zrobić, aby użyć ciągu w bazie danych, jest ucieczka jedynie znaku apostrofu (') (chociaż niektóre bazy danych mogą mieć problemy z większą liczbą znaków niż apostrof). Tak więc wszystko, co powinniśmy zrobić, to reprezentować apostrofy w sposób, który nie wpłynie na silnik bazy danych, jak znak odwrotnego ukośnika (\') lub podwajać każdy apostrof do dwóch apostrofów (''), a może nawet użyć innego znaku, który zastąpi apostrofy w zapytaniu i zamienić je ponownie, gdy musimy pokazać je użytkownikowi.

Ograniczanie wprowadzania przez parametry zapytania SQL

Obowiązkowa ilustracja wizualna.

Po upewnieniu się, że przestrzegamy ograniczeń, możemy kontynuować nasze próby. Aby uciec od kodu, możemy użyć kilku podejść. Mniej przyjazny dla debugowania sposób, ale pewny sposób na poprawną ucieczkę, to użycie techniki parametrów:

pascal
Query1.SQL.Add('SELECT Password FROM tblUsers WHERE Name=?');
Query1.Parameters.Add(sName);
if (Query1.Execute) then
...

Ta technika pozwala silnikowi bazy danych uciec przed parametrem w taki sposób, abyśmy mogli korzystać z zawartości bez żadnych problemów związanych z niedozwolonymi znakami. Ponadto niektóre bazy danych mają zwiększoną wydajność w przypadku powtarzających się wywołań tego kodu, ponieważ mogą one przygotować do tego instrukcję wewnętrzną z parametrami. Wadą jest to, że nigdy nie możemy debugować wyniku zapytania. Oznacza to, że nie możemy zobaczyć, w jaki sposób zawartość sName osadzona jest w instrukcji SQL i nigdy nie możemy z tego powodu sprawdzić, czy nasze zapytanie było poprawne.

Jednak po przetestowaniu zapytania bez parametrów dodawanie parametrów jest dość łatwe, więc w praktyce ten problem nie jest tak duży, jak się wydaje.

Wydajny kod

Wspomniane powyżej środki bezpieczeństwa komplikują kod, jeśli użyłeś najbardziej wydajnego kodu, aby uzyskać potrzebną funkcjonalność. Na szczęście czasami możesz przekazać komplikacje kodu do frameworka/biblioteki, która się tym zajmuje. Na przykład: w przykładzie SQL DoS możesz pozwolić silnikowi bazy danych zająć się ucieknięciem danych za pomocą sparametryzowanych zapytań, przy niewielkim dodatkowym koszcie konieczności użycia parametrów w kodzie.

Jednak pisanie wydajnego, ale podatnego na zagrożenia kodu nie pomaga nikomu poza prawnikami od odpowiedzialności cywilnej i ekspertami kryminalistyki.

Poza tym dokumentem

O ile w tym dokumencie podałem kilka krótkich (tak, wiem, że to niedopowiedzenie 😉) przykładów i informacji o tym, jak stworzyć lepszy kod, o tyle jest wiele kwestii, których nie poruszyłem w tym dokumencie. Część z nich to uprawnienia użytkownika do uruchamiania programów, rootkity systemu i inne problemy, które nasz kod musi wziąć pod uwagę (zmienna środowiskowa to tylko jeden przykład).

Przeczytaj więcej artykułów dotyczących problemów z bezpieczeństwem, takich jak

Przepełnienia bufora:

Blokada usług (DoS):

Wstrzykiwanie SQL:


From the Free Pascal wiki, page "Secure programming/pl", as it stood on 2022-10-02 after a change by Slawek, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's Licensing page. If you have concerns about this material, message the admin.

Revision 3, last changed by WikiArchive .

Talk about this page

Nobody has opened a bench for this page yet. Sign in to open one.