Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
Tags
- C
- terraform
- AWS 사고 사례 분석
- python
- operating system
- AWS 침해사고 사례 분석
- AWS 3 Tier Architecture
- dreamhack
- AWS 인프라 분석
- 프로그래머스
- 운영체제
- 네트워크
- AWS 인프라 아키텍처
- AWS 아키텍처 분석
- 드림핵
- AWS 침해 사고 사례 분석
- AWS Active Directory
- AWS
- reversing.kr
- AWS IAM Role
- reversing
- AWS 보안 아키텍처 분석
- network
- Amazon S3
- TryHackMe
- 침입 차단 시스템(IPS)
- AWS 보안 사고 사례 모음
- programmers
- 리버싱
- IAM Federation
Archives
- Today
- Total
lhywk 님의 블로그
[Pwnable] Buffer Overflow 본문
1. Memory Layout
프로그램이 실행되면 운영체제는 메모리를 다음과 같은 5가지 주요 세그먼트로 나눈다.
| 세그먼트 | 설명 |
| 텍스트 (Text) | 실행 가능한 기계어 코드 저장. 읽기 전용(Read-only). |
| 데이터 (Data) | 선언과 동시에 초기화된 전역 변수 및 정적 변수 저장. |
| BSS | 초기화되지 않은 전역 변수 저장. 프로그램 시작 시 0으로 초기화됨. |
| 힙 (Heap) | 사용자가 malloc(), new 등으로 동적 할당하는 공간. 낮은 주소 → 높은 주소. |
| 스택 (Stack) | 함수 호출 시 지역 변수, 매개변수, 반환 주소 등을 저장. 높은 주소 → 낮은 주소. |

변수별 메모리 위치
- x (전역 변수)
- 선언: int x = 100;
- 저장 위치: 데이터 세그먼트 (Data Segment)
- 이유: 함수 바깥에 선언된 전역 변수이며, 0이 아닌 값(100)으로 초기화되었기 때문에 데이터 세그먼트에 저장.
- a, b (지역 변수)
- 선언: int a=2;, float b=2.5;
- 저장 위치: 스택 (Stack)
- 이유: main 함수 내에 선언된 지역 변수. 함수가 호출될 때 스택에 생성되었다가 함수 실행이 끝나면 자동으로 사라짐.
- y (정적 지역 변수)
- 선언: static int y;
- 저장 위치: BSS 세그먼트
- 이유: static 키워드로 선언된 정적 변수는 프로그램 실행 시간 동안 계속 존재. 코드에서 별도로 초기화하지 않았으므로, 초기화되지 않은 정적 변수를 저장하는 BSS 세그먼트에 위치하며 값은 0으로 자동 초기화.
- ptr (포인터 변수)와 동적 메모리
- ptr 변수 자체
- 선언: int *ptr = ...
- 저장 위치: 스택 (Stack)
- 이유: ptr 자체는 main 함수 내에 선언된 지역 변수이므로 스택에 저장. 이 변수에는 힙에 할당된 메모리의 '주소 값'이 들어감.
- malloc으로 할당된 메모리 공간
- 할당 코드: malloc(2*sizeof(int))
- 저장 위치: 힙 (Heap)
- 이유: malloc 함수는 동적 메모리 할당을 요청하는 함수로, 힙 영역에 요청한 크기(정수 2개 크기)만큼의 공간을 할당받음. 코드의 ptr[0]=5;와 ptr[1]=6; 구문은 바로 이 힙 영역에 값을 저장하는 것.
- ptr 변수 자체
2. 함수 호출과 스택 프레임
함수가 호출될 때마다 해당 함수만을 위한 독립적인 공간인 스택 프레임이 생성된다.
2.1 스택 프레임 구성 요소
- 인수 (Arguments): 호출한 곳에서 전달한 값. (오른쪽에서 왼쪽 순서로 저장).
- 반환 주소 (Return Address): 함수 종료 후 돌아갈 코드의 위치.
- 이전 프레임 포인터 (Old EBP): 이전 함수의 기준 주소를 저장하여 복구용으로 사용.
- 지역 변수 (Local Variables): 함수 내부에서 선언된 변수들.

