용어: 오버플로우
개요
- 영단어
- ‘넘치다’, ‘범람하다’
- 컴퓨터
- 데이터나 연산 결과가 저장 공간 또는 허용 표현 범위를 초과할 때 발생하는 현상
- 종류
- 버퍼 오버플로우 (메모리 영역 침범)
- 스택 오버플로우
- 힙 오버플로우
- 산술 오버플로우 (정수/부동소수점 연산 결과 범위 초과)
- 정수 오버플로우
- 부동소수점 오버플로우
- 언더플로우 (연산 결과가 표현 가능한 최솟값보다 작아지는 현상)
- 버퍼 오버플로우 (메모리 영역 침범)
버퍼 오버플로우
- 버퍼 이후 공간을 침범하는 현상
- 버그
- 입력 값 크기가 버퍼 크기보다 큰 경우
- 공격 방법
- 프로그램의 메모리 값을 임의로 변조 (리턴 주소, 함수 포인터 등)
- 쉘 코드 삽입 후 실행 흐름 탈취
- 종류
- 스택 오버플로우
- 힙 오버플로우
- 대처
- 스택 가드 (Stack Guard / Stack Canary)
- 버퍼와 리턴 주소 사이에 특정 패턴 값(카나리)을 배치
- 함수 반환 시 값이 변경되었으면 프로세스 종료
- GCC
-fstack-protector옵션으로 활성화
- 스택 쉴드 (Stack Shield)
- 함수 시작 시 리턴 주소를 별도의 안전 영역에 복사
- 함수 반환 시 복사된 주소와 비교하여 변조 여부 확인
- ASLR (Address Space Layout Randomization)
- 스택, 힙, 라이브러리 등의 메모리 주소를 실행마다 랜덤화
- 공격자가 주입한 코드의 정확한 주소를 예측하기 어렵게 만듦
- OS 수준에서 지원 (Linux
/proc/sys/kernel/randomize_va_space)
- 안전한 함수 사용
strcpy→strncpy또는strlcpygets→fgetssprintf→snprintf
- 경계 검사
- 입력 길이를 검증한 후 버퍼에 복사
- 스택 가드 (Stack Guard / Stack Canary)
- 예시
-
발생 코드
#include <cstring> #include <iostream> using namespace std; int main() { char buffer[4]; strcpy(buffer, "12345678"); // 4바이트 버퍼에 9바이트 복사 → 오버플로우 cout << buffer << endl; return EXIT_SUCCESS; } -
방어 코드 (스택 가드)
#include <cstring> #include <iostream> using namespace std; int main() { int canary = 0xDEADBEEF; // 카나리 값 설정 char buffer[4]; strcpy(buffer, "12345678"); cout << buffer << endl; if (canary != 0xDEADBEEF) { cout << "buffer overflow detected!" << endl; exit(-1); } return EXIT_SUCCESS; } -
방어 코드 (안전한 함수 + 길이 검사)
#include <cstring> #include <iostream> using namespace std; int main() { char buffer[4]; const char* input = "12345678"; if (strlen(input) >= sizeof(buffer)) { cout << "입력 값이 너무 깁니다" << endl; return EXIT_FAILURE; } strncpy(buffer, input, sizeof(buffer) - 1); buffer[sizeof(buffer) - 1] = '\0'; cout << buffer << endl; return EXIT_SUCCESS; }
-
스택 오버플로우
- 버퍼 오버플로우의 한 종류
- 스택 포인터가 스택의 경계를 넘어설 때 발생
- 원인
- 무한 재귀 호출: 재귀 함수가 종료 조건 없이 계속 호출되어 스택 프레임이 쌓임
- 지역 변수 과다 할당: 함수 내에서 매우 큰 크기의 지역 배열 선언
- 스레드 스택 크기 초과: 스레드는 기본적으로 제한된 스택을 가짐 (Linux 기본 8MB)
- 결과
- 프로그램 비정상 종료 (Segmentation Fault)
- 인접한 스택 프레임의 데이터 변조 가능
- 대처
- 재귀 함수를 반복문으로 대체
- 큰 데이터는 스택이 아닌 힙(
new,malloc)에 할당 - 재귀 깊이 제한 (최대 깊이 검사)
- 예시
-
무한 재귀 (스택 소진)
// ❌ 종료 조건 없는 무한 재귀 void func() { func(); } int main() { func(); } // Segmentation fault -
올바른 재귀 (종료 조건 포함)
// ✅ 종료 조건 있는 재귀 void func(int depth) { if (depth == 0) return; func(depth - 1); } int main() { func(100); } -
큰 지역 변수로 인한 스택 오버플로우
// ❌ 스택에 16MB 할당 → 스택 오버플로우 void badFunc() { char bigBuffer[16 * 1024 * 1024]; // 16MB 지역 변수 bigBuffer[0] = 0; } // ✅ 힙에 할당 void goodFunc() { char* bigBuffer = new char[16 * 1024 * 1024]; bigBuffer[0] = 0; delete[] bigBuffer; }
-
힙 오버플로우
- 버퍼 오버플로우의 한 종류
- 힙(동적 메모리) 영역에서 할당된 버퍼의 경계를 넘어 인접 힙 청크를 침범하는 현상
- 특징
- 스택 오버플로우와 달리 즉시 크래시가 발생하지 않을 수 있음
- 힙 메타데이터(청크 헤더) 변조로 메모리 관리자 자체를 공격 가능
- Double Free, Use-After-Free와 결합하여 악용되기도 함
- 원인
- 힙에 할당된 버퍼보다 큰 데이터를 복사
- 할당 크기 계산 오류 (정수 오버플로우로 인한 너무 작은 할당)
- 대처
- 힙 할당 크기와 복사 크기 일치 검증
- AddressSanitizer(
-fsanitize=address) 로 컴파일하여 탐지 - 안전한 메모리 관리 (스마트 포인터 사용)
-
예시
#include <cstring> #include <cstdlib> #include <iostream> using namespace std; int main() { // 4바이트만 할당 char* heapBuffer = new char[4]; // 8바이트 복사 → 힙 오버플로우: 인접 힙 청크 침범 strcpy(heapBuffer, "AAAABBBB"); cout << heapBuffer << endl; delete[] heapBuffer; return EXIT_SUCCESS; }- 정수 오버플로우로 인한 힙 오버플로우
// 악의적인 입력으로 size 값을 조작하면 할당 크기가 0이 될 수 있음 void process(size_t count) { // count = 0x40000001이면 count * 4 = 4 (32비트 오버플로우) char* buf = new char[count * 4]; // 이후 count 기준으로 쓰기 → 힙 오버플로우 memset(buf, 0, count * 4); delete[] buf; }
- 정수 오버플로우로 인한 힙 오버플로우
산술 오버플로우
정수 오버플로우
- 정수 연산 결과가 해당 타입의 표현 범위를 벗어나는 현상
- 범위
int8_t(8비트 부호 있는): -128 ~ 127uint8_t(8비트 부호 없는): 0 ~ 255int32_t(32비트 부호 있는): -2,147,483,648 ~ 2,147,483,647
- 결과
- 부호 있는 정수: C/C++ 기준 미정의 동작(Undefined Behavior) — 값이 예측 불가
- 부호 없는 정수: 래핑(Wrapping) 발생 — 최댓값 이후 0으로 순환
-
예시
#include <iostream> #include <cstdint> #include <climits> using namespace std; int main() { // 부호 있는 오버플로우 (Undefined Behavior) int32_t a = INT32_MAX; // 2,147,483,647 cout << a + 1 << endl; // 미정의 동작 (-2,147,483,648이 출력될 수 있음) // 부호 없는 래핑 (well-defined) uint8_t b = 255; cout << (int)(b + 1) << endl; // 0 (256 mod 256) uint8_t c = 0; cout << (int)(c - 1) << endl; // 255 (언더플로우 → 최댓값으로 래핑) return EXIT_SUCCESS; }-
오버플로우 검사
#include <iostream> #include <climits> using namespace std; // 덧셈 오버플로우 안전 검사 bool safeAdd(int a, int b, int& result) { if (b > 0 && a > INT_MAX - b) return false; // 오버플로우 if (b < 0 && a < INT_MIN - b) return false; // 언더플로우 result = a + b; return true; } int main() { int result; if (!safeAdd(INT_MAX, 1, result)) { cout << "오버플로우 발생!" << endl; } return EXIT_SUCCESS; }
-
부동소수점 오버플로우
- 부동소수점 연산 결과의 지수부가 타입의 표현 한계를 초과하는 현상
- 결과
float: 최대 약 3.4 × 10³⁸ 초과 시+Inf(양의 무한대)double: 최대 약 1.8 × 10³⁰⁸ 초과 시+Inf
-
예시
#include <iostream> #include <cmath> #include <cfloat> using namespace std; int main() { float f = FLT_MAX; cout << f * 2 << endl; // inf double d = DBL_MAX; cout << d * 2 << endl; // inf cout << isinf(f * 2) << endl; // 1 (true) return EXIT_SUCCESS; }
언더플로우
- 오버플로우의 반대 개념
- 종류
- 정수 언더플로우
- 부호 없는 정수에서 0보다 작은 값으로 빼는 경우 최댓값으로 래핑
- 부호 있는 정수에서 최솟값보다 작아지는 경우 (미정의 동작)
- 부동소수점 언더플로우
- 지수부가 타입의 최솟값보다 작아져 0에 가까워지다가 결국 0이 되는 현상
float: 최소 약 1.2 × 10⁻³⁸ 미만 시 0 또는 비정규화 수
- 정수 언더플로우
-
예시
#include <iostream> #include <cfloat> using namespace std; int main() { // 부동소수점 언더플로우 float f = FLT_MIN; // 약 1.175e-38 (최소 정규화 수) cout << f / 1e20 << endl; // 0 또는 매우 작은 비정규화 수 // 부호 없는 정수 언더플로우 (래핑) unsigned int u = 0; cout << u - 1 << endl; // 4294967295 (UINT_MAX) return EXIT_SUCCESS; }