Udostępnij za pośrednictwem


Przewodnik: Używanie funkcji odczyszczania adresów w przypadku błędu w celu znalezienia problemów z bezpieczeństwem pamięci

W tym przewodniku utwórz sprawdzone kompilacje, które wyszukują i zgłaszają błędy bezpieczeństwa pamięci.

Błędy bezpieczeństwa pamięci, takie jak operacje odczytu i zapisu poza granicami pamięci, przy użyciu pamięci po jego uwolnieniu, NULL wyłuszczenia wskaźnika itd., są głównym problemem dla kodu C/C++. Rozwiązanie Address Sanitizer (ASAN) to technologia kompilatora i środowiska uruchomieniowego, która uwidacznia tego rodzaju trudne do znalezienia usterek i wykonuje je z zerowymi wynikami fałszywie dodatnimi. Aby zapoznać się z omówieniem usługi ASAN, zobacz AddressSanitizer.

Kontynuuj przy błędzie (COE) to nowa funkcja ASAN, która automatycznie diagnozuje i zgłasza błędy bezpieczeństwa pamięci podczas uruchamiania aplikacji. Po zakończeniu działania programu podsumowanie unikatowych błędów bezpieczeństwa pamięci to dane wyjściowe do stdout, stderrlub wybranego pliku dziennika. Po utworzeniu standardowej kompilacji sprawdzonej w -fsanitizer=addressjęzyku C++ z wywołaniami alokatorów, alokatorami, takimi jak free, memcpy, memseti tak dalej, są przekazywane do środowiska uruchomieniowego usługi ASAN. Środowisko uruchomieniowe usługi ASAN zapewnia te same semantyki dla tych funkcji, ale monitoruje, co dzieje się z pamięcią. Usługa ASAN diagnozuje i zgłasza ukryte błędy bezpieczeństwa pamięci z zerowymi wynikami fałszywie dodatnimi podczas uruchamiania aplikacji.

Ważną zaletą środowiska COE jest to, że w przeciwieństwie do poprzedniego zachowania usługi ASAN program nie przestaje działać po znalezieniu pierwszego błędu pamięci. Zamiast tego usługa ASAN zauważa błąd, a aplikacja nadal działa. Po zakończeniu działania aplikacji zostanie wyświetlone podsumowanie wszystkich problemów z pamięcią.

Dobrym rozwiązaniem jest utworzenie sprawdzonej kompilacji aplikacji C lub C++ z włączoną usługą ASAN, a następnie uruchomienie aplikacji w uprzęży testowej. Podczas wykonywania testów ścieżki kodu w aplikacji w poszukiwaniu usterek dowiesz się również, czy te ścieżki kodu zawierają problemy z bezpieczeństwem pamięci bez zakłócania testów.

Po zakończeniu pracy aplikacji zostanie wyświetlone podsumowanie problemów z pamięcią. Dzięki funkcji COE można skompilować i wdrożyć istniejącą aplikację w ograniczonej produkcji, aby znaleźć problemy z bezpieczeństwem pamięci. Możesz uruchomić sprawdzoną kompilację przez kilka dni, aby w pełni ćwiczyć kod, chociaż aplikacja będzie działać wolniej z powodu instrumentacji ASAN.

Za pomocą tej funkcji można utworzyć nową bramę wysyłkową. Jeśli wszystkie istniejące testy kończą się pomyślnie, ale coE zgłasza błąd bezpieczeństwa pamięci lub wyciek, nie wysyłaj nowego kodu ani nie integruj go z gałęzią nadrzędną.

Nie wdrażaj kompilacji z włączoną funkcją COE w środowisku produkcyjnym! Środowisko COE ma być używane tylko w środowiskach testowych i projektowych. Nie należy używać kompilacji z włączoną usługą ASAN w środowisku produkcyjnym z powodu wpływu na wydajność instrumentacji dodanej do wykrywania błędów pamięci, ryzyka ujawnienia wewnętrznej implementacji w przypadku zgłaszania błędów i uniknięcia zwiększenia obszaru powierzchni możliwych luk w zabezpieczeniach przez wysyłanie funkcji biblioteki, które asAN zastępuje alokację pamięci, zwalniając, i tak dalej.

