2025-09-02 22:05
-
오버플로우는 데이터가 자신에게 할당된 메모리 공간보다 커서 넘쳐흐르는 현상을 말하며, 컴퓨터의 유한한 자원에서 비롯된 근본적인 문제입니다.
-
종류는 크게 계산 오류를 유발하는 ‘정수 오버플로우’와 시스템 제어권을 탈취할 수 있는 ‘버퍼 오버플로우’ 등으로 나뉩니다.
-
아리안 5호 로켓 폭발부터 ‘핵 쏘는 간디’ 버그까지, 오버플로우는 역사적으로 다양한 문제를 일으켰으며, 이를 막기 위한 다층적인 방어 기술이 발전해왔습니다.
컴퓨터 과학의 보이지 않는 암초, 오버플로우 완벽 가이드
컴퓨터는 무한한 공간이 아닙니다. 우리가 물을 컵에 따를 때 컵의 용량을 넘으면 물이 넘쳐흐르듯, 컴퓨터의 메모리도 정해진 크기를 초과하는 데이터를 담으려 할 때 ‘오버플로우(Overflow)‘라는 현상이 발생합니다.
이것은 단순히 데이터가 손실되는 사소한 오류가 아닙니다. 때로는 프로그램 전체를 비정상적으로 종료시키고, 계산 결과를 완전히 뒤바꿔 금전적 손실을 일으키며, 심지어는 시스템의 제어권을 해커에게 넘겨주는 치명적인 보안 취약점이 되기도 합니다.
이 핸드북은 개발자라면 반드시 이해해야 할 오버플로우의 모든 것을 다룹니다. 왜 이런 현상이 발생하는지 근본적인 원인부터 시작해, 가장 대표적인 유형들과 실제 역사적 사건들, 그리고 우리의 코드를 안전하게 지킬 수 있는 방어 전략까지, 오버플로우의 세계를 완벽하게 탐험해 보겠습니다.
1장: 정수 오버플로우 (Integer Overflow) - 가장 순수하지만 가장 위험한 오류
1. 왜 발생하는가? 컴퓨터의 숫자 표현 방식
오버플로우를 이해하려면 컴퓨터가 숫자를 어떻게 저장하는지부터 알아야 합니다. 컴퓨터는 모든 데이터를 0과 1의 조합, 즉 비트(Bit)로 저장합니다. 8개의 비트가 모여 1바이트(Byte)가 되죠.
예를 들어, 8비트 부호 없는 정수(unsigned int)
자료형이 있다고 가정해 봅시다. 이 자료형은 8개의 비트를 모두 사용해 0부터 255까지의 숫자를 표현할 수 있습니다.
십진수 | 이진수 |
---|---|
0 | 00000000 |
1 | 00000001 |
… | … |
255 | 11111111 |
만약 최댓값인 255에 1을 더하면 어떻게 될까요? 8개의 비트로는 더 이상 표현할 공간이 없습니다. 11111111
에 1을 더하면 100000000
이 되지만, 8비트 공간에는 앞의 8개 비트(00000000
)만 남고 맨 앞의 1은 버려집니다. 그 결과, 256이 아닌 0이 되어버립니다. 이것이 바로 정수 오버플로우입니다.
음수까지 표현하는 부호 있는 정수(signed int)
는 더 흥미롭습니다. 8비트 부호 있는 정수는 맨 앞 비트(최상위 비트, MSB)를 부호(0: 양수, 1: 음수)로 사용하고, 나머지 7비트로 값을 표현합니다. 따라서 -128부터 127까지 표현할 수 있습니다.
여기서 최댓값인 127에 1을 더하면, 01111111
+ 1 = 10000000
이 됩니다. 맨 앞 비트가 1로 바뀌면서 이 숫자는 양수가 아닌 음수로 해석되며, 2의 보수 표현법에 따라 -128이 됩니다. 최댓값에서 한계를 넘자 최솟값으로 ‘순환(Wrap-around)‘해버린 것입니다.
2. 역사 속의 오버플로우: 현실을 뒤바꾼 버그들
아리안 5호 로켓 폭발 사고 (1996)
정수 오버플로우가 일으킨 가장 비극적인 사고 중 하나입니다. 유럽 우주국의 아리안 5호 로켓은 발사 후 37초 만에 공중에서 폭발했습니다. 원인은 로켓의 속도를 나타내는 64비트 부동소수점 값을 관성항법장치의 16비트 부호 있는 정수 변수에 변환하여 저장하는 과정에서 발생했습니다.
로켓의 속도가 너무 빨라지자, 그 값이 16비트 정수가 표현할 수 있는 최댓값(32,767)을 넘어섰고, 오버플로우가 발생하며 하드웨어 예외를 일으켰습니다. 이 예외 처리 실패가 연쇄 반응을 일으켜 로켓의 자세 제어 시스템을 마비시켰고, 결국 10년간 70억 달러를 투입한 프로젝트가 한순간에 잿더미가 되었습니다.
문명 ‘핵 쏘는 간디’ 버그
게임 역사상 가장 유명한 버그로, 오버플로우의 한 종류인 **언더플로우(Underflow)**가 원인이었습니다. 게임 ‘문명’에서 각 지도자는 공격성 수치를 가지고 있었습니다. 평화주의자인 간디의 초기 공격성은 가장 낮은 1이었습니다.
그런데 게임 시스템상 ‘민주주의’ 체제를 채택하면 지도자의 공격성이 2 감소하는 로직이 있었습니다. 간디가 민주주의를 채택하자, 그의 공격성은 1 - 2 = -1
이 되었습니다. 당시 공격성 데이터는 부호 없는 8비트 정수(0~255)로 처리되었기에, -1이 되는 순간 언더플로우가 발생하여 최댓값인 255로 순환했습니다. 그 결과, 세상에서 가장 평화롭던 간디가 갑자기 가장 호전적인 지도자로 돌변해 무자비하게 핵미사일을 발사하는 기현상이 벌어졌습니다.
2장: 버퍼 오버플로우 (Buffer Overflow) - 해커가 가장 사랑하는 취약점
정수 오버플로우가 계산 오류에 가깝다면, 버퍼 오버플로우는 시스템의 심장부를 겨누는 날카로운 칼과 같습니다.
1. 버퍼란 무엇인가?
버퍼(Buffer)는 데이터를 한 곳에서 다른 곳으로 옮길 때 임시로 저장하는 메모리 공간입니다. 예를 들어, 사용자가 로그인 창에 아이디를 입력하면, 프로그램은 이 아이디를 잠시 담아둘 ‘버퍼’를 메모리에 만듭니다.
// 10글자 크기의 버퍼를 만드는 C언어 코드 예시
char buffer[10];
버퍼 오버플로우는 이 정해진 크기의 버퍼에 더 큰 데이터를 복사할 때 발생합니다. 10칸짜리 상자에 15개의 물건을 억지로 밀어 넣으면, 뒤에 있던 다른 상자들까지 물건이 침범하는 것과 같습니다. 메모리에서도 마찬가지로, 버퍼 뒤에 위치한 다른 중요한 데이터들을 덮어쓰게 됩니다.
2. 스택 버퍼 오버플로우의 원리
이 취약점은 프로그램의 메모리 구조 중 스택(Stack) 영역에서 특히 치명적입니다. 스택은 함수가 호출될 때의 지역 변수, 매개변수, 그리고 가장 중요한 **‘복귀 주소(Return Address)‘**를 저장하는 공간입니다. 복귀 주소란, 함수가 모든 작업을 마친 뒤 돌아가야 할 원래 코드의 위치를 가리키는 이정표입니다.
해커는 이 복귀 주소를 노립니다.
-
취약점 발견: 프로그램이
strcpy
,gets
처럼 입력값의 길이를 검사하지 않는 위험한 함수를 사용하여 사용자 입력을 버퍼에 복사하는 부분을 찾아냅니다. -
악성 코드 주입: 해커는 버퍼의 크기를 훨씬 초과하는 긴 길이의 데이터를 입력값으로 준비합니다. 이 데이터의 앞부분에는 시스템 명령어를 실행하는 악성 코드(셸코드, Shellcode)를 심어둡니다.
-
복귀 주소 변조: 데이터의 뒷부분에는 덮어쓰고 싶은 ‘복귀 주소’를 넣습니다. 이 주소는 바로 앞서 심어둔 셸코드가 시작되는 메모리 주소입니다.
-
권한 탈취: 프로그램은 해커가 입력한 긴 데이터를 버퍼에 복사하다가 오버플로우를 일으킵니다. 버퍼 뒤에 있던 원래의 복귀 주소는 해커가 설계한 악성 주소로 덮어씌워집니다.
-
실행: 함수가 정상적으로 종료되고 스택에 저장된 ‘복귀 주소’로 돌아가려는 순간, 변조된 주소, 즉 셸코드가 있는 곳으로 점프하게 됩니다. 결국, 프로그램은 해커가 심어놓은 코드를 스스로 실행하게 되고 시스템의 제어권은 넘어가게 됩니다.
3. 왜 C/C++에서 유독 많이 발생할까?
C와 C++ 언어는 메모리를 직접 제어할 수 있는 강력한 기능을 제공하는 대신, 개발자에게 메모리 관리의 책임을 전적으로 맡깁니다. 배열의 경계를 자동으로 검사해주는 기능이 없어 buffer[10]
에 11번째 데이터를 넣으려는 시도를 막지 않습니다. 성능을 최우선으로 설계했기 때문입니다. 이러한 언어적 특성 때문에 버퍼 오버플로우는 C/C++로 작성된 프로그램에서 가장 빈번하게 발생하는 고전적인 취약점이 되었습니다.
3장: 그 외의 오버플로우
스택 오버플로우 (Stack Overflow) - 끝나지 않는 호출
프로그래머들에게는 웹사이트 이름으로 더 익숙한 이 용어는, 사실 버퍼 오버플로우와는 다른 종류의 오류입니다. 함수가 호출될 때마다 해당 함수의 정보(지역 변수, 복귀 주소 등)가 ‘스택 프레임’이라는 단위로 스택에 차곡차곡 쌓입니다.
만약 함수가 자기 자신을 무한히 호출하는 **무한 재귀(Infinite Recursion)**에 빠지면 어떻게 될까요? 스택 프레임이 계속해서 쌓이다가 결국 스택에 할당된 메모리 공간을 모두 소진하게 됩니다. 이 상태가 바로 스택 오버플로우이며, 프로그램은 비정상적으로 종료됩니다.
void recursive_function() {
recursive_function(); // 탈출 조건 없이 자기 자신을 계속 호출
}
4장: 오버플로우를 막는 방법 (방어 전략)
오버플로우를 막기 위한 노력은 개발자 개인의 노력부터 컴파일러, 운영체제 수준까지 다층적으로 이루어집니다.
1. 개발자 수준: 시큐어 코딩 (Secure Coding)
가장 기본적인 방어선입니다.
- 안전한 함수 사용: 경계 검사를 수행하지 않는 위험한 함수 대신, 복사할 데이터의 최대 길이를 지정할 수 있는 안전한 함수를 사용해야 합니다.
위험한 함수 | 대체할 안전한 함수 |
---|---|
gets() | fgets() |
strcpy() | strncpy() , strcpy_s() |
strcat() | strncat() , strcat_s() |
sprintf() | snprintf() |
- 입력값 검증: 사용자가 입력한 데이터의 길이와 형식을 항상 신뢰하지 말고, 서버 측에서 반드시 검증하는 로직을 추가해야 합니다.
2. 컴파일러 수준: 방어 기술 자동 삽입
최신 컴파일러들은 오버플로우 공격을 막기 위한 방어 코드를 컴파일 시점에 자동으로 삽입해 줍니다.
- 스택 카나리 (Stack Canary): 함수 시작 시 스택의 복귀 주소 바로 앞에 ‘카나리’라는 무작위 값을 숨겨둡니다. 만약 버퍼 오버플로우가 발생해 복귀 주소를 덮어쓰려 한다면, 이 카나리 값도 함께 변조될 것입니다. 함수가 종료되기 직전, 카나리 값이 원래 값과 일치하는지 검사하여 변조되었다면 공격 시도로 간주하고 프로그램을 즉시 중단시킵니다. (과거 탄광에서 유독가스를 감지하기 위해 카나리아 새를 데리고 들어갔던 것에서 유래한 이름입니다.)
3. 운영체제 수준: 공격을 무력화하는 기술
현대 운영체제는 공격 코드가 실행되는 것 자체를 원천적으로 어렵게 만듭니다.
-
ASLR (Address Space Layout Randomization): 프로그램이 실행될 때마다 스택, 힙, 라이브러리 등의 메모리 주소를 무작위로 변경합니다. 해커는 공격을 위해 정확한 복귀 주소를 알아야 하는데, ASLR 때문에 그 주소를 예측하기가 매우 어려워집니다. ‘어디로 뛰어야 할지’ 모르게 만드는 기술입니다.
-
DEP/NX Bit (Data Execution Prevention / No-eXecute Bit): 메모리 영역에 ‘실행 권한’을 부여하거나 제거하는 기술입니다. 스택이나 힙 영역은 원래 데이터만 저장하는 공간이므로, 운영체제는 이 영역에 ‘실행 불가’ 딱지를 붙여놓습니다. 해커가 버퍼 오버플로우를 통해 셸코드를 주입하더라도, CPU가 해당 코드를 실행하려는 순간 운영체제가 이를 차단하고 프로그램을 강제 종료시킵니다.
결론: 기본으로 돌아가자
오버플로우는 컴퓨터의 유한한 자원이라는 태생적 한계에서 비롯된, 어쩌면 영원히 사라지지 않을 문제입니다. 아리안 5호의 비극부터 수많은 시스템을 감염시킨 웜 바이러스까지, 오버플로우는 사소한 코딩 실수가 얼마나 큰 파장을 일으킬 수 있는지 끊임없이 경고해왔습니다.
다행히 스택 카나리, ASLR, DEP와 같은 현대적인 방어 기술들은 과거의 공격 기법 대부분을 무력화시켰습니다. 하지만 가장 중요한 방어선은 언제나 코드를 작성하는 개발자 자신입니다.
우리가 다루는 데이터의 경계를 항상 인식하고, 안전한 함수를 습관처럼 사용하며, 외부 입력을 절대 신뢰하지 않는 ‘시큐어 코딩’의 원칙을 지키는 것. 그것이 바로 보이지 않는 암초, 오버플로우로부터 우리의 소프트웨어를 지키는 가장 확실하고 강력한 방법입니다.