스택 버퍼 오버플로우(Stack Buffer Overflow)는 보안을 공부하지 않은 개발자들도 알만큼 유명하고 역사가 오래된 취약점이다. 최초의 웜이라고 불리는 모리스 웜도 스택 버퍼 오버플로우 공격을 통해 전파되었다고 한다.
이렇게 긴 역사를 자랑하는 스택 버퍼 오버플로우는 아직도 많은 소프트웨어에서 발견되고 있다.
CVE details에 따르면 스택 버퍼 오버플로우를 포함한 오버플로우 취약점은 이제까지 23,816개가 발견되어 전체에서 4번째로 많이 발견되었으며, 2022년에도 2,235개가 추가로 발견되었다.
이번 강의에서는 스택 버퍼 오버플로우가 발생하는 원인이 무엇인지, 그리고 이 취약점이 어떤 보안 문제로 이어질 수 있는지 살펴볼 것이다.

💡스택 오버플로우와 스택 버퍼 오버플로우의 차이점
스택 영역은 실행 중에 크기가 동적으로 확장될 수 있다. 그러나 한정된 크기의 메모리 안에서 스택이 무한히 확장될수는 없다. 스택 오버플로우(Stack Overflow)는 스택 영역이 너무 많이 확장돼서 발생하는 버그를 뜻한다.
반면, 스택 버퍼 오버플로우는 스택에 위치한 버퍼에 버퍼의 크기보다 많은 데이터가 입력되어 발생하는 버그를 뜻한다. 용어가 비슷하여 혼동하기 쉽지만, 둘은 전혀 다른 의미를 가지므로 사용에 주의해야한다.
** 스택 버퍼 오버플로우
버퍼 오버플로우
스택 버퍼 오버플로우는 스택의 버퍼에서 발생하는 오버플로우를 뜻한다. 이를 이해하기 위해 먼저 버퍼와 오버플로우의 개념부터 살펴보자.
버퍼
버퍼(Buffer)는 일상에서 '완충 장치'라는 뜻으로 사용되며, 컴퓨터 과학에서는 '데이터가 목적지로 이동되기 전에 보관되는 임시 저장소'의 의미로 쓰인다.
데이터의 처리 속도가 다른 두 장치가 있을 때, 이 둘 사이에 오가는 데이터를 임시로 저장해두는 것은 일종의 완충 작용을 한다. 이해를 돕기 위해 키보드에서 데이터가 입력되는 속도보다 데이터를 처리하는 속도가 느린 프로그램이 있다고 가정해보자. 사이에 별도의 장치가 없다면, 키보드의 입력 중에 프로그램에서 수용되지 못한 데이터는 모두 유실될 것이다. abcdefgh를 입력했는데 프로그램에는 abef만 전달될 수도 있다.
이런 문제를 해결하고자 수신 측과 송신 측 사이에 버퍼라는 임시 저장소를 두고, 이를 통해 간접적으로 데이터를 전달하게 한다. 송신 측은 버퍼를 데이터로 전송하고, 수신 측은 버퍼에서 데이터를 꺼내 사용한다. 이렇게 하면 버퍼가 가득 찰 때까지는 유실되는 데이터 없이 통신할 수 있다. 빠른 속도로 이동하던 데이터가 안정적으로 목적지에 도달할 수 있도록 완충 작용을 하는 것이 버퍼의 역할이다.
현대에는 이런 완충의 의미가 많이 희석되어 데이터가 저장될 수 있는 모든 단위를 버퍼라고 부르기도 한다. 스택에 있는 지역변수는 '스택 버퍼', 힙에 할당된 메모리 영역은 '힙 버퍼'라고 불린다.
버퍼링
스트리밍 서비스를 사용해봤다면, 버퍼링(Buffering)이라는 표현을 들어봤을 것이다. 이는 버퍼에서 유래된 단어로, 송신 측의 전송 속도가 느려서 수신 측의 버퍼가 채워질 때까지 대기하는 것을 의미한다.
버퍼 오버플로우
버퍼 오버플로우(Buffer Overflow)는 문자 그대로 버퍼가 넘치는 것을 의미한다. 버퍼는 제각기 크기를 가지고 있는데 int로 선언한 지역변수는 4바이트의 크기를 갖고, 10개의 원소를 갖는 char 배열은 10바이트의 크기를 갖는다.
만약 10바이트 크기의 버퍼에 20바이트 크기의 데이터가 들어가려 하면 오버플로우가 발생한다. 100ml 컵에 200ml 물을 받으면 넘치는 것과 같은 원리이다.
일반적으로 버퍼는 메모리 상에 연속해서 할당되어있으므로, 어떤 버퍼에서 오버플로우가 발생하면, 뒤에 있는 버퍼들의 값이 조작될 위험이 있다.