W poniższych przykładach utworzysz sprawdzone kompilacje i ustawisz zmienną środowiskową, aby wyświetlić informacje o usuwaniu adresów w celu stdout wyświetlenia błędów bezpieczeństwa pamięci zgłaszanych przez usługę ASAN.

Wymagania wstępne

Aby ukończyć ten przewodnik, potrzebny jest program Visual Studio 2022 w wersji 17.6 lub nowszej z zainstalowanym pakietem roboczym Programowanie aplikacji klasycznych w języku C++.

Podwójny bezpłatny przykład

W tym przykładzie utworzysz kompilację z włączoną usługą ASAN, aby przetestować, co się stanie, gdy pamięć zostanie zwolniona dwukrotnie. Usługa ASAN wykrywa ten błąd i zgłasza go. W tym przykładzie program nadal działa po wykryciu błędu, co prowadzi do drugiego błędu — przy użyciu pamięci, która została zwolniona. Podsumowanie błędów to dane wyjściowe po stdout zakończeniu działania programu.

Utwórz przykład:

  1. Otwórz wiersz polecenia dla deweloperów: otwórz menu Start , wpisz Developer i wybierz najnowszy wiersz polecenia, taki jak Wiersz polecenia dla dewelopera dla programu VS 2022 z listy dopasowań.

  2. Utwórz katalog na maszynie, aby uruchomić ten przykład. Na przykład %USERPROFILE%\Desktop\COE.

  3. W tym katalogu utwórz pusty plik źródłowy. Na przykład doublefree.cpp

  4. Wklej poniższy kod do pliku:

    #include <stdio.h>
    #include <stdlib.h>
    
    void BadFunction(int *pointer)
    {
        free(pointer);
        free(pointer); // double-free!
    }
    
    int main(int argc, const char *argv[])
    {
        int *pointer = static_cast<int *>(malloc(4));
        BadFunction(pointer);
    
        // Normally we'd crash before this, but with COE we can see heap-use-after-free error as well
        printf("\n\n******* Pointer value: %d\n", *pointer);
    
        return 1;
    }
    

W poprzednim kodzie pointer jest dwukrotnie zwalniany. Jest to spisany przykład, ale podwójne zwolnienia są łatwym błędem, który można popełnić w bardziej złożonym kodzie C++.

Utwórz kompilację poprzedniego kodu z włączonym programem COE, wykonując następujące kroki:

  1. Skompiluj kod w wierszu polecenia dewelopera, który został otwarty wcześniej: cl -fsanitize=address -Zi doublefree.cpp. Przełącznik -fsanitize=address włącza usługę ASAN i -Zi tworzy oddzielny plik PDB, który adresowy sanitizer używa do wyświetlania informacji o lokalizacji błędu pamięci.
  2. Wyślij dane wyjściowe usługi ASAN do, stdout ustawiając zmienną ASAN_OPTIONS środowiskową w wierszu polecenia dewelopera w następujący sposób: set ASAN_OPTIONS=continue_on_error=1
  3. Uruchom kod testowy za pomocą polecenia: doublefree.exe

Dane wyjściowe pokazują, że wystąpił podwójny wolny błąd i stos wywołań, w którym wystąpił. Raport rozpoczyna się od stosu wywołań, który pokazuje, że wystąpił błąd w pliku BadFunction:

==22976==ERROR: AddressSanitizer: attempting double-free on 0x01e03550 in thread T0:
    #0  free                           D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp(69)
    #1  BadFunction                    C:\Users\xxx\Desktop\COE\doublefree.cpp(8)
    #2  main                           C:\Users\xxx\Desktop\COE\doublefree.cpp(14)
    #3  __scrt_common_main_seh         D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
    #4  BaseThreadInitThunk            Windows
    #5  RtlInitializeExceptionChain    Windows

