정보보안 공부

Buffer Overflow에 대하여

JustSangRok 2026. 6. 14. 21:18

Buffer Overflow 

오늘은 Buffer Overflow에 대해 알아보겠다.

 

Stack Overflow스택 영역 자체가 한계를 넘어 터지는 것. 무한 재귀가 대표적이다.

Buffer Overflow버퍼(배열 등)의 경계를 넘어 인접 메모리를 덮어쓰는 것.

스택 위에서 일어나면 Stack-based Buffer Overflow, 힙 위에서 일어나면 Heap Buffer Overflow로 부른다.


1. 메모리는 어떻게 나뉘어 있나

프로그램이 실행되면 OS는 메모리 공간을 몇 개의 영역으로 나눠 준다. 크게 네 가지이다.

높은 주소
┌─────────────────────────┐
│        Stack            │  ← 함수 호출마다 쌓임 (↓ 낮은 주소로 자람)
│           ↓             │
├─────────────────────────┤
│                         │
│      (빈 공간)           │
│                         │
├─────────────────────────┤
│           ↑             │
│         Heap            │  ← malloc 등 동적 할당 (↑ 높은 주소로 자람)
├─────────────────────────┤
│         Data            │  ← 전역 변수, static 변수
├─────────────────────────┤
│      Code (Text)        │  ← 기계어 명령, 읽기 전용
└─────────────────────────┘
낮은 주소

Code(Text) 영역 — 실행할 기계어 명령이 들어가는 곳. 읽기 전용이다.

Data 영역 — 전역 변수나 static 변수가 들어간다.

Heap 영역 — malloc 같은 함수로 프로그램 실행 중에 동적으로 할당받는 공간. 낮은 주소에서 높은 주소 방향으로 자란다.

Stack 영역 — 함수가 호출될 때마다 지역 변수와 함수 복귀 정보를 담는 공간. Heap과는 반대로 높은 주소에서 낮은 주소 방향으로 자란다.

Static 변수 — 프로그램이 시작될 때 메모리에 한 번만 할당되고, 종료될 때까지 값이 소멸하지 않고 유지되는 변수.
malloc — 메모리 할당 함수. 프로그램 실행 중에 필요한 만큼 메모리를 동적으로 할당받는 C언어 함수.

오늘 다룰 Buffer Overflow는 Stack 영역에서 일어난다.


2. Stack Frame 

함수가 호출될 때마다 스택에는 그 함수만의 공간이 하나 잡힌다. 이걸 Stack Frame이라고 부른다. 함수가 끝나면 이 프레임은 통째로 사라지고 호출했던 함수로 돌아간다.

스택 프레임 안에는 보통 세 가지가 들어간다.

구성 요소 역할

지역 변수 함수 안에서 선언된 변수들 (배열 버퍼 포함)
SFP (Saved Frame Pointer) 이전 함수 프레임이 어디였는지 기억해두는 값. 함수가 끝나면 이걸 보고 원래 자리로 복귀한다
RET (Return Address) 함수가 끝나면 어느 명령으로 돌아갈지를 가리키는 주소

여기서 시스템 해킹에서 공격자가 진짜로 노리는 것이 RET 이다. 이 값만 자기가 원하는 주소로 덮어쓸 수 있다면, 함수가 끝나는 순간 CPU의 다음 발걸음을 공격자가 정할 수 있게 된다.


3. 핵심 — 스택이 자라는 방향과 데이터가 채워지는 방향은 반대다

스택 자체는 함수가 호출될 때마다 높은 주소 → 낮은 주소로 자란다.

● 그런데 버퍼 안에 데이터를 채울 때는 보통 낮은 주소 → 높은 주소 방향으로 한 바이트씩 써내려간다.

이 비대칭이 바로 Buffer Overflow의 모든 것이다. 지역 변수 버퍼에 입력을 받기 시작하면 데이터가 위쪽으로 넘쳐서 SFP와 RET를 차례대로 덮어쓰게 되는 것이다. 만약 위 두 방향이 같았다면, 버퍼를 넘쳐도 RET가 있는 쪽이 아니라 반대 방향으로 흘러서 함수 제어 정보를 건드릴 일이 없었을 것이다.


4. 시나리오 예시

아래 그림은 어떤 함수의 스택 프레임을 그린 것이다. (예시)

위에서부터 차례로 살펴보면 이렇다.

위치 항목 의미

