Buffer

youngwiki

상위 문서: 컴퓨터 시스템

개요

버퍼(Buffer)란 데이터를 임시로 저장하는 배열이다. 예를 들면, 동영상 스트리밍 시, 끊김없는 재생을 위해서 일부 데이터를 저장하는 경우가 있으며, 이를 비디오 버퍼링(vedio buffering)이라고 한다. C언어에서는 보통 char buf[256];과 같이 선언되는 문자 배열을 의미하며, 사용자의 입력 문자열을 저장하는데 자주 사용된다.

Classic Buffer Overflow

스택 기반 오버플로우(Stack-based buffer overflow)란 스택 메모리에 존재하는 버퍼에 데이터가 초과되어 쓰이면서, 다른 메모리 영역까지 덮어쓰는 오류이다.[1] 해당 오류의 주된 원인은 gets(), scanf("%s"), strcpy(), strcat()와 같은 함수들을 배열 크기를 고려하기 않고 사용하는 것이다. 해당 오류는 해커가 이를 이용해 시스템을 공격하거나 악성코드를 주입할 수 있어서 보안상 매우 위험하다.

Example Program with BOF

아래는 스택 기반 오버플로우 오류가 발생할 수 있는 C언어 코드의 예시이다:

void echo(void) {
    char buf[8];
    gets(buf); //Never use this function. 너무 많은 BOF의 원인
    puts(buf);
}

int main(void) {
    echo();
    return 0;
}
jschoi@ubuntu:~$ ./bof
Hello
Hello
jschoi@ubuntu:~$ ./bof 
0123456789ABCDE 
0123456789ABCDE
jschoi@ubuntu:~$ ./bof
0123456789ABCDEF
0123456789ABCDEF
Segmentation fault

위 코드에서 gets(buf)는 입력 크기를 검사하지 않으므로, 8바이트 초과 입력 시 버퍼를 넘쳐서 다른 데이터를 덮어 쓴다. 이때, 동일하게 8바이트 버퍼를 초과한 입력 "0123456789ABCDE"은 괜찮고, "0123456789ABCDEF"에서 비로소 오류가 생기는 이유는 아래 어셈블리 코드를 관찰하여 알 수 있다:

Figure 1. Stack Frame Layout
Figure 1. Stack Frame Layout
(gdb) disassemble echo
0x401136:	sub    $0x18,%rsp
0x40113a:	lea    0x8(%rsp),%rdi
0x40113f:	mov    $0x0,%eax
0x401144:	call   0x401040 <gets@plt>
0x401149:	lea    0x8(%rsp),%rdi
0x40114e:	call   0x401030 <puts@plt>
0x401153:	add    $0x18,%rsp
0x401157:	ret    

(gdb) disassemble main
0x401158:	sub    $0x8,%rsp
0x40115c:	call   0x401136 <echo>
0x401161:	mov    $0x0,%eax
0x401166:	add    $0x8,%rsp
0x40116a:	ret

이때 해당 코드는 figure 1과 같이 스택 프레임을 형성한다. 어셈블리 코드에서 call 0x401040 <gets@plt> 명령어가 호출되면 입력된 문자열의 각 문자들은 lower address 부터 순차적으로 char buf[8]에 저장된다. 먼저, "Hello"가 입력된다면 아래와 같이 메모리에 저장된다:

Fiugre 2. Example Input "Hello"

이는 주어진 char buf[8]에 할당된 메모리 공간을 벗어나서 입력 문자들을 정상적인 입력이다. 5글자를 입력하였으나 실제로는 6개의 바이트가 저장된 이유는 문자열은 끝에 NULL 문자로 종료되어야 하기 때문이다. 이때 "0123456789ABCDE"를 입력한다면, 아래와 같이 메모리에 저장된다:

Figure 3. Example Input "0123456789ABCDE"

이는 char buf[8]에 할당된 메모리 공간을 분명히 침해하였다. 하지만 그럼에도 문제가 생기지 않는 이유는, 침해된 공간이 어셈블리 코드 상에서 사용되지 않는 메모리 공간이었기 때문이다. 따라서 char buf[8]에 할당된 메모리 공간을 벗어나 침범한 문자들은 어떤 중요한 데이터를 덮어쓰지 않았고, 이에 따라 프로그램은 정상적으로 수행된다. 하지만, "0123456789ABCDE"를 입력한다면, 아래와 같이 메모리에 저장된다:

Figure 4. Example Input "0123456789ABCDEF"

비로소 메모리 상에서 문제가 생긴 것을 관찰할 수 있다. 이는 입력된 문자열 "0123456789ABCDEF"가 retrun address가 저장된 주소 공간을 침범하였기 때문이다. 이는 결과적으로 프로그램이 echo() 함수 호출이 끝난 후 "0x401100"에 해당하는 주소 공간으로 돌아가도록 한다. 하지만 이는 적절한 return address가 아니므로, 프로그램에게 결과적으로 오작동과 크래시(crash)를 야기한다. 이는 단순히 프로그래머나 유저의 실수에서 그치는 일이 아니다. 만약 악의적인 해커가 함수 호출이 끝난 후 "0x636261"으로 프로그램이 점프하기를 원한다면, 아래와 같이 입력을 줄 수 있고, 이에 따라 메모리 공간은 figure 5와 같이 바뀐다.

jschoi@ubuntu:~$ ./bof
0123456789ABCDEFabc
0123456789ABCDEFabc
Segmentation fault
Figure 5. Control Hijack Example

위 예시가 말하고 있는 것은 해커가 입력값을 어떻게 주느냐에 따라 return address의 값이 그대로 바뀌므로, 악의적인 해커에 의해서 control flow가 바뀔 수 있다는 것이다. 이와 같이 해커가 control flow를 탈취하는 것을 control flow라고 한다. 이는 매우 치명적이고도 고전적인 문제이므로, 프로그램을 설계할 때 이와 같은 문제를 야기할 수 있는 함수들의 사용은 가급적이면 피해야 한다.

Real-world Example: Morris Worm

BOF는 1988년에 배포된 최초의 인터넷 멀웨어(malware)인 모리스 웜(morris worm)을 구현하는 데 사용된 보안 취약점이라고 알려져있다. 해당 멀웨어는 사용자 정보[2]를 조회하는 finger 유틸리티를 대상으로 하였다. 이때 finger 유틸리티는 정상적으로는 아래와 같이 입력한다:

$ finger jschoi@cspro.sogang.ac.kr

이때 문제는 구형의 finger 서버가 네트워크 입력을 안전하게 처리하지 못했다는 것이고, 이 때문에 BOF가 발생할 수 있었다. 모리스 웜은 이를 악용하여 아래와 같이 finger 요청에 매우긴 문자열과, 조작된 return address를 포함시킨 입력을 포함시켰다:

$ finger "<long string>+<new return address>"

이렇게 하면 서버에 BOF가 발생해 return address가 해커가 지정한 위치로 덮어쓰기 되고, 결과적으로 서버에서 웜이 실행되어 control hijack가 발생한다. 감염된 컴퓨터는 다른 컴퓨터를 자동으로 스캔하고 감염시키는 작업을 반복한다. 감염 속도는 매우 빨랐고, 당시 전체 인터넷에 연결된 컴퓨터의 약 10%(6000대)가 그 영향을 받았다.

각주

  1. 이를 stack smashing이라 부르기도 한다.
  2. 로그인 여부, 메일 등이 있다.