Następnie znajdują się informacje o uwolnionej pamięci i stosie wywołań dla miejsca, w którym przydzielono pamięć:

0x01e03550 is located 0 bytes inside of 4-byte region [0x01e03550,0x01e03554)
freed by thread T0 here:
    #0  free                           D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp(69)
    #1  BadFunction                    C:\Users\xxx\Desktop\COE\doublefree.cpp(7)
    #2  main                           C:\Users\xxx\Desktop\COE\doublefree.cpp(14)
    #3  __scrt_common_main_seh         D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
    #4  BaseThreadInitThunk            Windows
    #5  RtlInitializeExceptionChain    Windows

previously allocated by thread T0 here:
    #0  malloc                         D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp(85)
    #1  main                           C:\Users\xxx\Desktop\COE\doublefree.cpp(13)
    #2  __scrt_common_main_seh         D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
    #3  BaseThreadInitThunk            Windows
    #4  RtlInitializeExceptionChain    Windows

Następnie znajdują się informacje o błędzie sterta-use-after-free. Odnosi się to do użycia *pointer w wywołaniu printf() , ponieważ pamięć pointer odwołuje się do została zwolniona wcześniej. Stos wywołań, w którym występuje błąd, podobnie jak stosy wywołań, w których ta pamięć została przydzielona i zwolniona:

==35680==ERROR: AddressSanitizer: heap-use-after-free on address 0x02a03550 at pc 0x00e91097 bp 0x012ffc64 sp 0x012ffc58READ of size 4 at 0x02a03550 thread T0
         #0  main                           C:\Users\xxx\Desktop\Projects\ASAN\doublefree.cpp(18)
         #1  __scrt_common_main_seh         D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
         #2  BaseThreadInitThunk            Windows
         #3  RtlInitializeExceptionChain    Windows

0x02a03550 is located 0 bytes inside of 4-byte region [0x02a03550,0x02a03554)
freed by thread T0 here:
         #0  free                           D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp(69)
         #1  BadFunction                    C:\Users\xxx\Desktop\Projects\ASAN\doublefree.cpp(7)
         #2  main                           C:\Users\xxx\Desktop\Projects\ASAN\doublefree.cpp(14)
         #3  __scrt_common_main_seh         D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
         #4  BaseThreadInitThunk            Windows
         #5  RtlInitializeExceptionChain    Windows

previously allocated by thread T0 here:
         #0  malloc                         D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp(85)
         #1  main                           C:\Users\xxx\Desktop\Projects\ASAN\doublefree.cpp(13)
         #2  __scrt_common_main_seh         D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
         #3  BaseThreadInitThunk            Windows
         #4  RtlInitializeExceptionChain    Windows

Następnie znajdują się informacje o bajtach cienia w pobliżu przepełnienia buforu. Aby uzyskać więcej informacji na temat bajtów w tle, zobacz AddressSanitizer shadow bytes (Bajty w tle addressSanitizer).

Po informacjach o bajtach w tle zobaczysz dane wyjściowe z programu, co oznacza, że nadal działa po wykryciu błędu przez usługę ASAN:

******* Pointer value: xxx

Następnie znajduje się podsumowanie plików źródłowych, w których wystąpił błąd pamięci. Jest ona sortowana według unikatowych stosów wywołań dla błędów pamięci w tym pliku. Unikatowy stos wywołań jest określany przez typ błędu i stos wywołań, w którym wystąpił błąd.

To sortowanie określa priorytety problemów z bezpieczeństwem pamięci, które mogą być najbardziej niepokojące. Na przykład pięć unikatowych stosów wywołań prowadzących do różnych błędów bezpieczeństwa pamięci w tym samym pliku jest potencjalnie bardziej niepokojące niż jeden błąd, który występuje wiele razy. Podsumowanie wygląda następująco:

=== Files in priority order ===

File: D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp Unique call stacks: 1
File: C:\Users\xxx\Desktop\COE\doublefree.cpp Unique call stacks: 1

Na koniec raport zawiera podsumowanie, gdzie wystąpiły błędy pamięci:

