스택 버퍼 오버플로우 취약점이란 무엇일까
스택 버퍼 오버플로우 취약점에 관한 간단 설명
스택 버퍼 오버플로우란 무엇인가?
스택 버퍼 오버플로우는 메모리 커럽션(메모리 손상)과 관련된 시스템 취약점이다.
이것이 정확히 무엇을 의미하는지 살펴보기 이전에 용어 정리부터 해야한다.
용어 정리
버퍼
컴퓨터 과학에서 버퍼는 어떠한 정보가 A에서 B로 넘어갈 때 A와 B의 서로 다른 처리 능력으로 생기는 혼선을 방지하기 위해 중간에 존재하는 일종의 창고를 의미한다.
예를 들어 우리가 아주 안좋은 성능을 지닌 컴퓨터를 사용한다고 가정했을 때 이 컴퓨터는 유저가 키보드를 한 자 한 자 입력할 때마다 2초씩 처리 시간이 걸리는 것이다.
그렇다면 유저는 한 자를 입력하고 컴퓨터가 그것을 처리할 시간 2초를 기다렸다가 다음 자를 입력해야 한다.
이런식으로 컴퓨터가 작동한다면 이를 이용하는 유저는 답답해서 미쳐버릴 것이다.
버퍼를 이용하면 이를 방지할 수 있다. 유저가 키보드를 입력할 때마다 입력한 정보를 ‘버퍼’라는 임시 공간에 넣어두고 프로세서가 처리할 준비가 되면 그때 그 임시 공간인 ‘버퍼’에서 정보를 꺼내서 쓰도록 하는 것이다.
키보드로만 예시를 들었을 뿐이지 알게 모르게 이 버퍼라는 임시 공간은 컴퓨터 내에서 셀 수 없이 쓰이는 개념이다.
현대에 들어서는 단순히 ‘정보가 A에서 B로 넘어갈 때 중간에 놓이는 임시 저장 창고’라는 개념에서 벗어나 단순 데이터 묶음을 뜻하는 의미로 확장되기도 했다.
스택
리눅스를 기준으로 설명
스택은 컴퓨터의 주기억장치 메모리(RAM) 속에 들어있는 많은 메모리 공간 중 하나이다.
개발을 해보았으면 알 수 있듯이 프로그램에는 다양한 변수, 상수들이 존재한다.
그 중, 함수들과 지역 변수들이 저장되는 공간이 바로 스택이다.
예시
1
2
3
4
5
6
7
8
9
10
11
12
int add(int add_a, int add_b) {
int c = add_a + add_b;
return c;
}
int main() {
int main_a = 10;
int main_b = 20;
printf("%d", add(main_a, main_b));
return 0;
}
자세하게 다루지는 않겠지만 예를 들어 위와 같은 코드가 있다고 가정해보자.
(컴파일러 상황에 따라서 아래 설명처럼 작동하지 않을 수 있음)
해당 프로그램이 실행되면 스택 메모리에 main
함수가 올라가게 되고 지역 변수인 main_a
, main_b
이 저장될 공간을 스택에 마련한다.
그 다음 main
함수에서 add
함수를 호출하면 스택 메모리의 main
위로 add
함수가 올라간다(스택은 실제로는 위가 아니라 아래로 확장되지만 편의상 위로 설명).
올라감과 동시에 add_a
, add_b
변수들을 위한 자리를 확보한다. add
함수가 return c;
를 실행하여 동작을 모두 끝냈을 때엔 스택에 자리를 차지하고 있던 것을 모두 지우고 이젠 main
만 스택에 남게된다.
스택은 이런식으로 작동한다.
한마디로 정리해서 스택 버퍼 오버플로우는 단순히 프로그램이 실행될 때 쓰이는 스택이라는 메모리 공간(버퍼)이 넘쳐나면서 생기는 취약점이라는 것이다.
그렇기 때문에 메모리 커럽션(손상) 취약점이라고 불린다.
스택 버퍼 오버플로우 예시
스택 버퍼 오버플로우 취약점을 이용해서 공격자가 할 수 있는 것은 여러가지가 있고 대표적인 예시로는 다음이 있다.
- 중요 데이터 변조
- 실행 흐름 조작
- 데이터 유출
중요 데이터 변조
```c // 파일 이름: sbo1.c // 컴파일 명령어: gcc -o sbo1 sbo1.c -O0 -fno-stack-protector -z execstack -D_FORTIFY_SOURCE=0
#include
int main() { char name[10]; int money = 100;
1
2
3
4
5
6
7
printf("What is your name? ");
scanf("%20s", name);
printf("Hi, %10s\n", name);
printf("You have %d dollars\n", money);
return 0; } ``` 아주 조악하고 별 볼일 없는 프로그램이지만 나름 제대로된 금융 프로그램이라고 가정해보겠다.<br /> 프로그램이 실행되면 10 바이트 만큼의 길이를 가지는 `name`이라는 문자열과 `money`라는 변수가 초기화된다.<br /> 각자마다 `money`는 기본으로 100이 부여된다.<br />
다음으로 유저로부터 이름을 입력받는다.
입력받을 때 문제가 발생하는데 무엇이냐 하면, 10 바이트 만큼만 입력을 받아야하지만 개발자의 실수로 20 바이트만큼 입력을 받는 것이다.
그렇게 되면 스택 내부에서 name
바로 뒤에 위치하게된 money
의 값이 변조될 수 있다.
실제 프로그램 작동 분석
GDB 디버거를 이용해 프로그램을 실행하여 분석을 해보자.
1
2
3
4
5
6
0x555555555190 <main+39> lea rax, [rbp - 0xe] RAX => 0x7fffffffe5f2 ◂— 0
0x555555555194 <main+43> mov rsi, rax RSI => 0x7fffffffe5f2 ◂— 0
0x555555555197 <main+46> lea rax, [rip + 0xe7a] RAX => 0x555555556018 ◂— 0x2c69480073303225 /* '%20s' */
0x55555555519e <main+53> mov rdi, rax RDI => 0x555555556018 ◂— 0x2c69480073303225 /* '%20s' */
0x5555555551a1 <main+56> mov eax, 0 EAX => 0
0x5555555551a6 <main+61> call __isoc99_scanf@plt <__isoc99_scanf@plt>
이름을 입력받는 부분을 보면 rbp - 0xe
가 두 번째 인자로 들어가는 것이 보인다.
그렇다는 것은 name
이라는 변수의 위치는 rbp - 0xe
이라는 의미가된다. 임시로 ‘aaaaaaaaa’(a가 총 9개)를 입력해보고 해당 위치의 값을 읽어보면 다음과 같다.
1
2
pwndbg> x/2gx $rbp - 0xe
0x7fffffffe5f2: 0x6161616161616161 0x0001000000640061
이때 총 9개의 ‘a’(0x61)이 스택 메모리에 들어간 것이 보인다.
name
문자열 바로 옆에 있는 값을 보면 0x64가 보이는데 이것은 바로 money
의 값이다. 참고로 10진수로 0x64는 100이다.
우리는 개발자의 실수로 name
문자열에 10글자가 아닌 20글자를 입력할 수 있게됐다.
하지만 10글자가 넘어가버리면 money
의 메모리 영역까지 침범하게 돼 해당 값을 건드릴 수 있게 된다.
프로그램을 재실행해서 이번엔 ‘a’를 15글자 입력해보면 어떻게 될까?
pwndbg> Hi, aaaaaaaaaaaaaaaYou have 1633771873 dollars
. 이처럼 100이었던 money
의 값이 1633771837 달러로 변하게 됐다.
다른 예시들
이와 비슷한 개념, ‘원래 허용된 범위 이상의 값을 입력했을 때 벌어지는 현상’으로 발생할 수 있는 문제들이 여러가지 있다.
위와 같이 실제 코드로 예를 들면 너무 설명이 길어지기 때문에 간단히 요약하겠다.
데이터 유출
C언어에서 모든 문자열의 끝은 null 값이 들어가도록 돼있다.
이 null을 오버플로우를 통해 다른 값으로 변경했을 때 개발자가 원하지 않은 정보를 유출할 수 있다.
실행 흐름 조작
함수가 실행될 때 스택에 그 함수만을 위한 공간이 할당된다.
그리고 모든 공간의 시작 부분에는 그 다음으로 돌아갈 코드의 주소가 저장돼있고 이 것을 반환 주소(Return Address)라고 한다.
스택 버퍼 오버플로우로 해당 반환 주소의 값을 공격자 코드가 있는 주소로 변환을 해주면 해당 코드가 실행되면서 프로그램을 망칠 수 있다.