다음을 통해 공유


연습: AddressSanitizer 계속 오류 발생을 사용하여 메모리 안전 문제 찾기

이 연습에서는 메모리 안전 오류를 찾아 보고하는 확인된 빌드를 만듭니다.

범위를 벗어난 메모리 읽기 및 쓰기, 해제된 후 메모리 사용, NULL 포인터 역참조 등과 같은 메모리 안전 오류는 C/C++ 코드의 주요 관심사입니다. ASAN(AddressSanitizer)은 이러한 종류의 찾기 어려운 버그를 노출하고 가양성이 없는 컴파일러 및 런타임 기술입니다. ASAN에 대한 개요는 AddressSanitizer를 참조 하세요.

COE(Continue On Error)는 앱이 실행되면 메모리 안전 오류를 자동으로 진단하고 보고하는 새로운 ASAN 기능입니다. 프로그램이 종료되면 고유한 메모리 안전 오류 요약이 선택한 로그 파일에 출력stdoutstderr됩니다. 표준 C++ 확인된 빌드-fsanitizer=address를 만들 때 할당자에 대한 호출, , 등과 같은 freememcpymemset할당 취소자는 ASAN 런타임으로 전달됩니다. ASAN 런타임은 이러한 함수에 대해 동일한 의미 체계를 제공하지만 메모리에서 발생하는 동작을 모니터링합니다. ASAN은 앱이 실행됨에 따라 가양성 0으로 숨겨진 메모리 안전 오류를 진단하고 보고합니다.

COE의 중요한 장점은 이전 ASAN 동작과 달리 첫 번째 메모리 오류가 발견되면 프로그램 실행이 중지되지 않는다는 것입니다. 대신 ASAN은 오류를 기록하며 앱은 계속 실행됩니다. 앱이 종료되면 모든 메모리 문제에 대한 요약이 출력됩니다.

ASAN이 켜져 있는 C 또는 C++ 앱의 확인된 빌드를 만든 다음 테스트 도구에서 앱을 실행하는 것이 좋습니다. 테스트에서 버그를 찾는 앱의 코드 경로를 연습할 때 테스트를 방해하지 않고 해당 코드 경로에 메모리 안전 문제가 있는지도 확인할 수 있습니다.

앱이 완료되면 메모리 문제에 대한 요약이 표시됩니다. COE를 사용하면 기존 애플리케이션을 컴파일하고 제한된 프로덕션으로 배포하여 메모리 안전 문제를 찾을 수 있습니다. ASAN 계측으로 인해 앱이 느리게 실행되더라도 며칠 동안 확인된 빌드를 실행하여 코드를 완전히 연습할 수 있습니다.

이 기능을 사용하여 새 배송 게이트를 만들 수 있습니다. 모든 기존 테스트가 통과하지만 COE에서 메모리 안전 오류 또는 누수 발생을 보고하는 경우 새 코드를 제공하거나 부모 분기에 통합하지 마세요.

프로덕션 환경에 COE를 사용하도록 설정된 빌드를 배포하지 마세요. COE는 테스트 및 개발 환경에서만 사용됩니다. 메모리 오류를 감지하기 위해 추가된 계측의 성능 영향, 오류가 보고될 경우 내부 구현을 노출할 위험, ASAN이 메모리 할당, 해제를 대체하는 라이브러리 함수를 전달하여 가능한 보안 악용의 노출 영역을 증가시키는 것을 방지하기 위해 프로덕션 환경에서 ASAN 사용 빌드를 사용하면 안 됩니다. 등등.

다음 예제에서는 확인된 빌드를 만들고 ASAN이 보고하는 메모리 안전 오류를 확인하기 위해 주소 소독기 정보를 출력하는 stdout 환경 변수를 설정합니다.

필수 조건

이 연습을 완료하려면 C++ 워크로드가 설치된 데스크톱 개발과 함께 Visual Studio 2022 17.6 이상이 필요합니다.

