버퍼 오버플로우 기초: 바이너리 익스플로잇의 첫걸음
버퍼 오버플로우는 소프트웨어 보안에서 가장 기본적인 취약점 중 하나로, 리버스 엔지니어링, CTF, 침투 테스트에 관심 있는 누구에게나 필수 지식이다. 수십 년 전부터 알려진 취약점이지만 실제 애플리케이션에서 여전히 발견되기 때문에 반드시 익혀야 할 스킬이다.
버퍼 오버플로우란?
버퍼 오버플로우는 프로그램이 버퍼가 수용할 수 있는 것보다 많은 데이터를 쓸 때 발생한다. 자동 경계 검사가 없는 C/C++ 같은 언어에서는 이 초과 데이터가 인접 메모리 위치를 덮어쓸 수 있어, 데이터 손상이나 프로그램 충돌, 심하면 공격자가 임의 코드를 실행하는 것도 가능해진다.
스택 기반 버퍼를 이용한 고전적인 예시:
#include <stdio.h>
#include <string.h>
void vulnerable_function(char *input) {
char buffer[64];
strcpy(buffer, input); // 경계 검사 없음!
printf("입력값: %s\n", buffer);
}
int main(int argc, char **argv) {
if (argc > 1) {
vulnerable_function(argv[1]);
}
return 0;
}
이 프로그램에 64바이트 이상을 전달하면 버퍼가 넘쳐 스택의 메모리를 덮어쓰게 된다.
스택 레이아웃
스택을 이해하는 것이 핵심이다. 함수가 호출될 때 스택은 대략 이렇게 생겼다 (아래로 증가하는 단순화된 구조):
[지역 변수]
[저장된 프레임 포인터 (EBP/RBP)]
[리턴 주소]
[함수 인자]
지역 버퍼를 오버플로우하면 저장된 리턴 주소를 덮어쓸 수 있다. 이게 바로 황금 티켓이다 — 함수 실행 완료 후 프로그램이 리턴할 위치를 제어하면 실행 흐름을 탈취할 수 있다.
간단한 익스플로잇
secret()이라는 함수가 있는데 프로그램이 정상적으로는 절대 호출하지 않는다고 가정해 보자. 공격은 이렇게 이루어진다:
-
오프셋 찾기: 리턴 주소까지 몇 바이트인가? 패턴 생성 도구나 시행착오를 통해 찾는다.
-
타깃 주소 찾기: 메모리에서
secret()의 위치는?objdump,gdb,radare2같은 도구를 사용한다:objdump -d vulnerable | grep secret -
페이로드 제작: 버퍼를 쓰레기 값으로 채우고 리턴 주소를 타깃 주소로 덮어쓴다.
-
엔디안 고려: 대부분의 시스템은 리틀 엔디안을 사용하므로
0x08048456주소는\x56\x84\x04\x08이 된다.
현대의 보호 기법
실제 익스플로잇은 보안 메커니즘 때문에 이 단순한 예시보다 훨씬 어렵다:
ASLR (주소 공간 레이아웃 랜덤화): 메모리 주소를 무작위화하여 점프할 위치를 알기 어렵게 만든다. 우회 기법으로는 정보 누출과 return-to-PLT 공격이 있다.
스택 카나리: 버퍼와 리턴 주소 사이에 특수 값을 배치한다. 이 값이 변경되면 프로그램이 중단된다. 일부 시나리오에서는 누출이나 브루트포스로 우회 가능하다.
NX/DEP (실행 불가): 스택을 실행 불가로 표시해 셸코드 실행을 방지한다. Return-Oriented Programming (ROP)으로 기존 코드를 체이닝하여 우회한다.
PIE (위치 독립 실행 파일): 실행 파일 자체의 베이스 주소를 무작위화하여 ASLR을 보완한다.
CTF Pwn 시작하기
안전하고 합법적으로 바이너리 익스플로잇을 연습하려면 CTF 문제가 최고다:
필수 도구:
- GDB with pwndbg/GEF: 익스플로잇 개발용 강화 디버거
- pwntools: 익스플로잇 작성용 Python 라이브러리
- ROPgadget: 바이너리에서 ROP 가젯 탐색
- checksec: 활성화된 보안 기능 확인
연습 플랫폼:
- pwnable.kr
- exploit.education
- picoCTF
- HackTheBox
기본 워크플로우:
file,checksec, 디스어셈블러로 바이너리 분석- 정적/동적 분석으로 취약점 찾기
- 오프셋 계산하고 페이로드 제작
- 로컬에서 테스트 후 원격 익스플로잇
첫 익스플로잇 작성
pwntools를 활용한 최소한의 예시:
from pwn import *
# 타깃 연결
p = process('./vulnerable')
# 페이로드 제작
offset = 72
target_addr = p32(0x08048456) # secret() 주소
payload = b"A" * offset + target_addr
# 전송 및 인터랙션
p.sendline(payload)
p.interactive()
책임 있는 공개
항상 권한이 있는 타깃에서만 연습하라. 허가 없이 실제 시스템을 익스플로잇하지 마라. CTF 플랫폼, 개인 VM, 명확한 범위가 있는 버그 바운티 프로그램이 합법적인 연습 공간이다.
마치며
버퍼 오버플로우는 바이너리 익스플로잇이라는 매혹적인 세계로 들어가는 관문이다. 현대의 보호 기법이 난이도를 크게 높였지만 핵심 개념은 여전히 유효하다. 간단한 문제에서 기초를 마스터하고, 우회 기법을 배우면서 점점 보호가 강화된 바이너리에 도전해 나가자.
스택 기반 오버플로우부터 시작해서 기초를 깊이 이해하면, 힙 오버플로우, 포맷 스트링 공격, ROP 체인 같은 고급 기법의 탄탄한 기반이 된다.
해피 해킹, 그리고 기억하라: 강한 힘에는 큰 책임이 따른다.