=== Source Code Details: Unique errors caught at instruction offset from source line number, in functions, in the same file. ===

File: D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp
        Func: free()
                Line: 69 Unique call stacks (paths) leading to error at line 69 : 1
                        Bug: double-free at instr 19 bytes from start of line
File: C:\Users\xxx\Desktop\COE\doublefree.cpp
        Func: main()
                Line: 18 Unique call stacks (paths) leading to error at line 18 : 1
                        Bug: heap-use-after-free at instr 55 bytes from start of line

>>>Total: 2 Unique Memory Safety Issues (based on call stacks not source position) <<<

#0 C:\Users\xxx\Desktop\COE\doublefree.cpp Function: main(Line:18)
        Raw HitCnt: 1  On Reference: 4-byte-read-heap-use-after-free
#1 D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp Function: free(Line:69)
        Raw HitCnt: 1

Przykład dostępu do pamięci poza granicami

W tym przykładzie utworzysz kompilację z włączoną usługą ASAN, aby przetestować, co się stanie, gdy aplikacja uzyskuje dostęp do pamięci, która jest niedostępna. Usługa ASAN wykrywa ten błąd i zgłasza podsumowanie błędów do stdout momentu zakończenia programu.

Utwórz przykład:

  1. Otwórz wiersz polecenia dla deweloperów: otwórz menu Start , wpisz Developer i wybierz najnowszy wiersz polecenia, taki jak Wiersz polecenia dla dewelopera dla programu VS 2022 z listy dopasowań.

  2. Utwórz katalog na maszynie, aby uruchomić ten przykład. Na przykład %USERPROFILE%\Desktop\COE.

  3. W tym katalogu utwórz plik źródłowy, na przykład coe.cpp, i wklej następujący kod:

    #include <stdlib.h> 
    
    char* func(char* buf, size_t sz)
    { 
        char* local = (char*)malloc(sz); 
        for (auto ii = 0; ii <= sz; ii++) // bad loop exit test 
        {
            local[ii] = ~buf[ii]; // Two memory safety errors 
        }
    
        return local; 
    } 
    
    char buffer[10] = {0,1,2,3,4,5,6,7,8,9}; 
    
    int main()
    {   
        char* inverted_buf= func(buffer, 10); 
    }
    

W poprzednim kodzie parametr sz ma wartość 10, a oryginalny bufor to 10 bajtów. Istnieją dwa błędy bezpieczeństwa pamięci:

  • obciążenie poza granicami z buffor pętli
  • magazyn poza granicami localfor w pętli

Przepełnienie buforu jest spowodowane testem <=szzakończenia pętli . Po uruchomieniu tego przykładu jest on bezpieczny zbieg okoliczności. Wynika to z nadmiernej alokacji i wyrównania wykonywanego przez większość implementacji środowiska uruchomieniowego języka C++. Gdy sz % 16 == 0funkcja zapisu local[ii] końcowego uszkodzi pamięć. Inne przypadki tylko odczyt/zapis do "malloc slop", który jest dodatkową pamięci przydzieloną ze względu na sposób, w jaki środowisko uruchomieniowe C (CRT) pads alokacji do granicy 0 mod 16.

Błędy można zaobserwować tylko wtedy, gdy strona po alokacji jest niezmapowana lub w przypadku użycia uszkodzonych danych. Wszystkie inne przypadki są dyskretne w tym przykładzie. Po zakończeniu działania programu błędy są widoczne w podsumowaniu po uruchomieniu programu.

Utwórz kompilację poprzedniego kodu z włączonym programem COE:

  1. Skompiluj kod za pomocą polecenia cl -fsanitize=address -Zi coe.cpp. Przełącznik -fsanitize=address włącza usługę ASAN i -Zi tworzy oddzielny plik PDB, który adresowy sanitizer używa do wyświetlania informacji o lokalizacji błędu pamięci.
  2. Wyślij dane wyjściowe usługi ASAN do, stdout ustawiając zmienną ASAN_OPTIONS środowiskową w wierszu polecenia dewelopera w następujący sposób: set ASAN_OPTIONS=continue_on_error=1
  3. Uruchom kod testowy za pomocą polecenia: coe.exe