이중 무료 예제

이 예제에서는 ASAN을 사용하도록 설정된 빌드를 만들어 메모리가 두 번 해제될 때 발생하는 작업을 테스트합니다. ASAN이 이 오류를 감지하고 보고합니다. 이 예제에서는 오류가 검색된 후에도 프로그램이 계속 실행되므로 해제된 메모리를 사용하는 두 번째 오류가 발생합니다. 오류 요약은 프로그램이 종료되는 시점으로 stdout 출력됩니다.

예제를 만듭니다.

  1. 개발자 명령 프롬프트 열기: 시작 메뉴를 열고, 개발자를 입력하고, 일치 항목 목록에서 VS 2022용 개발자 명령 프롬프트와 같은 최신 명령 프롬프트를 선택합니다.

  2. 컴퓨터에 디렉터리를 만들어 이 예제를 실행합니다. 예들 들어 %USERPROFILE%\Desktop\COE입니다.

  3. 해당 디렉터리에서 빈 소스 파일을 만듭니다. 예를 들어 doublefree.cpp

  4. 다음 코드를 파일에 붙여넣습니다.

    #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;
    }
    

앞의 코드 pointer 에서는 두 번 해제됩니다. 이는 모순된 예제이지만 이중 해제는 더 복잡한 C++ 코드에서 쉽게 수행할 수 있는 실수입니다.

다음 단계를 사용하여 COE가 켜져 있는 이전 코드의 빌드를 만듭니다.

  1. 앞에서 연 개발자 명령 프롬프트에서 코드를 컴파일합니다 cl -fsanitize=address -Zi doublefree.cpp. 스위치는 -fsanitize=address ASAN을 켜고 -Zi AddressSanitizer가 메모리 오류 위치 정보를 표시하는 데 사용하는 별도의 PDB 파일을 만듭니다.
  2. 다음과 같이 개발자 명령 프롬프트에서 환경 변수를 stdout 설정하여 ASAN 출력 ASAN_OPTIONS 을 보냅니다.set ASAN_OPTIONS=continue_on_error=1
  3. 다음을 사용하여 테스트 코드를 실행합니다. doublefree.exe

출력은 이중 사용 가능한 오류와 발생한 호출 스택이 있음을 보여줍니다. 보고서는 다음에서 발생한 오류를 보여 주는 호출 스택으로 시작됩니다.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

다음으로, 해제된 메모리 및 메모리가 할당된 위치에 대한 호출 스택에 대한 정보가 있습니다.

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

그런 다음 힙 사용 후 오류에 대한 정보가 있습니다. 이는 참조하는 메모리 *pointerprintf() 이전에 해제되었기 때문에 호출에서 사용을 pointer 의미합니다. 이 메모리가 할당되고 해제된 호출 스택과 마찬가지로 오류가 발생하는 호출 스택이 나열됩니다.

==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

다음으로 버퍼 오버플로 부근의 그림자 바이트에 대한 정보가 있습니다. 섀도 바이트에 대한 자세한 내용은 AddressSanitizer 섀도 바이트를 참조 하세요.

섀도 바이트 정보에 따라 ASAN에서 오류를 감지한 후에도 계속 실행되었음을 나타내는 프로그램의 출력이 표시됩니다.

******* Pointer value: xxx

그런 다음 메모리 오류가 발생한 원본 파일에 대한 요약이 있습니다. 해당 파일의 메모리 오류에 대한 고유 호출 스택을 기준으로 정렬됩니다. 고유한 호출 스택은 오류 유형과 오류가 발생한 호출 스택에 따라 결정됩니다.

이 정렬은 가장 중요한 메모리 안전 문제의 우선 순위를 지정합니다. 예를 들어 동일한 파일에서 서로 다른 메모리 안전 오류로 이어지는 5개의 고유한 호출 스택은 여러 번 발생하는 한 번의 오류보다 더 걱정스러울 수 있습니다. 요약은 다음과 같습니다.

