스택 버퍼 오버플로

스택 버퍼 오버플로()는 프로그램이 프로그램이 의도한 데이터 구조체의 메모리 주소(일반적으로 고정된 버퍼 길이를 갖는) 외부의 콜 스택에 쓸 때 발생하는 버그이다.

스택 버퍼 오버플로 버그는 프로그램이 스택에 위치한 버퍼에 할당된 것보다 더 많은 데이터를 쓸 때 발생한다. 이것은 항상 스택에서 인접한 데이터의 오염을 유발하며 실수로 오버플로된 경우 프로그램은 충돌하거나 부정확하게 동작한다. 스택 버퍼 오버플로는 버퍼 오버플로로 알려진 일반적인 프로그래밍 불량의 한 종류이다. 스택에서 버퍼에 겹쳐쓰는 것은 힙에 존재하는 버퍼에 겹쳐쓰는 것보다 더 문제를 일으키기 쉬운데 그 이유는 스택이 모든 활성화된 함수 호출을 위한 반환 주소를 포함하기 때문이다.

스택 버퍼 오버플로는 스택 스매싱이라고 알려진 공격의 한 부분으로서 고의적으로 유발될 수 있다. 만약 영향을 받는 프로그램이 특정한 권한에서 실행되고 있거나 신뢰되지 않는 네트워크 호스트들(또는 웹 서버)에서 데이터를 받는다면 이 버그는 잠재적으로 보안 취약점이 된다. 만약 스택 버퍼가 신뢰받지 않은 사용자로부터 공급된 데이터로 채워진다면 사용자는 실행 가능한 코드를 실행 중인 프로그램에 삽입하고 프로세스의 제어를 얻는 방식으로 오염시킬 수 있다. 이것은 공격자가 컴퓨터에 대한 비인가된 접근을 얻을 수 있는 가장 오래되고 신뢰받는 방식들 중 하나이다.

스택 버퍼 오버플로 취약점 공격 
스택에 기반한 버퍼 오버플로를 취약점 공격하는 정석적인 방법은 함수의 반환 주소를 공격자에 의해 제어된 데이터를 가리키는 포인터로 겹쳐쓰는 것이다. 이것은 다음의 예시에서 strcpy()를 사용해서 설명한다.#include <string.h>

void foo (char *bar)
{
   char  c[12];

   strcpy(c, bar);  // no bounds checking
}

int main (int argc, char **argv)
{
   foo(argv[1]);
}이 코드는 명령 줄에서 인자를 받고 지역 스택 변수 c에 복사한다. 이것은 명령 줄 인자가 12 문자 보다 작다면 정상적으로 동작한다(아래의 "그림 B" 에서 보이듯이). 11 문자보다 큰 어느 인자들도 스택의 오염을 일으킨다. (안전한 문자의 수는 버퍼의 크기보다 하나 작은데 이것은 C 프로그래밍 언어 문자열들이 0 문자로 종료되기 때문이다. 12 문자 입력은 그래서 13바이트의 저장소를 필요로 한다.)

 foo()의 프로그램 스택과 여러 입력들:

위의 "그림 C"에서 보듯이, 인자가 11 바이트보다 더 클 때  foo()는 프레임 포인터가 저장된 지역 스택 데이터, 무엇보다 반환 주소를 겹쳐쓴다. foo()가 반환할 때 이것은 반환 주소를 반환하고 그 주소로 점프한다(즉, 그 주소에서부터 명령어를 실행한다). 공격자는 반환 주소를 스택 버퍼 char c[12]를 가리키는 포인터로 겹쳐썼으며, 이제 공격자가 제공한 데이터를 포함하고 있다. 실제 스택 버퍼 오버플로 익스플로잇에서는 "A"의 문자열이 대신에 플랫폼과 바랬던 함수에 적합한 셸코드로 대체된다. 만약 프로그램이 특별한 권한을 가졌다면(예를 들면 SUID 비트), 공격자는 이 취약점을 슈퍼유저 권한을 얻는데 사용할 수 있을 것이다.

공격자는 또한 내부 변수 값을 몇몇 버그들을 익스플로잇하기 위해 수정할 수 있다. 다음의 예시를 보자:#include <string.h>
#include <stdio.h>