Dane wyjściowe pokazują, że wystąpiły dwa błędy przepełnienia buforu pamięci i udostępnia stos wywołań dla miejsca ich wystąpienia. Raport zaczyna się w następujący sposób:

==9776==ERROR: AddressSanitizer: global-buffer-overflow on address 0x0047b08a at pc 0x003c121b bp 0x012ffaec sp 0x012ffae0
READ of size 1 at 0x0047b08a thread T0
	 #0  func                           C:\Users\xxx\Desktop\COE\coe.cpp(8)
	 #1  main                           C:\Users\xxx\Desktop\COE\coe.cpp(18)
	 #2  __scrt_common_main_seh         D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
	 #3  BaseThreadInitThunk            Windows
	 #4  RtlInitializeExceptionChain    Windows

Następnie znajdują się informacje o bajtach cienia w pobliżu przepełnienia buforu. Aby uzyskać więcej informacji na temat bajtów w tle, zobacz AddressSanitizer shadow bytes (Bajty w tle addressSanitizer).

Po raporcie bajtów w tle znajduje się podsumowanie plików źródłowych, w których wystąpiły błędy pamięci. Jest ona sortowana według unikatowych stosów wywołań dla błędów pamięci w tym pliku. Unikatowy stos wywołań jest określany przez typ błędu i stos wywołań, w którym wystąpił błąd.

To sortowanie określa priorytety problemów z bezpieczeństwem pamięci, które mogą być najbardziej niepokojące. Na przykład pięć unikatowych stosów wywołań prowadzących do różnych błędów bezpieczeństwa pamięci w tym samym pliku jest potencjalnie bardziej niepokojące niż jeden błąd, który występuje wiele razy.

Podsumowanie wygląda następująco:

=== Files in priority order ===

File: C:\Users\xxx\Desktop\COE\coe.cpp Unique call stacks: 2

Na koniec raport zawiera podsumowanie miejsc, w których wystąpiły błędy pamięci. Kontynuuj w obszarze Błąd zgłasza dwa odrębne błędy występujące w tym samym wierszu źródłowym. Pierwszy błąd odczytuje pamięć na adresie globalnym w .data sekcji, a pozostałe zapisy do pamięci przydzielonej ze sterta.

Raport wygląda następująco:

=== Source Code Details: Unique errors caught at instruction offset from source line number, in functions, in the same file. === 

File: C:\Users\xxx\Desktop\COE\coe.cpp 
	Func: func()
		Line: 8 Unique call stacks (paths) leading to error at line 8 : 2
			Bug: heap-buffer-overflow at instr 124 bytes from start of line

>>>Total: 2 Unique Memory Safety Issues (based on call stacks not source position) <<<

#0 C:\Users\xxx\Desktop\COE\coe.cpp Function: func(Line:8) 
	Raw HitCnt: 1  On Reference: 1-byte-read-global-buffer-overflow 
#1 C:\Users\xxx\Desktop\COE\coe.cpp Function: func(Line:8) 
	Raw HitCnt: 1  On Reference: 1-byte-write-heap-buffer-overflow 

Domyślne zachowanie środowiska uruchomieniowego Narzędzia sanitizer adresów kończy działanie aplikacji po zgłoszeniu pierwszego znalezionego błędu. Nie zezwala na wykonanie instrukcji "złej" maszyny. Nowe środowisko uruchomieniowe rozwiązania Address Sanitizer diagnozuje i zgłasza błędy, ale następnie wykonuje kolejne instrukcje.