=== 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

마지막으로 보고서에는 메모리 오류가 발생한 위치에 대한 요약이 포함되어 있습니다.

=== 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

범위를 벗어난 메모리 액세스 예제

이 예제에서는 ASAN을 사용하도록 설정된 빌드를 만들어 앱이 범위를 벗어난 메모리에 액세스할 때 발생하는 작업을 테스트합니다. ASAN은 이 오류를 감지하고 프로그램 종료 시 오류 요약을 stdout 보고합니다.

예제를 만듭니다.

  1. 개발자 명령 프롬프트를 엽니다. 시작 메뉴를 열고, 개발자를 입력하고, 일치 항목 목록에서 VS 2022용 개발자 명령 프롬프트와 같은 최신 명령 프롬프트를 선택합니다.

  2. 컴퓨터에 디렉터리를 만들어 이 예제를 실행합니다. 예들 들어 %USERPROFILE%\Desktop\COE입니다.

  3. 해당 디렉터리에서 원본 파일(예 coe.cpp: )을 만들고 다음 코드를 붙여넣습니다.

    #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); 
    }
    

이전 코드에서 매개 변수 sz 는 10이고 원래 버퍼는 10바이트입니다. 두 가지 메모리 안전 오류가 있습니다.

  • 루프에서 buffor 범위를 벗어난 로드
  • 루프에 대한 범위를 벗어난 저장소 localfor

버퍼 오버플로는 루프 종료 테스트 <=sz때문입니다. 이 예제를 실행하면 우연의 일치로 안전합니다. 이는 대부분의 C++ 런타임 구현에서 수행되는 초과 할당 및 맞춤 때문입니다. 때 sz % 16 == 0에 대한 최종 쓰기가 메모리를 local[ii] 손상합니다. 다른 경우는 CRT(C 런타임)가 할당을 0 모드 16 경계로 패딩하는 방식으로 인해 할당된 추가 메모리인 "malloc slop"에 대해서만 읽기/쓰기가 가능합니다.

할당 다음 페이지가 매핑되지 않은 경우 또는 손상된 데이터를 사용하는 경우에만 오류가 관찰됩니다. 다른 모든 경우는 이 예제에서 자동으로 수행됩니다. 오류가 계속되면 프로그램이 완료될 때 실행된 후 요약에 오류가 표시됩니다.

COE가 켜져 있는 이전 코드의 빌드를 만듭니다.

  1. 를 사용하여 코드를 cl -fsanitize=address -Zi coe.cpp컴파일합니다. 스위치는 -fsanitize=address ASAN을 켜고 -Zi AddressSanitizer가 메모리 오류 위치 정보를 표시하는 데 사용하는 별도의 PDB 파일을 만듭니다.
  2. 다음과 같이 개발자 명령 프롬프트에서 환경 변수를 stdout 설정하여 ASAN 출력 ASAN_OPTIONS 을 보냅니다.set ASAN_OPTIONS=continue_on_error=1
  3. 다음을 사용하여 테스트 코드를 실행합니다. coe.exe

출력은 두 개의 메모리 버퍼 오버플로 오류가 있었으며 발생한 위치에 대한 호출 스택을 제공합니다. 보고서는 다음과 같이 시작됩니다.

==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

다음으로 버퍼 오버플로 부근의 그림자 바이트에 대한 정보가 있습니다. 섀도 바이트에 대한 자세한 내용은 AddressSanitizer 섀도 바이트를 참조 하세요.

섀도 바이트 보고서에 따라 메모리 오류가 발생한 원본 파일에 대한 요약이 있습니다. 해당 파일의 메모리 오류에 대한 고유 호출 스택을 기준으로 정렬됩니다. 고유한 호출 스택은 오류 유형과 오류가 발생한 호출 스택에 따라 결정됩니다.