↑ 높은 주소 이전 함수의 스택 프레임 호출자의 영역
  RET (Return Address) 함수 끝나고 돌아갈 곳
  SFP (이전 베이스 포인터) 이전 프레임 기준점
  cmd_ip [256] system()에 넘어가는 명령 (지역 변수)
  dummy 정렬용 더미
↓ 낮은 주소 center_name [24] 입력이 처음 들어오는 곳

center_name은 24바이트짜리 버퍼이다. 그런데 만약 이 자리에 read()로 100바이트를 받는다고 하면, 24바이트를 넘는 순간부터 위쪽의 dummy를 지나 cmd_ip를 지나 SFP를 지나 RET까지 차례대로 덮이게 된다.

[정상 입력: 24바이트 이하]
center_name [24] ████████░░░░░░░░  ← 24바이트 안에서 끝남
dummy            ░░░░░░░░░░░░░░░░  ← 영향 없음
cmd_ip [256]     ░░░░░░░░░░░░░░░░  ← 영향 없음

[100바이트 입력 시]
center_name [24] ████████████████  ← 가득 참
dummy            ████████████████  ← 덮어씀
cmd_ip [256]     ████████░░░░░░░░  ← 일부 덮어씀
...      (계속 위로)               → SFP, RET까지 도달 가능

여기서 cmd_ip가 마침 system()의 인자로 들어가는 변수라면, 공격자는 center_name 입력을 넘쳐 흘려서 cmd_ip에 자기가 실행시키고 싶은 명령(예: /bin/sh)을 심어둘 수 있다. 그러면 그 다음에 호출되는 system(cmd_ip)은 공격자의 명령을 그대로 실행하게 된다.

offset은 gdb로 직접 재야 한다 

여기서 한 가지 함정이 있다. 위 그림만 보면 center_name(24) + dummy + cmd_ip 이런 식으로 단순 산수가 될 것 같지만, 컴파일러는 메모리 정렬(alignment)을 위해 변수 사이에 패딩(padding)을 끼워 넣는다.

그래서 선언 크기 합과 실제 간격이 딱 맞아떨어지지 않는다.

그래서 BoF 공격을 실제로 짤 때는 반드시 gdb 같은 디버거로 직접 재서 버퍼 시작점부터 RET까지의 정확한 거리를 알아내야 한다. (pwndbg나 peda 같은 확장에서는 cyclic이나 pattern create로 일정한 패턴을 입력해두고, 크래시가 났을 때 RIP에 남은 값을 가지고 오프셋을 역산하는 방식을 흔히 쓴다)


5. 방어 기법

이런 식으로 Buffer Overflow를 악용한 공격이 가능하기 때문에, 현대 시스템에는 여러 겹의 방어가 깔려 있다.

5-1. 안전한 함수 쓰기

가장 근본적인 방어는 위험한 함수를 안 쓰는 것이다.

위험한 것 안전한 것
gets() fgets()
strcpy() strncpy() 또는 strlcpy()
sprintf() snprintf()
scanf("%s", ...) scanf("%<숫자>s", ...)

 

입력을 다룰 때는 반드시 버퍼 크기를 명시하는 함수(fgets 등)를 사용하고, 입력 길이를 검증하는 코드를 추가해야 한다.

5-2. OS와 컴파일러 차원의 보호기법

코드만으로 막을 수 없는 경우를 위해, OS와 컴파일러가 세 겹의 방어를 깔아둔다.

Stack Canary — 컴파일러가 함수 진입 시 스택의 RET 바로 아래에 비밀 값(canary) 을 심어둔다. 함수가 끝날 때 이 값이 변조됐는지 확인하고, 변조됐다면 즉시 프로그램을 abort한다. 버퍼 오버플로우가 RET까지 도달하기 전에 잡아내는 셈이다.

DEP / NX bit — 데이터 영역(스택·힙)을 "실행 불가" 로 표시한다. 공격자가 셸코드를 스택에 심어 RET를 거기로 향하게 해도, 그 코드는 실행되지 않는다.

ASLR — 스택·힙·라이브러리의 시작 주소를 매번 무작위로 배치한다. 공격자가 RET를 어디로 향하게 할지 미리 알 수 없게 만든다.

세 가지가 함께 깔려 있으면 단순 BoF로는 거의 뚫리지 않는다. 그래서 현대 시스템 해킹은 ROP(Return-Oriented Programming) 같은 기법으로 이 방어를 우회하는 쪽으로 발달했다.