허상 포인터

허상 포인터(Dangling pointer) 그리고 와일드 포인터(wild pointers)는 컴퓨터 프로그래밍에서 적절한 타입의 유효한 객체를 가리키고 있지 않는 포인터를 말한다. 이것들은 메모리 보안 위반의 특별한 경우들이다. 일반적으로 허상 포인터는 인터넷의 죽은 링크 (link rot) 형상들처럼 유효하지 않은 목적지 주소에 대한 참조이다. 허상 포인터는 객체 파괴시에 발생하는데, 즉 객체에 대한 참조가 포인터 값에 대한 수정 없이 삭제되거나 할당 해제돼서 포인터가 계속 할당 해제된 메모리를 가리킬 때이다. 시스템은 할당 해제된 메모리를 다른 프로세스에게 재할당하겠지만, 기존 프로그램이 허상 포인터를 역참조하면 메모리는 현재 전혀 다른 데이터를 갖고 있을 것이므로 예측할 수 없는 행동이 발생한다. 특히 프로그램이 허상 포인터가 가리키는 메모리에 쓰기를 하면, 관련되지 않은 데이터의 조용한 오염이 발생하게 되고, 이것은 찾기가 매우 힘들어진다. 참고로 리눅스, 유닉스의 경우에는 세그멘테이션 오류가, 윈도우의 경우에는 일반 보호 오류(general protection fault)가 발생한다. 만약 이 겹쳐써진 데이터가 시스템의 메모리 할당자가 사용하는 북키핑 데이터라면, 이 오염은 시스템을 불안정하게 만들 수 있다. 객체 지향 프로그래밍의 경우 쓰레기 수집으로 허상 참조들은 참조되지 않는 객체를 파괴함으로써 예방된다. 이것은 추적이나 참조 횟수 계산 방식에 의해 보증된다. 그러나 파이널라이저(finalizer)는 허상 잠조를 예방하기 위하여 객체 재생을 요구하면서, 객체에 대한 새로운 참조를 생성할 수 있다. 와일드 포인터들은 포인터가 알려진 상태로 초기화되기 전에 사용될 때 발생한다. 이것들은 허상 포인터와 같은 오류적인 행동들을 보이지만, 탐지되기가 쉽다는 차이점이 있다. 많은 언어들에서 메모리의 객체를 명시적으로 지우는 것이나 반환 시의 스택 프레임 파괴는 적절한 포인터로의 변경을 수행하지 않는다. 이 포인터는 참조가 삭제되고, 다른 목적에 쓰이고 있더라도 계속 같은 위치를 가리킨다. 간단한 예시가 아래에 있다. { char *dp = NULL; /* ... */ { char c; dp = &c; } /* c falls out of scope */ /* dp is now a dangling pointer */ } 만약 운영체제가 널 포인터에 대한 런타임 참조를 탐지할 수 있다면, 해결책은 내부 블록이 종료되기 직전에 dp에게 0 (null)값을 주는 것이다. 다른 해결책으로는 추후의 초기화 없이 dp가 다시는 사용되지 못하게 하는 것이 있다. 다른 흔한 경우는 malloc()과 free() 라이브러리 호출의 무질서한 호출이다. 포인터는 가리키는 메모리 블록이 할당 해제가 되면 허상 포인터가 된다. 아래에서 한 것처럼 이것을 피하기 위한 방법으로는 이것이 참조하는 것이 할당 해제가 될 경우에 포인터를 null로 리셋하는 것이 있다. 아래의 코드를 보자. #include <stdlib.h> void func() { char *dp = malloc(A_CONST); /* ... */ free(dp); /* dp now becomes a dangling pointer */ dp = NULL; /* dp is no longer dangling */ /* ... */ } 너무 흔한 실수로는 스택 할당된 지역 변수의 주소들을 리턴하는 것이 있다. 호출된 함수가 리턴되면, 이 변수들을 위한 공간은 할당 해제되고, 이것들은 "쓰레기 값"을 갖게 된다. int *func(void) { int num = 1234; /* ... */ return &num; } 함수 호출 이후에 포인터로 읽는 것은 아직 정확한 값(1234)을 반환하지만, 이후에 호출된 다른 함수가 스택 저장소를 겹쳐쓸 수 있으므로 이후에는 더 이상 정확한 값을 갖지 못한다. num에 대한 포인터가 반환되어야 한다면, num은 반드시 이 함수 이상의 유효 범위를 가져야 한다. 이것은 static으로 정의함으로써 가능해질 수 있다. 와일드 포인터는 첫 사용시에 초기화를 생략함으로 인해 발생한다. 엄밀히 말해서 초기화가 강요되지 않는 언어의 모든 포인터는 와일드 포인터로 시작된다. 이것은 대부분의 경우에 초기화의 생략이 아니라 초기화를 건너뜀으로써 발생한다. 대부분의 컴파일러들은 이것에 대해 경고할 수 있다. int f(int i) { char *dp; /* dp is a wild pointer */ static char *scp; /* scp is not a wild pointer: * static variables are initialized to 0 * at start and retain their values from * the last call afterwards. * Using this feature may be considered bad * style if not commented */ } 버퍼 오버플로처럼 허상/와일드 포인터 버그들은 잦은 확률로 보안 허점으로 이어진다. 예를 들면 가상 함수 호출 시에, vtable 포인터가 겹쳐 써지므로 포인터가 다른 주소를 호출하는 경우가 있다. 다른 경우로, 포인터가 메모리 쓰기에 사용될 때 몇몇 다른 데이터 구조가 오염될 수 있다. 심지어 포인터가 허상 포인터가 된 이후에 메모리가 오직 한 번만 읽어지더라도, 이것은 정보 유출이나 권한 확대로 이어질 수 있다. 허상 포인터가 새로운 메모리 청크 할당 없이 할당 해제된 이후에 사용되면, "use after free" 취약점으로 사용된다. C에서, 가장 쉬운 방법은 포인터의 재설정을 보장하는 대체 버전의 free() 함수로 구현하는 것이 있다. 그러나 이 기법도 포인터의 복사본을 갖는 다른 포인터 변수들까지 보장하지는 않는다. #include <assert.h> #include <stdlib.h> /* Alternative version for 'free()' */ void safefree(void **pp) { /* in debug mode, abort if pp is NULL */ assert(pp); if (pp != NULL) { /* safety check */ free(*pp); /* deallocate chunk, note that free(NULL) is valid */ *pp = NULL; /* reset original pointer */ } } int f(int i) { char *p = NULL, *p2; p = (char *)malloc(1000); /* get a chunk */ p2 = p; /* copy the pointer */ /* use the chunk here */ safefree((void **)&p); /* safety freeing; does not affect p2 variable */ safefree((void **)&p); /* this second call won't fail */ char c = *p2; /* p2 is still a dangling pointer, so this is undefined behavior. */ return i + c; } 대체 버전은 malloc() 호출 이전에 빈 포인터의 유효성을 보장하는데 사용될 수 있다. safefree(&p); /* i'm not sure if chunk has been released */ p = malloc(1000); /* allocate now */ 이러한 사용들은 유용한 매크로를 만들어 주는 #define 지시자를 통해 가려질 수 있다. (메타언어를 생성하거나 따로의 툴 라이브러리에 삽입될 수 있다.) 모든 경우에 이 기법을 사용하는 프로그래머들은 free()가 사용되는 모든 요소에서 안전한 버전을 사용해야 한다. 또한 이 해결책은 단일한 프로그램이나 프로젝트로 제한되며, 적절히 문서화되어야 한다. 이러한 구조화된 해결책들 중에서, C++에서 사용하는 해결책은 smart pointers이다. 이것은 일반적으로 참조 횟수 계산 방식을 사용하여 객체를 재활용한다. 다른 해결책으로는 tombstones 기법과 locks-and-keys 기법이 있다. 이 접근법은 free()를 비활성화하고 쓰레기 수집(Garbage Collection)으로 객체를 재활용 함으로써 허상 포인터를 완전하게 제거한다. 자바 같은 언어에서는 메모리 할당 해제를 명시적으로 하는 메커니즘이 없기 때문에 허상 포인터는 발생하지 않는다. 대신에 쓰레기 수집기는 객체가 어떤 참조에서도 닿을 수 없는 경우에 메모리를 할당 해제한다. 허상 포인터 오류를 노출시키기 위한 한 가지 보편적인 기법으로, 포인터들을 널 포인터로 세팅하거나 가리키는 저장소가 풀린 경우에 유효하지 않은 주소로 세팅하는 방식이 있다. 널 포인터가 역참조되는 경우, 프로그램은 즉시 종료된다. 이 방식은 잘 드러나지 않는 프로그래밍 실수를 찾고 해결하기 쉽게 해주지만, 포인터의 여러 복사본이 있는 경우에는 도움이 되지 않는다. 몇몇 디버거들은 할당 해제된 데이터를 0xDEADBEEF 같은 특정한 패턴으로 자동으로 겹쳐쓰고 파괴해 준다. (마이크로소프트의 Visual C/C++ 디버거는 무엇이 할당 해제되었는지에 따라 0xCC, 0xCD 또는 0xDD를 사용한다.). 이 방식은 데이터를 쓸모없게 함으로써 재사용을 막아주는 것이다. Polyspace, TotalView, Valgrind, Mudflap, AddressSanitizer, 또는 LLVM에 기반한 툴들은 허상 포인터의 사용을 탐지하는데 사용될 수 있다. SoftBound와 CheckPointer 같은 다른 툴들은 합법적인 포인터의 값들을 모으고 추적하며, 유효성을 위해 메타데이터에 대한 각 접근을 체크함으로써 소스코드를 기구화한다. 클래스들의 작은 집합이 의심될 시에, 다른 전략으로는 자신들의 모든 멤버 함수들을 임시적으로 virtual으로 만드는 방법이 있다. 클래스 인스턴스들이 파괴/할당 해제된 후에, 가상 메소드 테이블에 대한 이것의 포인터는 NULL로 세팅되고, 멤버 함수에 대한 호출은 프로그램의 충돌로 이어져, 디버거에서 길티 코드로서 보이게 된다. Wild branch Dalci, Eric; anonymous author; CWE Content Team (2012년 5월 11일). “CWE-416: Use After Free”. 《Common Weakness Enumeration》. Mitre Corporation. 2014년 4월 28일에 확인함.  Visual C++ 6.0 memory-fill patterns Mudflap Pointer Debugging Dhurjati, D. and Adve, V. Efficiently Detecting All Dangling Pointer Uses in Production Servers