void foo (char *bar)
{
   float My_Float = 10.5; // Addr = 0x0023FF4C
   char  c[28];           // Addr = 0x0023FF30

   // Will print 10.500000
   printf("My Float value = %f\n", My_Float);

    /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       Memory map:
       @ : c allocated memory
       # : My_Float allocated memory

           *c                      *My_Float
       0x0023FF30                  0x0023FF4C
           |                           |
           @@@@@@@@@@@@@@@@@@@@@@@@@@@@#####
      foo("my string is too long !!!!! XXXXX");

   memcpy will put 0x1010C042 (little endian) in My_Float value.
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

   memcpy(c, bar, strlen(bar));  // no bounds checking...

   // Will print 96.031372
   printf("My Float value = %f\n", My_Float);
}

int main (int argc, char **argv)
{
   foo("my string is too long !!!!! \x10\x10\xc0\x42");
   return 0;
}

플랫폼 관련 차이점 
많은 플랫폼들이 콜 스택(스택 버퍼 오버플로 익스플로잇이 통하는 방식에 영향을 끼치는)의 구현에 미묘한 차이점들을 갖는다. 어떤 기계 구조는 레지스터에서 콜 스택의 탑 레벨 반환 주소를 저장한다. 이것은 어떤 겹쳐써진 반환 주소도 추후에 콜 스택이 풀려질 때까지 사용될 수 없다는 것을 의미한다. 익스플로잇 기법의 선택에 영향을 미칠 수 있는 다른 사항으로 대부분의 RISC 스타일 기계 구조들이 메모리에 대한 비정렬 접근을 허용하지 않는다는 것이 있다. 고정 길이 기계어 옵코드와 결합해서 이 제한은 ESP로 점프하는 기법의 구현을 거의 불가능하게 한다.

자라는 스택 
스택 버퍼 오버플로라는 토픽 하에서, 종종 논의되지만 거의 마주치지 않은 구조로 스택이 반대 방향으로 자라는 것이 있다. 구조에서의 이 변화는 자주 제안되는데, 같은 스택 프레임에서 발생하는 어떤 스택 버퍼의 오버플로도 반환 포인터를 겹쳐쓸 수 없기 때문이다. 이 주장된 보호 대한 조사에 의하면 이것은 매우 순진한 생각이다. 이전 스택 프레임에서 발생하는 어떤 오버플로도 여전히 반환 포인터를 겹쳐쓰고 버그의 악의적인 익스플로잇을 허용하게 된다. 예를 들면 위의 예시에서 foo의 반환 주소는 겹쳐써질 수 없는데, 이것은 오버플로가 실제로는 strcpy를 위한 스택 프레임 내에서 발생하기 때문이다. 그러나 strcpy 호출 도중에 오버플로 되는 버퍼가 이전 스택 프레임에 위치하기 때문에 strcpy를 위한 반환 포인터는 버퍼 보다 높은 메모리 주소를 가질 것이다. 이것은 겹쳐써지는 foo 를 위한 반환 포인터 대신 strcpy 를 위한 반환 주소가 겹쳐써진다는 것을 의미한다. 기껏해야 이것은 반대 방향으로 자라는 스택이 어떻게 스택 버퍼 오버플로가 익스플로잇될 수 있는지를 바꿀 뿐이고 익스플로잇 가능한 버그를 줄이지는 못한다는 것을 의미한다.

방어 방식 
해가 지날수록 악의적인 스택 버퍼 오버플로를 억제하는 계획들이 개발되어 왔다. 이것들은 크게 세 카테고리로 분류될 수 있다:
 발생한 스택 버퍼 오버플로를 탐지하고 악의적인 코드로의 명령어 포인터 리다이렉션을 방지한다.
 직접적으로 스택 버퍼 오버플로를 탐지하지 않고, 악의적인 코드가 스택에서 실행되는 것을 방지한다.
 실행 가능한 코드를 찾는 것을 신뢰하지 못하게 메모리 공간을 랜덤화한다.

스택 카나리스 
스택 카나리스는 악의적인 코드의 실행이 일어나기 전에 스택 버퍼 오버플로를 탐지하는데 사용된다. 이 방식은 프로그램 시작 시에 선택된 랜덤한 작은 정수를 스택 반환 포인터 전에 놓은 것이다. 대부분의 버퍼 오버플로는 낮은 곳에서 높은 곳으로 메모리 주소를 겹쳐쓰므로 반환 포인터를 겹쳐쓰기 위해서는 카나 값이 반드시 겹쳐써질 수 밖에 없다. 이 값은 루틴이 스택의 반환 주소를 사용하기 전에 바뀌었는지 검사된다. 이 기법은 스택 버퍼 오버플로를 익스플로잇하는 난이도를 증가시키는데, 공격자가 스택의 다른 중요한 변수들을 오염시키는 것 같은 전통적이지 않은 방식으로 명령어 포인터의 제어를 얻게 하기 때문이다.