버퍼 오버플로우는 일반적으로 어떤 메모리 영역에서 발생해도 큰 보안 위협으로 이어진다. 이번 강의에서는 스택 영역의 버퍼에서 오버플로우가 발생했을 때, 이것이 어떤 보안 위협으로 이어질 수 있을지 알아볼 것이다.
** 버퍼 오버플로우 공격 예시
중요 데이터 변조
버퍼 오버플로우가 발생하는 버퍼 뒤에 중요한 데이터가 있다면, 해당 데이터가 변조됨으로써 문제가 발생할 수 있다. 예를 들어 입력 데이터에서 악성 데이터를 감지하여 경고해주는 프로그램이 있을 때, 악성의 조건이 변경되면 악성 데이터에도 알람이 울리지 않을 수 있다. 또한 "https://twitter.com"와 통신하는 프로그램이 있다면, 주소를 "https://example.evil"로 조작하여 악성 서버와 데이터를 주고받게 할 수도 있다.
// Name: sbof_auth.c
// Compile: gcc -o sbof_auth sbof_auth.c -fno-stack-protector
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int check_auth(char *password) {
int auth = 0;
char temp[16];
strncpy(temp, password, strlen(password));
if(!strcmp(temp, "SECRET_PASSWORD"))
auth = 1;
return auth;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: ./sbof_auth ADMIN_PASSWORD\n");
exit(-1);
}
if (check_auth(argv[1]))
printf("Hello Admin!\n");
else
printf("Access Denied!\n");
}
위 코드의 main 함수는 argv[1]을 check_auth 함수의 인자로 전달한 후, 반환값을 받아온다. 이 때, 반환값이 0이 아니라면 "Hello Admin!"을, 반환값이 0이라면 "Access Denied!" 라는 문자열을 출력한다.
check_auth 함수에서는 16바이트 크기의 temp 버퍼에 입력받은 패스워드를 복사한 후, 이를 "SECRET_PASSWORD" 문자열과 비교한다. 문자열이 같다면 auth를 1로 설정하고 반환한다.
그런데 check_auth에서 strncpy 함수를 통해 temp 버퍼를 복사할 때, temp의 크기인 16바이트가 아닌 인자로 전달된 password의 크기만큼 복사한다. 그러므로 argv[1]에 16바이트가 넘는 문자열을 전달하면, 이들이 모두 복사되어 스택 버퍼 오버플로우가 발생하게 된다.
auth는 temp 버퍼의 뒤에 존재하므로, temp 버퍼에 오버플로우를 발생시키면 auth의 값을 0이 아닌 임의의 값으로 바꿀 수 있다. 이 경우, 실제 인증 여부와는 상관 없이 main 함수의 if(check_auth(argv[1]))는 항상 참이 된다.
데이터 유출
C 언어에서 정상적인 문자열은 널바이트로 종결되며, 표준 문자열 출력 함수들은 널 바이트를 문자열의 끝으로 인식한다. 만약 어떤 버퍼에 오버플로우를 발생시켜서 다른 버퍼와의 사이에 있는 널 바이트를 모두 제거하면, 해당 버퍼를 출력시켜 다른 버퍼의 데이터도 같이 출력시키는 것이 가능하다.
획득한 데이터는 각종 보호기법을 우회하는데 사용될 수 있으며, 해당 데이터 자체가 중요한 정보일 수도 있다.
// Name: sbof_leak.c
// Compile: gcc -o sbof_leak sbof_leak.c -fno-stack-protector
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main() {
char secret[16] = "secret message";
char barrier[4] = {};
char name[8] = {};
memset(barrier, 0, 4);
printf("Your name: ");
read(0, name, 12);
printf("Your name is %s.", name);
}
위 코드에서는 8바이트 크기의 name 버퍼에 12바이트의 입력을 받는다. 읽고자 하는 데이터인 secret 버퍼와의 사이에 barrier라는 4바이트의 널바이트로 채워진 배열이 존재하는데, 오버플로우를 이용하여 널 바이트를 모두 널 바이트가 아닌 다른 값으로 변경하면 secret을 읽을 수 있다.
실행 흐름 조작
Background: Calling Convention의 내용을 되짚어보면, 함수를 호출할 때 반환 주소를 스택에 쌓고, 함수에서 반환될 때 이를 꺼내어 원래의 실행 흐름으로 돌아간다고 했다. 이를 공격자의 관점에서 바라보면, '스택 버퍼 오버플로우로 반환주소(Return Address)를 조작하면 어떻게 될까' 하는 궁금증을 가져볼 수 있다.
실제로 함수의 반환 주소를 조작하면 프로세스의 실행 흐름을 바꿀 수 있다.
// Name: sbof_ret_overwrite.c
// Compile: gcc -o sbof_ret_overwrite sbof_ret_overwrite.c -fno-stack-protector
#include <stdio.h>
#include <unistd.h>
void win() {
printf("You won!\n");
}
int main(void) {
char buf[8];
printf("Overwrite return address with %p:\n", &win);
read(0, buf, 32);
return 0;
}
위 코드에서는 win()의 주소를 출력하고, 8바이트 버퍼 buf에 32바이트 입력을 받는다.
buf 이후에는 saved RBP값 8바이트와 반환주소 8바이트가 있으므로, b'A'*16 이후에 win() 주소를 이어붙여보내면 win() 함수를 호출하는 것이 가능하다.
1. 스택에 저장되는 순서:
- buf[8]: 8바이트의 버퍼
- Saved RBP: 함수 호출 이전의 RBP 값을 저장하는 공간 (8바이트)
- Return Address: main 함수가 끝난 후, 호출되었던 함수로 돌아가기 위한 주소 (8바이트)
2. 버퍼 오버플로우 공격:
buf는 8바이트 크기인데, 프로그램은 read(0, buf, 32)로 32바이트를 읽습니다. 이때 buf를 넘어선 Saved RBP와 Return Address가 있는 메모리 영역까지 덮어쓸 수 있습니다.
- buf를 넘어 첫 8바이트는 Saved RBP를 덮어쓰게 됩니다.
- 그 뒤의 8바이트는 Return Address를 덮어씁니다.
함수 호출이 끝난 후, 프로그램은 스택에 저장된 Return Address로 돌아가 실행을 계속합니다. 이 Return Address가 원래는 main 함수가 끝난 후 다음으로 이어지는 코드의 주소를 가리키지만, 공격자가 이 주소를 win() 함수의 주소로 덮어쓰면 프로그램 흐름이 win() 함수로 이어집니다.
- b'A' * 16:
- 8바이트의 buf와 8바이트의 Saved RBP를 덮어씁니다.
- 총 16바이트는 그냥 의미 없는 값(A)을 채워서 덮어쓰는 것입니다.
- win()의 주소를 이어서 보냄:
- 그 다음 8바이트에는 Return Address를 덮어씁니다. 여기서 Return Address에 win() 함수의 주소를 덮어쓰는 것이 중요합니다.
- 이때 printf("Overwrite return address with %p:\n", &win);로 win 함수의 주소가 출력되므로, 이 주소를 Return Address 자리로 덮어쓰면, main 함수가 끝난 후 프로그램이 win() 함수를 호출하게 됩니다.
16바이트(A * 16) 이후에 win() 함수의 주소를 이어서 보내면 스택을 통해 프로그램 흐름을 조작하여 win() 함수를 실행할 수 있다.
마치며
버퍼 오버플로우는 지정된 버퍼의 크기보다 많은 데이터가 입력되어 발생합니다. 버퍼 오버플로우는 모든 메모리 영역에서 발생할 수 있으며, 이를 통해 데이터 변조, 데이터 유출, 실행 흐름 조작 등이 가능할 수 있습니다. 이번 강의에서는 여러 메모리 영역 중 스택에서 버퍼 오버플로우가 발생하면 어떤 보안 위협으로 이어질 수 있을지 알아보았습니다.
스택 버퍼 오버플로우는 역사가 깊은 취약점인 만큼 다양한 보호 기법이 제시되었고, 이를 다시 우회하는 기법들도 많이 연구되었습니다. 로드맵을 따라가면서 이 둘이 어떻게 상호 견제하며 발전해 왔는지 살펴볼 수 있을 것입니다.
다음 강의에서는 스택 버퍼 오버플로우 취약점이 존재하는 간단한 예제 코드를 컴파일하고, 해당 바이너리를 공격하여 셸을 획득하는 실습을 해보겠습니다. 아울러 해당 취약점을 어떻게 패치할 수 있을지, 관련된 패치 방안도 함께 살펴보겠습니다
'공부중 > 시스템 해킹' 카테고리의 다른 글
| [Dreamhack] Exploit Tech: Return to Shellcode (0) | 2024.09.17 |
|---|---|
| [Dreamhack] Exploit Tech: Return Address Overwrite (0) | 2024.09.16 |
| [Dreamhack] Exploit Tech: Shellcode (execve 셸코드) & Quiz: shellcode (0) | 2024.09.14 |
| Tool: pwntools (0) | 2024.09.14 |
| Tool: gdb (0) | 2024.09.12 |
댓글