1.8 허용된 범위 내 메모리 접근
개요
소프트웨어가 데이터를 처리하기 위해 사용하는 메모리 버퍼(일정 크기의 메모리 공간)에 접근할 때, 할당된 범위를 벗어나서 데이터를 읽거나 쓰려고 시도하면 심각한 보안 약점이 발생할 수 있습니다. 가장 대표적인 것이 버퍼 오버플로우(Buffer Overflow)입니다.
버퍼 오버플로우는 주로 C/C++와 같이 메모리를 직접 관리하는 언어에서 발생하며, 입력 데이터의 길이를 제대로 검증하지 않고 고정된 크기의 버퍼에 복사할 때 발생합니다. 공격자는 버퍼의 경계를 넘어 인접한 메모리 영역(예: 반환 주소, 함수 포인터)을 덮어써서 프로그램의 실행 흐름을 변경하고, 최종적으로 임의의 코드(악성 코드)를 실행시킬 수 있습니다.
이 기준은 메모리 버퍼 접근 시 경계를 검사하고, 안전하지 않은 함수 사용을 지양하며, 메모리 관련 오류를 방지하기 위한 보안 요구사항을 정의합니다.
보안 대책
- - 경계 검사(Boundary Checking): 메모리 버퍼에 데이터를 쓰거나 읽기 전에, 접근하려는 인덱스나 길이가 버퍼의 할당된 크기 범위 내에 있는지 항상 확인합니다.
- - 입력값 길이 제한 및 검증: 버퍼에 저장될 외부 입력값의 최대 길이를 제한하고, 입력값이 이 길이를 초과하지 않는지 서버 측에서 엄격하게 검증합니다.
- - 안전한 함수 사용: 버퍼 오버플로우에 취약한 C/C++ 표준 라이브러리 함수(예: `strcpy`, `strcat`, `sprintf`, `gets`) 대신, 버퍼의 크기를 인자로 받아 경계를 검사하는 안전한 함수(예: `strncpy`, `strncat`, `snprintf`, `fgets`)를 사용합니다. 또는 C++의 `std::string`, Java의 `String` / `StringBuilder`, Python의 문자열 타입 등 메모리 관리가 자동화된 언어 기능을 활용합니다.
- - 메모리 안전 언어 사용: 가능하다면 Java, Python, C#, Rust 등 메모리 접근 오류를 컴파일 시점이나 런타임에 방지해주는 언어를 사용합니다.
- - 컴파일러 보안 옵션 활용: 컴파일러가 제공하는 스택 보호 기능(예: Stack Canaries/Stack Guard), 주소 공간 레이아웃 랜덤화(ASLR), 데이터 실행 방지(DEP/NX) 등의 보안 옵션을 활성화하여 버퍼 오버플로우 공격의 성공 가능성을 낮춥니다.
- - 정적/동적 분석 도구 사용: 코드 작성 단계에서 버퍼 오버플로우 가능성을 탐지하는 정적 분석 도구(SAST)나, 실행 중 메모리 오류를 탐지하는 동적 분석 도구(DAST, Fuzzing)를 활용합니다.
코드 예시 (C/C++)
취약한 코드 (strcpy 사용):
#include <string.h>
#include <stdio.h>
void vulnerable_function(char *input) {
char buffer[10]; // 10바이트 크기의 버퍼
strcpy(buffer, input); // 입력값 길이 검사 없이 복사 -> 버퍼 오버플로우 발생 가능
printf("Input: %s\n", buffer);
}
int main(int argc, char *argv[]) {
if (argc > 1) {
vulnerable_function(argv[1]); // 긴 인자를 주면 오버플로우 발생
}
return 0;
}
#include <string.h>
#include <stdio.h>
#define BUFFER_SIZE 10
void safe_function(char *input) {
char buffer[BUFFER_SIZE];
// 입력값 길이 검증 (NULL 종료 문자 포함)
if (strlen(input) >= BUFFER_SIZE) {
printf("Error: Input is too long.\n");
return;
}
// strncpy 사용: 복사할 최대 길이를 지정
strncpy(buffer, input, BUFFER_SIZE - 1);
buffer[BUFFER_SIZE - 1] = '\0'; // 항상 NULL 문자로 종료 보장
printf("Input: %s\n", buffer);
}
int main(int argc, char *argv[]) {
if (argc > 1) {
safe_function(argv[1]); // 오버플로우 방지됨
}
return 0;
}
코드 예시 (Java - 배열 경계 검사)
Java는 기본적으로 배열 접근 시 경계 검사를 수행하여 버퍼 오버플로우를 방지하지만, 로직 오류로 인해 `ArrayIndexOutOfBoundsException`이 발생할 수 있습니다.
안전한 코드 (경계 확인):
int[] dataArray = new int[10];
int index = getIndexFromUserInput(); // 사용자로부터 인덱스 입력 받음
// 배열에 접근하기 전에 인덱스 유효성 검사
if (index >= 0 && index < dataArray.length) {
int value = dataArray[index]; // 안전한 접근
System.out.println("Value at index " + index + ": " + value);
} else {
System.err.println("Error: Index out of bounds."); // 오류 처리
}
Python, C#, JavaScript 등 다른 메모리 안전 언어에서도 유사하게 배열/리스트 접근 시 인덱스 범위를 확인하는 것이 중요합니다.