실행 불가능 스택 
스택 버퍼 오버플로 익스플로잇을 방지하는 다른 접근법으로 (스택에서의 실행을 불허하는) 스택 메모리 영역에 대한 정책을 강제하는 것이 있다(W^X, "Write XOR Execute"). 이것은 공격자가 메모리에서 실행 보호를 비활성화하거나 셸코드 페이로드를 메모리의 보호되지 않은 영역에 놓는 방식을 찾게 한다. 이 방식은 현재 더 유명해 졌는데, 대부분의 데스크탑 프로세서들에서 하드웨어가 실행 불가능 플래그를 지원하기 때문이다.

이 방식이 스택 버퍼 오버플로 취약점 공격을 실패하게 하는 좋은 접근법인 반면에 여전히 문제는 존재한다. 첫째, 셸코드를 힙 같은 보호되지 않은 메모리에 저장하는 방식을 찾기가 쉽다.

이 방법이 아니더라도 다른 방식들도 존재한다. 가장 최악은 셸코드 생성을 위한 return-to-libc 공격 방식이다. 이 공격에서 악의적인 페이로드는 셸코드 없이, 하지만 적절한 콜 스택과 함께 스택을 로드해서 실행은 표준 라이브러리 호출들의 체인에 위치하게 된다. 이것은 실행이 실제로는 스택 자체에 위치하지 않기 때문에 동작한다.

return-to-libc의 변형으로 반환 주소 시리즈들을 설치하는 반환 지향형 프로그래밍(ROP)이 있는데 이것들 각각은 현재의 프로그램 코드나 시스템 라이브러리 내에서 기계어 명령어들의 작은 시퀀스(return으로 끝나는)들을 실행한다. 반환 전에 각각 몇몇 간단한 레지스터 조작이나 비슷한 실행을 수행하고 같이 잇는 이러한 일명 가젯들은 공격자의 종료를 달성한다. 이것은 반환 명령어와 비슷하게 행동하는 명령어들을 익스플로잇 함으로써 심지어 "반환 없는" ROP를 사용할 수도 있다.

랜덤화 
코드를 데이터에서 분리하는 것 대신 다른 기법으로 실행 가능한 프로그램의 메모리 공간을 랜덤화하는 것이 있다. 공격자가 위치할 실행 코드를 어디에 위치시킬 지를 결정할 필요가 있기 때문에 실행 가능한 페이로드는 실행 가능한 스택과 함께 제공되거나 ret2libc 또는 ROP같은 코드 재사용을 사용해서 구성되는데, 메모리 레이아웃을 랜덤화하는 것은 공격자가 코드가 어디에 존재하는지를 알게 하는 것을 방어한다. 그러나 구현은 일반적으로 모든 것을 랜덤화하지는 않으며, 심지어 ASLR이 실행 불가 스택에 적용되더라도 주로 실행 파일 자체는 고정 주소에 로드되며 공격자는 메모리의 이 고정 영역을 사용할 수 있다. 그러므로 모든 프로그램들은 반드시 메모리의 영역까지 랜덤화되는 PIE(position-independent executables)와 함께 컴파일되어야 한다. 랜덤화의 엔트로피는 구현 마다 다르며 충분히 작은 엔트로피는 본질적으로 메모리 공간을 무차별 대입 공격하는 것에 대해 문제가 될 수 있다.

같이 보기 
 주소 공간 배치 난수화
 버퍼 오버플로
 콜 스택
 컴퓨터 보안
 Exec Shield
 실행 가능 공간 보호
 취약점 공격
 포맷 스트링 버그
 grsecurity
 힙 오버플로
 정수 오버플로
 NX 비트
 PaX
 반환 지향형 프로그래밍
 보안 강화 리눅스
 스택 오버플로
 스토리지 위반
 보안 취약점
 return-to-libc 공격

각주 

컴퓨터 오류
취약점 공격
소프트웨어 버그