[TIL] 오버플로우, 언더플로우, 버퍼 오버플로우 위험성
in TIL on Til Last modified at:
2025-04-03 TIL
📝 TIL (Today I Learned)
🔗 원본 이슈: #36
📅 작성일: 2025-04-03
🔄 최종 수정: 2025년 04월 09일
🍀 새롭게 배운 것
- 오버플로우, 언더플로우
- 버퍼 오버플로우가 왜 위험한지 궁금해서 이것도 알아보았다.
🔷 1. 숫자에서의 오버플로우 & 언더플로우
✔️ 정의
- 오버플로우 (Overflow): 값이 표현할 수 있는 범위를 초과한 경우
- 언더플로우 (Underflow): 값이 표현할 수 있는 최솟값보다 작아지는 경우
✔️ 예시: 정수(int)의 범위 초과
unsigned char x = 255;
x = x + 1; // -> x는 0이 됨! (오버플로우)
📌 왜 0이 될까?
unsigned char
은 0~255까지만 저장 가능 (8bit)- 255 + 1 = 256 → 표현할 수 없음 → 0부터 다시 시작 (mod 256)
signed char y = 127;
y = y + 1; // -> y는 -128이 됨 (signed overflow)
✔️ 오버플로우 숫자 예시
타입 | 최대값 | 설명 |
---|---|---|
unsigned char | 255 (2⁸-1) | |
unsigned short | 65535 (2¹⁶-1) | |
unsigned int | 약 42억 (2³²-1 = 4,294,967,295) | |
signed int | -2,147,483,648 ~ 2,147,483,647 |
🔷 2. 스택, 큐, 리스트 등 자료구조에서의 오버/언더플로우
✔️ 오버플로우
- 스택이 가득 찬 상태에서 push하면 발생
- 큐가 가득 찬 상태에서 enqueue하면 발생
Stack<Integer> stack = new Stack<>();
stack.setSize(3); // 최대 3개
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4); // ❌ StackOverflowError
✔️ 언더플로우
- 스택에서 비어 있는데 pop하면 발생
- 큐에서 요소 없는데 dequeue하면 발생
Queue<Integer> q = new LinkedList<>();
q.poll(); // 비었는데 빼면 null 반환 (언더플로우)
📌 비유
오버플로우: 컵에 물을 넘치게 부음
언더플로우: 컵에서 물을 꺼내려 했는데, 이미 텅 비어 있음
🔷 3. 메모리에서의 오버플로우 (Stack Overflow, Buffer Overflow)
이건 C/C++에서 아주 심각한 보안 문제를 일으키는 부분.
💣 스택 오버플로우 (Stack Overflow)
- 함수를 재귀적으로 너무 많이 호출해서 스택 메모리를 넘쳐버림
💣 버퍼 오버플로우 (Buffer Overflow)
- 고정된 크기의 메모리 배열(buffer)을 넘어서 데이터를 쓰는 것
- 공격자는 이걸 이용해서 악성 코드 실행, 프로그램 흐름 장악
void vulnerable(char *input) {
char buf[10];
strcpy(buf, input); // 길이 확인 안함
}
int main() {
vulnerable("AAAAAAAAAAAAAAAAAAAAAAAAA"); // 💥 buffer overflow
}
📌 해킹의 대표 기술
공격자가
buf
뒤에 있는 return address를 덮어씌워서, 자기 코드로 프로그램 흐름을 강제로 바꿔버림 → 시스템 장악
🚫 해결 방법: Rust 언어 사용
- C/C++의 위험한 메모리 접근을 차단하기 위해 미국 국무부(DoS)에서도 권장
- Rust는 메모리 안전(memory safety)을 컴파일 타임에 강제함
fn main() {
let mut v = vec![1, 2, 3];
println!("{}", v[100]); // panic! 런타임에서 안전하게 막아줌
}
- 배열 범위를 벗어나면 컴파일 또는 런타임에서 즉시 멈춰버림 (→ 보안상 안전)
✅ 전체 요약
범주 | 오버플로우 | 언더플로우 | 예시 | 문제 |
---|---|---|---|---|
정수 | 범위 초과 → 값이 초기로 순환 | 최솟값보다 작음 → 값 왜곡 | unsigned char x = 255 + 1 | 잘못된 계산 |
자료구조 | 공간 초과 시 push 등 | 비어 있는데 pop | stack.push(4) when full | 런타임 오류 |
메모리 | 스택 깊이 초과 / 버퍼 초과 | 거의 없음 | char buf[10]; strcpy(buf, input); | 해킹 가능 |
보안 대책 | Rust, 메모리 안전 언어 | 컴파일러 경고, 타입 안전 | Rust, Swift | OS/국가 차원 도입 중 |
🔥 1. 왜 버퍼 오버플로우가 위험한가?
✅ 메모리는 일렬로 연결된 공간이야
함수를 실행하면 스택(stack)에 다음과 같은 순서로 저장돼:
[로컬 변수]
[리턴 주소] ← 함수가 끝난 뒤 어디로 돌아갈지 주소
이걸 공격자가 조작할 수 있다면?
➡ 함수가 끝난 뒤 돌아갈 주소를 자기 마음대로 바꿀 수 있어!
➡ 해커가 원하는 코드로 흐름을 바꿔버릴 수 있음!! 😱
💣 2. 실전 예시로 보기 (C 코드)
void vulnerable(char *input) {
char buf[10];
strcpy(buf, input); // 🔥 위험! 길이 체크 안 함
}
공격 입력:
vulnerable("AAAAAAAAAA\x90\x90\x90\x90\xDE\xAD\xBE\xEF");
"AAAAAAAAAA"
→buf[10]
꽉 채움\x90...\xEF
→ 리턴 주소를 덮어씀!
💀 3. 해커가 하는 짓
📌 목적: 리턴 주소를 조작해서 자신이 심어둔 쉘코드(shellcode) 로 흐름을 튼다!
jmp *shellcode_address → 쉘 실행, 백도어 오픈
- 공격자가 만든 악성 코드를 버퍼 바로 뒤에 몰래 숨겨두고
- 리턴 주소를 그 코드 위치로 덮어씀
- 함수가 끝나는 순간 → 해커 코드 실행
🧠 비유로 이해해보자
너가 엘리베이터를 타고 10층(정상 리턴 주소)으로 가야 되는데,
누군가가 몰래 버튼 회로를 바꿔서 지하 해커실(해커 코드) 로 보내버린 것과 같아.
📍 왜 C/C++에서 잘 터지나?
- 포인터(pointer), 직접 메모리 접근, 길이 체크 안함
strcpy()
,gets()
,sprintf()
같은 함수들: 길이 제한이 없음 → 💥
🛡️ 대책: 메모리 안전 언어 (Rust, Swift 등)
Rust 예시:
let arr = [1, 2, 3];
println!("{}", arr[10]); // 컴파일 또는 런타임에서 panic!
Rust는:
- 배열 접근 시 범위 검사(bound check) 함
- 포인터를 마음대로 조작 못함
- 사용 후 자동으로 메모리 해제 (ownership)
➡ 해커가 리턴 주소에 접근 불가능
➡ 버퍼 오버플로우로는 뚫을 수 없음
🔐 실제 피해 사례
해킹 사건 | 설명 |
---|---|
MS Blaster 웜 (2003) | 버퍼 오버플로우로 윈도우 서비스 제어 |
Heartbleed (2014) | OpenSSL에서 메모리 무단 접근 |
SolarWinds (2020) | 내부 시스템 오염 후, 취약점 타고 들어감 |
➡ 대부분이 C/C++ 기반 시스템에서 발생