security
CHAPTER 91 / 96
읽기 약 2분
FUNCTION
버퍼 오버플로우: 왜 발생하는가
핵심 개념
⚠️ 교육 목적. strcpy의 위험 + 스택 버퍼 오버플로우 원리 + 리턴 주소 덮어쓰기 개념. 안전한 코드 비교.
본문
버퍼 오버플로우의 원리
스택 메모리 (높은 주소 → 낮은 주소):
[ saved %rbp ]
[ return addr ] ← 함수 종료 시 점프할 주소
[ buffer[64] ] ← 사용자 입력 저장
[ ]
[ ↑ 입력 방향 ] ← 작성하다 보면 위로 진행
[ ]
⚠️ 입력 65바이트면 saved %rbp 덮어씀
⚠️ 입력 73바이트면 return addr 덮어씀
→ 임의 코드 실행 가능 (전형적인 익스플로잇)취약한 코드
// ⚠️ 교육 목적. 허가된 환경에서만 실행하세요.
#include <stdio.h>
#include <string.h>
void vulnerable(const char *input) {
char buffer[64];
strcpy(buffer, input); // ⚠️ 경계 검사 없음
printf("입력: %s\n", buffer);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("사용: %s <input>\n", argv[0]);
return 1;
}
vulnerable(argv[1]);
return 0;
}
// 실행:
// $ ./vuln "Hello" → 정상
// $ ./vuln "$(python3 -c 'print("A"*100)')"
// → Stack smashing detected (-fstack-protector 활성 시)
// → 또는 Segfault안전한 코드 — 5가지 패턴
#include <stdio.h>
#include <string.h>
// 패턴 1: snprintf (권장)
void safe_v1(const char *input) {
char buffer[64];
snprintf(buffer, sizeof(buffer), "%s", input);
// 자동 잘림 + \0 종료
printf("%s\n", buffer);
}
// 패턴 2: strncpy + 명시적 \0
void safe_v2(const char *input) {
char buffer[64];
strncpy(buffer, input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
printf("%s\n", buffer);
}
// 패턴 3: 사전 길이 검사
void safe_v3(const char *input) {
char buffer[64];
size_t len = strlen(input);
if (len >= sizeof(buffer)) {
fprintf(stderr, "입력이 너무 깁니다 (최대 %zu)\n",
sizeof(buffer) - 1);
return;
}
strcpy(buffer, input); // 이제 안전
printf("%s\n", buffer);
}
// 패턴 4: fgets — 줄바꿈 포함 여부 처리
void safe_v4(void) {
char buffer[64];
if (fgets(buffer, sizeof(buffer), stdin) == NULL) return;
// \n 제거
buffer[strcspn(buffer, "\n")] = '\0';
printf("%s\n", buffer);
}
// 패턴 5: 동적 할당
char *safe_v5(const char *input) {
size_t len = strlen(input);
char *buf = malloc(len + 1);
if (!buf) return NULL;
memcpy(buf, input, len);
buf[len] = '\0';
return buf; // 호출자가 free 책임
}위험 함수 vs 안전 함수
| 위험 | 안전 |
|---|---|
| strcpy(d, s) | strncpy / snprintf |
| strcat(d, s) | strncat / snprintf |
| sprintf(d, ...) | snprintf |
| gets(d) | fgets |
| scanf("%s", d) | scanf("%63s", d) (크기 제한) |
| memcpy(d, s, len) | 사전 d 크기 검증 |-fstack-protector 효과
// 컴파일러가 자동 삽입하는 카나리 (canary)
// 함수 진입: 스택에 비밀 값 저장
// 함수 종료: 비밀 값 검사 → 변조 시 abort
// 컴파일
// gcc -fstack-protector-strong vuln.c -o vuln
// (모던 gcc는 기본 활성화)
// 오버플로우 시도 시:
// *** stack smashing detected ***
// terminated
// Aborted (core dumped)ASan으로 정확한 진단
gcc -fsanitize=address -g -O0 vuln.c -o vuln
./vuln "$(python3 -c 'print("A"*100)')"
# 출력:
# ==12345== ERROR: AddressSanitizer: stack-buffer-overflow
# WRITE of size 100 at 0x7fff... thread T0
# #0 0x... in vulnerable vuln.c:7
# #1 0x... in main vuln.c:14
#
# Address 0x7fff... is located in stack of thread T0
# at offset 100 in frame
# #0 0x... in vulnerable vuln.c:5실전 — 강건한 입력 처리
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAX_INPUT 1024
int read_safe_input(char *buf, size_t size) {
if (size == 0) return -1;
if (fgets(buf, (int)size, stdin) == NULL) return -1;
size_t len = strlen(buf);
if (len > 0 && buf[len-1] == '\n') {
buf[len-1] = '\0';
} else {
// 입력이 너무 김 — stdin 비우기
int c;
while ((c = getchar()) != '\n' && c != EOF) {}
return -1;
}
return 0;
}
int main(void) {
char input[MAX_INPUT];
if (read_safe_input(input, sizeof(input)) != 0) {
fprintf(stderr, "입력 오류\n");
return 1;
}
printf("입력: %s\n", input);
return 0;
}다음 챕터
CH.8 "포맷 스트링 취약점" — printf 잘못 쓰면 어떤 일이.
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 C 코드의 모든 strcpy/sprintf/gets/scanf 위치를 찾고 안전한 함수로 변환해줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
OWASP Buffer Overflow 분류와 실제 CVE 사례 5개를 코드 비교로 분석해줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 C 프로젝트에 fortify_source/stack-protector 적용 + ASan/MSan 통합 가이드를 만들어줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 메모리 안전 언어(Rust/Zig) 채택률과 C/C++ 점유율 변화를 솔직히 알려줘.
⭐ 이것만 기억하세요
버퍼 오버플로우: 왜 발생하는가는 이 3가지만 확실히 잡으세요
1.버퍼 오버플로우는 입력이 버퍼 크기를 넘어 인접 메모리를 덮어쓰는 현상
2.리턴 주소 덮어쓰기로 임의 코드 실행 가능 — 모든 strcpy/strcat/sprintf/gets 제거
3.다음 챕터 CH.8에서 포맷 스트링 — printf의 의외의 위험
공유하기
진행도 91 / 96