- 인수 전달: 함수를 호출하는 쪽에서 함수에 전달할 인수 b와 a를 역순으로(오른쪽부터) 스택에 push.
- 함수 호출: call func 명령이 실행되면, CPU는 함수 종료 후 돌아갈 반환 주소를 스택에 자동으로 push.
- 프레임 포인터 설정: func 함수가 시작되면, 먼저 이전 함수의 프레임 포인터(이전 프레임 포인터)를 스택에 push하여 백업. 그 후 현재 스택 위치를 현재 프레임 포인터(EBP)로 설정. 이 EBP는 func 함수가 실행되는 동안 기준점 역할.
- 지역 변수 공간 할당: 함수 내에서 사용할 지역 변수(x, y)를 위해 스택 포인터(ESP)를 이동시켜 공간을 확보.
2.2 레지스터 역할
- EBP (Base Pointer): 현재 스택 프레임의 기준점.
- EBP + 오프셋: 매개변수 접근
- EBP - 오프셋: 지역 변수 접근
- ESP (Stack Pointer): 현재 스택의 가장 꼭대기(가장 낮은 주소)를 가리킴.
3. Stack Buffer Overflow
스택에 할당된 버퍼의 크기를 초과하는 데이터를 입력하여 인접한 메모리를 덮어쓰는 공격이다.
공격자는 버퍼를 넘치게 채워 스택 프레임에 저장된 반환 주소를 자신이 심어놓은 악성 코드(셸코드)의 주소로 덮어쓴다. 함수가 종료되는 순간, CPU는 원래 돌아가야 할 곳이 아닌 공격자의 코드로 점프하게 된다.

코드 분석
- main 함수:
- char *str = "This is definitely longer than 12";
- 12바이트보다 훨씬 긴 문자열을 준비
- foo(str);
- 이 긴 문자열을 foo 함수에 인자로 전달
- char *str = "This is definitely longer than 12";
- foo 함수:
- char buffer[12];
- 함수 내부에 단 12바이트 크기의 buffer라는 지역 변수(저장 공간)를 스택에 생성
- strcpy(buffer, str);
- 핵심 문제 지점. strcpy는 전달받은 긴 문자열(str)을 크기 검사 없이 12바이트짜리 buffer에 그대로 복사하기 시작
- char buffer[12];
오버플로우 순간
- 버퍼 채우기: "This is def" (12바이트)까지의 문자열이 buffer[0]부터 buffer[11]까지 순서대로 채워짐
- 경계 침범 (Overflow): strcpy는 여기서 멈추지 않고 나머지 문자열("initely longer than 12")을 계속해서 복사
- 중요 데이터 파괴: buffer 바로 위에(더 높은 주소에) 위치해 있던 이전의 프레임 포인터와 반환 주소(Return Address)가 넘쳐흐른 데이터에 의해 순차적으로 덮어씌워짐
결과 및 영향
- 실행 흐름 조작: foo 함수가 실행을 마치고 원래의 호출 지점(main)으로 돌아가려고 할 때, 정상적인 반환 주소는 이미 엉뚱한 데이터(문자열의 일부)로 덮어씌워진 상태
- 프로그램 충돌: 프로그램은 이 망가진 주소로 점프하려고 시도하고, 이는 대부분의 경우 세그멘테이션 오류(Segmentation Fault)를 일으키며 비정상적으로 종료
4. 공격 페이로드 구성

1단계: 공격 전의 정상적인 스택
- 함수가 정상적으로 호출되면 스택에는 지역 변수인 buffer, 이전 프레임 포인터, 그리고 함수 종료 후 돌아갈 정상적인 반환 주소가 순서대로 쌓여 있다.
2단계: 공격용 페이로드 준비 ("badfile")
- 공격자는 프로그램에 입력할 악성 데이터 파일(페이로드)을 제작.
- 악성 코드 (Shellcode): 공격자가 최종적으로 실행시키고 싶은 기계어 코드. 주로 시스템의 제어권을 탈취하기 위한 셸(shell)을 실행시키는 코드라 셸코드
- 새로운 주소: 이 주소는 악성 코드가 메모리(버퍼)에 복사될 때 그 악성 코드의 시작 위치를 가리키는 정확한 메모리 주소
3단계: 오버플로우 발생 및 실행 흐름 탈취
- 취약한 함수(예: strcpy)가 이 badfile을 buffer로 복사
- badfile의 크기가 buffer보다 크므로 오버플로우가 발생
- 코드 삽입: badfile의 악성 코드 부분이 buffer 내부에 자리를 잡음
- 주소 덮어쓰기: badfile의 새로운 주소 부분이 버퍼를 넘어서, 기존의 정상적인 반환 주소가 있던 자리를 덮어씀
- 함수가 종료될 때, CPU는 스택에 저장된 반환 주소로 점프. 하지만 그 자리엔 공격자가 심어놓은 "새로운 반환 주소"가 있다.
- CPU는 이 새로운 주소로 점프하고, 그 주소는 바로 buffer 안에 위치한 악성 코드의 시작점. 결국, CPU는 공격자가 심어놓은 코드를 실행.

