
CWE-787: Out-of-Bounds Write는 버퍼의 유효한 경계를 벗어나 데이터를 쓰는 취약점으로, C/C++ 계열 프로그램에서 자주 발생하며 메모리 손상(Memory Corruption), 프로그램 충돌(Crash), 원격 코드 실행(RCE)으로 이어질 수 있습니다. 이 취약점은 CWE Top 25에서 지속적으로 상위권을 유지하고 있으며, KEV(Known Exploited Vulnerabilities) 목록에 18개의 CVE가 등재되어 실제 공격에 활발히 악용되고 있습니다.
목차
- CWE-787 개요와 현황
- 취약점 발생 메커니즘과 유형
- 실제 공격 사례와 CVE 분석
- 탐지 및 분석 방법론
- 완화 전략과 방어 기법
- 개발 생명주기 통합 방안
- 결론 및 실무 가이드
CWE-787 개요와 현황
정의와 핵심 특징
CWE-787: Out-of-Bounds Write는 프로그램이 의도된 버퍼의 끝을 넘어서거나 시작 이전에 데이터를 쓰는 취약점입니다. 이는 CWE-119 (Improper Restriction of Operations within the Bounds of a Memory Buffer)의 하위 분류로, 메모리 안전성 위반의 대표적인 사례입니다.
현재 위험도와 트렌드
| 지표 | 수치 | 설명 |
|---|---|---|
| KEV CVEs | 18개 | CISA KEV 목록 등재 CVE 수 |
| CWE Top 25 순위 | #1 (2024) | 2022년 #3에서 지속 상승 |
| 익스플로잇 가능성 | High | 높은 공격 성공률 |
| 영향 범위 | Critical | RCE, 메모리 손상, 시스템 장악 |
주요 영향 플랫폼
- C/C++ 애플리케이션: 가장 빈번한 발생
- 시스템 소프트웨어: 커널, 드라이버, 펌웨어
- 웹 브라우저: JavaScript 엔진, 렌더링 엔진
- ICS/OT 시스템: 산업 제어 시스템
취약점 발생 메커니즘과 유형
주요 발생 원인
Out-of-Bounds Write는 다음과 같은 상황에서 발생합니다:
1. 배열 인덱스 검증 부족
// 취약한 코드 예시
void vulnerable_array_access(int index) {
int buffer[10];
// ❌ 인덱스 범위 검증 없음
buffer[index] = 42; // index가 0-9 범위를 벗어날 수 있음
}
// 안전한 코드
void safe_array_access(int index) {
int buffer[10];
// ✅ 경계 검사 수행
if (index >= 0 && index < 10) {
buffer[index] = 42;
} else {
// 에러 처리
handle_bounds_error(index);
}
}
2. 동적 버퍼 크기 계산 오류
// 취약한 코드
char* create_buffer(size_t count) {
// ❌ 정수 오버플로우 가능
char* buf = malloc(count * sizeof(char));
for (size_t i = 0; i <= count; i++) { // ❌ off-by-one 오류
buf[i] = 'A';
}
return buf;
}
// 안전한 코드
char* create_safe_buffer(size_t count) {
// ✅ 오버플로우 검사
if (count == 0 || count > SIZE_MAX / sizeof(char)) {
return NULL;
}
char* buf = malloc(count * sizeof(char));
if (!buf) return NULL;
// ✅ 올바른 범위
for (size_t i = 0; i < count; i++) {
buf[i] = 'A';
}
return buf;
}
3. 안전하지 않은 문자열 함수 사용
// 취약한 코드
void vulnerable_string_copy(char *input) {
char buffer[64];
// ❌ 입력 길이 검증 없음
strcpy(buffer, input); // input이 64자를 초과하면 오버플로우
}
// 안전한 코드
void safe_string_copy(const char *input) {
char buffer[64];
if (!input) return;
// ✅ 안전한 함수 사용
strncpy(buffer, input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0'; // NULL 종료 보장
}
메모리 레이아웃과 공격 벡터
스택 기반 버퍼 오버플로우
void stack_overflow_example() {
char buffer[256];
char *user_input = get_user_input();
// ❌ 스택 오버플로우 발생 가능
strcpy(buffer, user_input);
// 공격자가 return address를 덮어쓸 수 있음:
// [buffer][saved ebp][return addr] <- 스택 레이아웃
}
힙 기반 버퍼 오버플로우
typedef struct {
char data[100];
void (*function_ptr)(void);
} heap_struct;
void heap_overflow_example() {
heap_struct *obj = malloc(sizeof(heap_struct));
char *malicious_input = get_malicious_input();
// ❌ 힙 메타데이터나 인접 객체 손상 가능
strcpy(obj->data, malicious_input);
// function_ptr이 덮어써져 임의 코드 실행 가능
obj->function_ptr();
}
실제 공격 사례와 CVE 분석
주요 CVE 사례 분석
CVE-2022-22620: WebKit Use-After-Free / OOB Write
CVE_ID: CVE-2022-22620
Product: WebKit (Safari)
Impact: Remote Code Execution
Attack_Vector: Malicious Web Page
Root_Cause: JavaScript 엔진의 메모리 관리 오류
Exploitation:
- 악성 웹페이지 방문
- JavaScript 엔진 메모리 손상
- 샌드박스 탈출 및 임의 코드 실행
Mitigation:
- Apple 긴급 보안 업데이트
- 브라우저 샌드박스 강화
CVE-2021-21220: Chrome V8 Engine Heap Corruption
// 공격 시나리오 (의사 코드)
function exploit_v8_oob() {
// 1. 힙 스프레이로 메모리 레이아웃 조작
let spray_array = new Array(0x1000);
// 2. 타입 컨퓨전으로 경계 검사 우회
let confused_array = trigger_type_confusion();
// 3. Out-of-bounds write로 객체 메타데이터 손상
confused_array[0x100] = crafted_object;
// 4. 임의 코드 실행
execute_payload();
}
CVE-2020-17087: Windows Kernel Pool Overflow
// 커널 풀 오버플로우 시나리오
NTSTATUS vulnerable_kernel_function(PVOID user_buffer, ULONG size) {
// ❌ 정수 절단으로 작은 버퍼 할당
USHORT truncated_size = (USHORT)size; // CWE-197
PVOID kernel_buffer = ExAllocatePool(PagedPool, truncated_size); // CWE-131
if (kernel_buffer) {
// ❌ 원본 크기로 복사하여 오버플로우 발생
RtlCopyMemory(kernel_buffer, user_buffer, size); // CWE-787
}
return STATUS_SUCCESS;
}
공격 체인 분석
전형적인 Out-of-Bounds Write 공격 체인:
- 정보 수집: 메모리 레이아웃 파악
- 트리거: 취약한 함수 호출
- 익스플로잇: 메모리 손상을 통한 제어권 탈취
- 페이로드: 쉘코드 실행 또는 권한 상승
탐지 및 분석 방법론
정적 분석 도구
1. 컴파일 타임 검사
# GCC/Clang 정적 분석 옵션
gcc -Wall -Wextra -Warray-bounds -Wstringop-overflow \
-fstack-protector-strong -D_FORTIFY_SOURCE=2 \
vulnerable_code.c -o safe_binary
# Clang Static Analyzer
clang --analyze -Xanalyzer -analyzer-checker=security \
vulnerable_code.c
2. 고급 정적 분석 도구
| 도구 | 특징 | 적용 분야 |
|---|---|---|
| CodeQL | 의미론적 분석, 대규모 코드베이스 | 오픈소스, 엔터프라이즈 |
| Coverity | 상용 도구, 높은 정확도 | 미션크리티컬 시스템 |
| SonarQube | 개발 워크플로우 통합 | DevSecOps 파이프라인 |
| PC-lint Plus | C/C++ 전문, 임베디드 시스템 | 항공우주, 자동차 |
동적 분석 및 런타임 탐지
AddressSanitizer (ASan)
// ASan 활성화 컴파일
// gcc -fsanitize=address -g vulnerable_code.c
#include <stdio.h>
#include <string.h>
int main() {
char buffer[10];
// ASan이 다음 라인에서 오버플로우를 탐지
strcpy(buffer, "This string is longer than 10 characters!");
return 0;
}
// ASan 출력 예시:
// ==1234==ERROR: AddressSanitizer: stack-buffer-overflow
// WRITE of size 44 at 0x7fff12345678 thread T0
// #0 0x... in main vulnerable_code.c:8
Valgrind Memcheck
# Valgrind를 이용한 메모리 오류 탐지
valgrind --tool=memcheck --leak-check=full \
--show-leak-kinds=all ./vulnerable_program
# 출력 예시:
# ==1234== Invalid write of size 4
# ==1234== at 0x...: main (vulnerable_code.c:15)
# ==1234== Address 0x... is 0 bytes after a block of size 40 alloc'd
하드웨어 기반 보호 기법
// Intel CET (Control Flow Enforcement Technology)
#ifdef __CET__
#include <immintrin.h>
void protected_function() {
// 간접 분기 보호
_endbr64(); // ENDBR instruction for IBT
// 함수 로직
sensitive_operation();
// 리턴 주소 보호는 자동으로 처리됨 (Shadow Stack)
}
#endif
퍼징(Fuzzing) 기법
AFL++ 활용
# AFL++ 컴파일 및 실행
CC=afl-gcc ./configure
make
# 퍼징 실행
afl-fuzz -i input_corpus -o findings ./target_program @@
# 크래시 분석
afl-analyze -i findings/crashes/id:000000* ./target_program
libFuzzer 통합
// libFuzzer 타겟 함수
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size < 4) return 0;
// 퍼징 대상 함수 호출
parse_network_packet(data, size);
return 0;
}
// 컴파일: clang++ -fsanitize=fuzzer,address fuzzer_target.cc
완화 전략과 방어 기법
1. 언어 수준 보호
메모리 안전 언어 도입
// Rust - 컴파일 타임 메모리 안전성 보장
fn safe_buffer_access(data: &[u8], index: usize) -> Option<u8> {
// bounds checking이 자동으로 수행됨
data.get(index).copied() // panic 대신 Option 반환
}
// C++의 안전한 대안
#include <array>
#include <span>
void safe_cpp_access(std::span<const int> buffer, size_t index) {
// at() 함수는 경계 검사 수행
if (index < buffer.size()) {
int value = buffer[index]; // 안전한 접근
}
}
안전한 라이브러리 함수
// 안전한 문자열 처리 (Microsoft SafeStr)
#include <strsafe.h>
HRESULT safe_string_operations(LPCSTR source) {
CHAR destination[256];
// StringCchCopy는 버퍼 크기 검증
return StringCchCopy(destination,
ARRAYSIZE(destination),
source);
}
// POSIX 안전 함수들
void secure_memory_operations(const void* src, size_t src_size) {
char dest[100];
// 크기 제한 복사
if (src_size < sizeof(dest)) {
memcpy_s(dest, sizeof(dest), src, src_size);
}
}
2. 컴파일러 수준 보호
스택 카나리 (Stack Canaries)
// GCC/Clang 스택 보호 활성화
// -fstack-protector-strong 옵션
void protected_function(char* user_input) {
// 컴파일러가 자동으로 카나리 값 삽입
char local_buffer[256];
// 취약한 연산
strcpy(local_buffer, user_input);
// 함수 종료 시 카나리 값 검증
// 변조 시 __stack_chk_fail() 호출
}
FORTIFY_SOURCE
// 컴파일 시: -D_FORTIFY_SOURCE=2
#include <string.h>
void fortified_example(char* src) {
char dest[50];
// FORTIFY_SOURCE가 활성화되면
// strcpy가 __strcpy_chk로 대체되어 경계 검사 수행
strcpy(dest, src); // 런타임에 오버플로우 탐지
}
3. 운영체제 수준 보호
ASLR (Address Space Layout Randomization)
# Linux ASLR 설정
echo 2 > /proc/sys/kernel/randomize_va_space
# Windows ASLR 확인
dumpbin /headers /loadconfig program.exe | findstr "Dynamic base"
DEP/NX Bit (Data Execution Prevention)
// 페이지 권한 설정 예시
#include <sys/mman.h>
void setup_secure_memory() {
void* code_region = mmap(NULL, PAGE_SIZE,
PROT_READ | PROT_EXEC, // 실행 가능, 쓰기 불가
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
void* data_region = mmap(NULL, PAGE_SIZE,
PROT_READ | PROT_WRITE, // 쓰기 가능, 실행 불가
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
}
Control Flow Integrity (CFI)
// Clang CFI 활성화: -fsanitize=cfi
// 간접 호출 보호
typedef int (*function_ptr)(int);
int secure_indirect_call(function_ptr fp, int arg) {
// CFI가 함수 포인터 유효성 검증
return fp(arg); // 유효하지 않은 대상 시 중단
}
4. 하드웨어 기반 보호
Intel CET (Control-flow Enforcement Technology)
; Intel CET - Indirect Branch Tracking
endbr64 ; 유효한 간접 분기 대상 마킹
; Shadow Stack - 리턴 주소 보호
call function
; 리턴 주소가 shadow stack에도 저장됨
ret ; shadow stack과 일반 스택 비교
ARM Pointer Authentication
// ARM64 포인터 인증
#ifdef __ARM_FEATURE_PAC_DEFAULT
void* authenticated_return_address() {
// 리턴 주소에 인증 코드 추가
return __builtin_return_address(0);
}
#endif
개발 생명주기 통합 방안
Secure Development Lifecycle (SDL) 통합
1. 요구사항 단계
security_requirements:
memory_safety:
- "모든 버퍼 접근에 경계 검사 필수"
- "동적 메모리 할당 시 정수 오버플로우 검증"
- "외부 입력에 대한 길이 제한 적용"
tool_integration:
- "정적 분석 도구 의무 사용"
- "AddressSanitizer 빌드 테스트"
- "퍼징 테스트 케이스 작성"
2. 설계 단계
// 안전한 API 설계 원칙
typedef struct {
size_t capacity;
size_t length;
char data[];
} safe_string_t;
// 경계 검사가 내장된 API
int safe_string_append(safe_string_t* str, const char* src, size_t src_len) {
if (!str || !src) return -1;
// 용량 검사
if (str->length + src_len >= str->capacity) {
return -1; // 버퍼 오버플로우 방지
}
memcpy(str->data + str->length, src, src_len);
str->length += src_len;
str->data[str->length] = '\0';
return 0;
}
3. 구현 단계
# Makefile 보안 플래그 설정
SECURITY_CFLAGS = -Wall -Wextra -Werror \
-fstack-protector-strong \
-D_FORTIFY_SOURCE=2 \
-fPIE -pie \
-Wl,-z,relro,-z,now
DEBUG_CFLAGS = -fsanitize=address,undefined \
-fno-omit-frame-pointer \
-g3
secure_build:
$(CC) $(SECURITY_CFLAGS) $(DEBUG_CFLAGS) -o secure_app *.c
4. 테스트 단계
# 자동화된 보안 테스트
import subprocess
import os
def run_security_tests():
test_cases = [
"run_static_analysis",
"run_asan_tests",
"run_fuzzing_suite",
"run_valgrind_checks"
]
for test in test_cases:
result = subprocess.run([test], capture_output=True)
if result.returncode != 0:
raise Exception(f"Security test {test} failed")
def run_asan_tests():
env = os.environ.copy()
env['ASAN_OPTIONS'] = 'detect_odr_violation=1:abort_on_error=1'
return subprocess.run(['./test_suite'], env=env)
CI/CD 파이프라인 통합
GitHub Actions 예시
name: Security Analysis Pipeline
on: [push, pull_request]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Static Analysis
run: |
clang --analyze --analyzer-output text *.c
cppcheck --enable=all --error-exitcode=1 *.c
- name: AddressSanitizer Build
run: |
gcc -fsanitize=address -g -o test_asan test_suite.c
./test_asan
- name: Fuzzing Test
run: |
clang -fsanitize=fuzzer,address -o fuzzer fuzzer_target.c
timeout 300 ./fuzzer corpus/ || true
- name: CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
languages: cpp
queries: security-and-quality
결론
CWE-787: Out-of-Bounds Write는 현재 가장 위험하고 빈번하게 발생하는 취약점 중 하나로, 체계적인 접근이 필요합니다. 메모리 안전성 확보를 위해서는 기술적 완화 방안과 프로세스 개선을 동시에 추진해야 합니다.
핵심 권장사항
- 언어 선택: 가능한 메모리 안전 언어(Rust, Go, Java) 도입 검토
- 도구 활용: 정적/동적 분석 도구를 개발 워크플로우에 필수 통합
- 컴파일러 보호: 모든 보호 기법(Stack Canaries, ASLR, CFI) 활성화
- 교육 강화: 개발자 대상 메모리 안전성 교육 정기 실시
- 지속적 모니터링: 런타임 보호와 모니터링 시스템 구축
앞으로는 "메모리 안전성(Memory Safety)"이 보안 코딩의 핵심 지표가 될 것입니다. C/C++ 프로젝트에서는 정적 분석과 하드웨어 보호 기법을 반드시 병행하여 Defense in Depth 전략을 구현해야 합니다.
실무 체크리스트
개발 단계별 점검사항
코딩 단계
- 모든 배열 접근에 경계 검사 구현
- 안전한 문자열 함수 사용 (strncpy, snprintf 등)
- 동적 메모리 할당 시 정수 오버플로우 검증
- 입력 데이터 길이 제한 및 검증
- 포인터 역참조 전 NULL 검사
빌드 단계
- 컴파일러 보안 플래그 활성화 (-fstack-protector-strong, -D_FORTIFY_SOURCE=2)
- AddressSanitizer 빌드 테스트 수행
- 정적 분석 도구 통과 확인
- 링커 보안 옵션 적용 (-z relro, -z now, -fPIE)
테스트 단계
- 퍼징 테스트 케이스 작성 및 실행
- Valgrind 메모리 검사 통과
- 경계값 테스트 케이스 포함
- 부하 테스트에서 메모리 누수 확인
배포 단계
- ASLR 활성화 확인
- DEP/NX bit 설정 검증
- 런타임 보호 모니터링 구성
- 보안 패치 적용 프로세스 수립
긴급 대응 가이드
취약점 발견 시
- 즉시 조치: 영향 받는 시스템 격리 및 패치 적용
- 영향 분석: 데이터 유출 여부 및 피해 범위 조사
- 근본 원인 분석: 코드 리뷰 및 프로세스 개선점 도출
- 재발 방지: 유사 패턴 전수 조사 및 자동 탐지 규칙 추가
참고 자료
'Vulnerability' 카테고리의 다른 글
| [Vulnerability] CWE-79: 크로스사이트 스크립팅(XSS) — 웹 페이지 입력값 검증 실패로 인한 스크립트 삽입 공격 (0) | 2025.11.10 |
|---|