5 분 소요

개요

  • 영단어
    • ‘넘치다’, ‘범람하다’
  • 컴퓨터
    • 데이터나 연산 결과가 저장 공간 또는 허용 표현 범위를 초과할 때 발생하는 현상
    • 종류
      • 버퍼 오버플로우 (메모리 영역 침범)
        • 스택 오버플로우
        • 힙 오버플로우
      • 산술 오버플로우 (정수/부동소수점 연산 결과 범위 초과)
        • 정수 오버플로우
        • 부동소수점 오버플로우
      • 언더플로우 (연산 결과가 표현 가능한 최솟값보다 작아지는 현상)


버퍼 오버플로우

  • 버퍼 이후 공간을 침범하는 현상
  • 버그
    • 입력 값 크기가 버퍼 크기보다 큰 경우
  • 공격 방법
    • 프로그램의 메모리 값을 임의로 변조 (리턴 주소, 함수 포인터 등)
    • 쉘 코드 삽입 후 실행 흐름 탈취
  • 종류
    • 스택 오버플로우
    • 힙 오버플로우
  • 대처
    • 스택 가드 (Stack Guard / Stack Canary)
      • 버퍼와 리턴 주소 사이에 특정 패턴 값(카나리)을 배치
      • 함수 반환 시 값이 변경되었으면 프로세스 종료
      • GCC -fstack-protector 옵션으로 활성화
    • 스택 쉴드 (Stack Shield)
      • 함수 시작 시 리턴 주소를 별도의 안전 영역에 복사
      • 함수 반환 시 복사된 주소와 비교하여 변조 여부 확인
    • ASLR (Address Space Layout Randomization)
      • 스택, 힙, 라이브러리 등의 메모리 주소를 실행마다 랜덤화
      • 공격자가 주입한 코드의 정확한 주소를 예측하기 어렵게 만듦
      • OS 수준에서 지원 (Linux /proc/sys/kernel/randomize_va_space)
    • 안전한 함수 사용
      • strcpystrncpy 또는 strlcpy
      • getsfgets
      • sprintfsnprintf
    • 경계 검사
      • 입력 길이를 검증한 후 버퍼에 복사
  • 예시
    • 발생 코드
      #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 ~ 127
    • uint8_t (8비트 부호 없는): 0 ~ 255
    • int32_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;
    }