작업 A: 오프셋 거리 찾기 (Return Address 위치 계산)
- 목표: 버퍼의 시작점부터 반환 주소(Return Address) 직전까지의 정확한 거리가 몇 바이트인지 계산.
- 이유: 이 거리를 알아야만 정확한 위치에 새로운 반환 주소를 덮어쓸 수 있다. 너무 적은 데이터를 보내면 반환 주소에 닿지 못하고 너무 많으면 엉뚱한 값을 덮어쓰게 되어 공격이 실패할 확률이 높다.
- 방법: 주로 GDB와 같은 디버거를 사용하여 취약한 함수의 스택 프레임을 분석하고 버퍼의 시작 주소와 반환 주소의 저장 위치 간의 차이를 계산하여 알아냄.
작업 B: 셸코드 배치 주소 찾기 (JUMP 할 목표 주소 계산)
- 목표: 덮어쓴 반환 주소에 어떤 값을 넣을지 결정하는 것. 이 값은 우리가 실행시키고 싶은 악성 코드(셸코드)가 위치할 메모리 주소.
- 이유: 함수가 종료될 때, CPU는 우리가 덮어쓴 이 주소로 점프하게 됨. 따라서 이 주소가 셸코드를 정확히 가리켜야 공격이 성공.
- 방법: 가장 간단한 방법은 버퍼의 시작 주소를 알아내 사용하는 것. 악성 코드를 버퍼에 채워 넣고 그 버퍼의 시작 주소로 점프하도록 만드는 것.
악성 페이로드(Payload)의 최종 구조
- NOP 슬레드 (NOP Sled)
- NOP (No-Operation)은 아무 동작도 하지 않고 다음 명령어로 넘어가는 기계어.
- 셸코드 앞에 수많은 NOP을 배치하여 미끄럼틀을 만듦.
- 이유: ASLR 같은 최신 보안 기법 때문에 정확한 셸코드 시작 주소를 예측하기 어려울 수 있다. 이때 NOP 슬레드 영역 안의 아무 곳으로만 점프시켜도, CPU는 NOP을 따라 미끄러지듯 내려가 결국 셸코드를 실행하게 됨. 이는 공격 성공률을 크게 높여주는 기법.
- 셸코드 (Shellcode)
- 실제 실행하려는 악성 코드 본체. NOP 슬레드 뒤에 위치시킴.
- 반환 주소 (Return Address)
- 작업 A에서 계산한 거리에 맞춰 정확한 위치에 삽입.
- 이 값은 작업 B에서 찾은 주소, 즉 NOP 슬레드 영역 안의 한 주소로 설정.

페이로드의 구성 요소
- 버퍼 시작 (주소: 0xbfffea8c)
- 이 주소는 Badfile의 내용이 프로그램의 buffer로 복사될 때 시작되는 메모리 위치.
- NOP 슬레드 (NOP Sled)
- 페이로드의 상당 부분을 차지하는 NOP 명령어들.
- 이 영역은 공격 성공률을 높이기 위한 방법.
- 조작된 반환 주소 (Return Address)
- 위치: 이 값은 버퍼 시작점으로부터 정확히 112바이트 떨어진 곳에 위치. 이 '112바이트'라는 거리는 GDB로 계산한 오프셋. 이 위치에 값을 써넣으면 원래의 반환 주소가 있던 자리를 정확히 덮어쓰게 됨.
- 값: 여기에 기록되는 주소 값은 NOP 슬레드 영역 내의 한 주소 (예: 0xbfffea8c 근처의 주소).
- 악성 코드 (Shellcode)
- 페이로드의 끝부분에 위치한 실제 공격 코드.
- NOP 슬레드를 타고 내려온 실행 흐름이 최종적으로 도달하여 실행되는 부분.
공격 실행 시나리오
- 프로그램이 이 Badfile을 읽어 버퍼에 복사.
- strcpy와 같은 함수가 오버플로우를 일으켜 Badfile의 내용이 스택을 덮어씀.
- 버퍼 시작부터 112바이트 지점에 있던 Return Address 값이 원래의 반환 주소를 대체.
- 함수가 종료되고 return할 때, CPU는 조작된 Return Address 값을 읽고 그 주소로 점프.
- 점프한 위치는 NOP 슬레드의 중간이며, CPU는 NOP을 따라 미끄러지듯 내려가 최종적으로 악성 코드를 실행.
- 공격이 성공하고 시스템의 제어권이 탈취.
5. Stack Buffer Overflow 방어 기법
| ASLR | 프로그램 실행 시마다 메모리 주소를 무작위로 변경하여 주소 예측을 어렵게 함. |
| Stack Guard (Canary) | 반환 주소 앞에 '카나리'라는 비밀 값을 삽입. 함수 종료 전 이 값이 변했는지 확인하여 오버플로우 탐지. |
| Non-Executable Stack (NX) | 스택 영역에서 코드가 실행되는 것을 하드웨어적으로 차단. |
| 안전한 함수 사용 | strcpy() 대신 길이를 제한하는 strncpy() 등을 사용. |
출처
- Wenliang Du, 시스템 & 어플리케이션 보안
'Pwnable' 카테고리의 다른 글
| [Pwnable] Dirty COW (0) | 2025.12.08 |
|---|---|
| [Pwnable] Race Condition (0) | 2025.12.08 |
| [Pwnable] ROP(Return-Oriented Programming) (0) | 2025.12.08 |
| [Pwnable] Format String (0) | 2025.12.08 |
| [Pwnable] RTL(Return-To-Libc) (0) | 2025.12.08 |