CoE próbuje automatycznie zwrócić kontrolę z powrotem do aplikacji po zgłoszeniu każdego błędu bezpieczeństwa pamięci. Występują sytuacje, w których nie można, na przykład w przypadku naruszenia dostępu do pamięci (AV) lub alokacji pamięci, która zakończyła się niepowodzeniem. CoE nie jest kontynuowane po naruszeniach dostępu, które nie przechwytują obsługi wyjątków ustrukturyzowanych programu. Jeśli coE nie może zwrócić wykonywania do aplikacji, CONTINUE CANCELLED - Deadly Signal. Shutting down. zostanie wyświetlony komunikat.

Wybieranie miejsca wysyłania danych wyjściowych usługi ASAN

Użyj zmiennej środowiskowej ASAN_OPTIONS , aby określić, gdzie wysyłać dane wyjściowe usługi ASAN w następujący sposób:

  • Dane wyjściowe do stdout: set ASAN_OPTIONS=continue_on_error=1
  • Dane wyjściowe do narzędzia stderr: set ASAN_OPTIONS=continue_on_error=2
  • Dane wyjściowe do wybranego pliku dziennika: set COE_LOG_FILE=yourfile.log

Obsługa niezdefiniowanego zachowania

Środowisko uruchomieniowe USŁUGI ASAN nie naśladuje wszystkich niezdefiniowanych zachowań funkcji alokacji/cofania C++. W poniższym przykładzie pokazano, jak wersja usługi ASAN _alloca różni się od wersji środowiska uruchomieniowego języka C:

#include <cstdio>
#include <cstring>
#include <malloc.h>
#include <excpt.h>
#include <windows.h>

#define RET_FINISH 0
#define RET_STACK_EXCEPTION 1
#define RET_OTHER_EXCEPTION 2

int foo_redundant(unsigned long arg_var)
{
    char *a;
    int ret = -1;

    __try
    {
        if ((arg_var+3) > arg_var)
        {
            // Call to _alloca using parameter from main
            a = (char *) _alloca(arg_var);
            memset(a, 0, 10);
        }
        ret = RET_FINISH;
    }
    __except(1)
    {
        ret = RET_OTHER_EXCEPTION;
        int i = GetExceptionCode();
        if (i == EXCEPTION_STACK_OVERFLOW)
        {
            ret = RET_STACK_EXCEPTION;
        }
    }
    return ret;
}

int main()
{
    int cnt = 0;

    if (foo_redundant(0xfffffff0) == RET_STACK_EXCEPTION)
    {
        cnt++;
    }

    if (cnt == 1)
    {
        printf("pass\n");
    }
    else
    {
        printf("fail\n");
    }
}

W main() dużej liczbie jest przekazywana do foo_redundantmetody , która ostatecznie jest przekazywana do _alloca()elementu , co powoduje _alloca() niepowodzenie.

W tym przykładzie dane wyjściowe, gdy są kompilowane bez usługi ASAN (czyli bez przełącznika), ale dane wyjściowe pass-fsanitize=address podczas kompilacji z włączoną usługą ASAN (czyli z przełącznikiemfail).-fsanitize=address Dzieje się tak dlatego, że bez usługi ASAN kod wyjątku jest zgodny RET_STACK_EXCEPTIONcnt z ustawieniem 1. Zachowuje się inaczej podczas kompilowania za pomocą usługi ASAN, ponieważ zgłoszony wyjątek jest błędem Sanitizer adresu zamiast tego: dynamic-stack-buffer-overflow. Oznacza to, że kod zwraca RET_OTHER_EXCEPTION wartość zamiast RET_STACK_EXCEPTION , więc cnt nie jest ustawiona na 1.

Inne korzyści

W przypadku nowego środowiska uruchomieniowego usługi ASAN nie trzeba wdrażać dodatkowych plików binarnych w aplikacji. Ułatwia to nawet korzystanie z usługi ASAN z normalną uprzężą testowej, ponieważ nie trzeba zarządzać dodatkowymi plikami binarnymi.

Zobacz też

AdresSanitizer Kontynuuj we wpisie w blogu o błędzie
Przykładowe błędy bezpieczeństwa pamięci
-Flaga kompilatora Zi
-fsanitize=flaga kompilatora adresu
25 najbardziej niebezpiecznych słabości oprogramowania