이 정렬은 가장 중요한 메모리 안전 문제의 우선 순위를 지정합니다. 예를 들어 동일한 파일에서 서로 다른 메모리 안전 오류로 이어지는 5개의 고유한 호출 스택은 여러 번 발생하는 한 번의 오류보다 더 걱정스러울 수 있습니다.

요약은 다음과 같습니다.

=== Files in priority order ===

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

마지막으로 보고서에는 메모리 오류가 발생한 위치에 대한 요약이 포함되어 있습니다. 오류 발생 시 계속은 동일한 소스 줄에서 발생하는 두 가지 고유한 오류를 보고합니다. 첫 번째 오류는 섹션의 전역 주소에서 메모리를 .data 읽고 다른 오류는 힙에서 할당된 메모리에 씁니다.

보고서는 다음과 같습니다.

=== 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 

기본 AddressSanitizer 런타임 동작은 찾은 첫 번째 오류를 보고한 후 앱을 종료합니다. "잘못된" 컴퓨터 명령을 실행할 수 없습니다. 새 AddressSanitizer 런타임은 오류를 진단하고 보고하지만 후속 지침을 실행합니다.

COE는 각 메모리 안전 오류를 보고한 후 자동으로 다시 애플리케이션으로 제어를 반환하려고 시도합니다. AV(메모리 액세스 위반) 또는 실패한 메모리 할당이 있는 경우와 같이 할 수 없는 상황이 있습니다. COE는 프로그램의 구조적 예외 처리가 catch되지 않는 액세스 위반 후에도 계속되지 않습니다. COE가 앱 CONTINUE CANCELLED - Deadly Signal. Shutting down. 에 실행을 반환할 수 없는 경우 메시지가 출력됩니다.

ASAN 출력을 보낼 위치 선택

환경 변수를 ASAN_OPTIONS 사용하여 다음과 같이 ASAN 출력을 보낼 위치를 결정합니다.

  • stdout에 출력: set ASAN_OPTIONS=continue_on_error=1
  • stderr에 출력: set ASAN_OPTIONS=continue_on_error=2
  • 선택한 로그 파일에 출력: set COE_LOG_FILE=yourfile.log

정의되지 않은 동작 처리

ASAN 런타임은 C 및 C++ 할당/할당 취소 함수의 정의되지 않은 동작을 모두 모방하지 않습니다. 다음 예제에서는 ASAN 버전의 _alloca 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");
    }
}

많은 수에서 main() 궁극적으로 전달되는 많은 수 foo_redundant_alloca()전달되며, 이로 인해 _alloca() 실패합니다.

이 예제 pass 에서는 ASAN 없이 컴파일될 때(즉, 스위치 없음 -fsanitize=address ) 출력하지만 ASAN이 켜져 있는 상태에서 컴파일될 때 출력 fail 됩니다(즉, 스위치 사용 -fsanitize=address ). ASAN이 없으면 예외 코드가 일치 RET_STACK_EXCEPTION 하므로 cnt 1로 설정됩니다. throw된 예외가 AddressSanitizer 오류인 dynamic-stack-buffer-overflow이므로 ASAN을 사용하여 컴파일할 때 다르게 동작합니다. 즉, 코드가 반환 RET_OTHER_EXCEPTION 되는 대신 RET_STACK_EXCEPTIONcnt 1로 설정되지 않습니다.

기타 장점

새 ASAN 런타임을 사용하면 앱과 함께 추가 이진 파일을 배포할 필요가 없습니다. 이렇게 하면 추가 이진 파일을 관리할 필요가 없으므로 일반적인 테스트 하네스에서 ASAN을 더 쉽게 사용할 수 있습니다.

참고 항목

오류 블로그 게시물에서 AddressSanitizer 계속
메모리 안전 오류 예제
-Zi 컴파일러 플래그
-fsanitize=address 컴파일러 플래그
가장 위험한 소프트웨어 약점 25개