[TIL] 오버플로우, 언더플로우, 버퍼 오버플로우 위험성

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 char255 (2⁸-1) 
unsigned short65535 (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 등비어 있는데 popstack.push(4) when full런타임 오류
메모리스택 깊이 초과 / 버퍼 초과거의 없음char buf[10]; strcpy(buf, input);해킹 가능
보안 대책Rust, 메모리 안전 언어컴파일러 경고, 타입 안전Rust, SwiftOS/국가 차원 도입 중

🔥 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++ 기반 시스템에서 발생