본문 바로가기
카테고리 없음

C언어 보안코딩 (버퍼 오버플로우, 입력검증, 방지전략)

by info95686 2025. 10. 4.

C언어는 성능과 제어력이 뛰어나지만 메모리를 직접 다루는 만큼 보안 취약점에 노출되기 쉽습니다. 대표적인 문제가 버퍼 오버플로우로, 작은 실수에서 치명적 보안 사고로 이어질 수 있습니다. 이번 글에서는 버퍼 오버플로우의 원인과 실제 예제, 입력 검증 및 안전한 함수 사용법, 그리고 실무에서 적용할 수 있는 방지 전략과 보안 코딩 습관을 정리합니다.

버퍼 오버플로우의 원인과 실제 예제

버퍼 오버플로우는 정해진 크기의 메모리 버퍼에 허용된 크기보다 많은 데이터를 기록하려 할 때 발생합니다. 공격자가 악의적으로 긴 입력을 넣으면 스택이나 힙의 인접 메모리가 덮어써져 프로그램 동작을 변조하거나 임의 코드를 실행할 수 있습니다. 주된 원인은 다음과 같습니다.

  • 입력 길이 미검증: 사용자 입력의 길이를 제한하지 않고 고정 크기 배열에 그대로 저장하는 경우
  • 안전하지 않은 표준 함수 사용: gets(), scanf("%s"), strcpy(), strcat() 등 길이 제한이 없는 함수
  • 널('\0') 종료 누락: 문자열 처리 시 끝나는 문자를 못 넣어 경계를 벗어나는 경우
  • 잘못된 크기 계산: 멀티바이트 문자, 인코딩 차이, 포맷 문자열 처리 시 크기를 잘못 계산하는 실수

간단한 취약 코드 예시는 다음과 같습니다.

#include <stdio.h>

int main() {
    char buffer[10];
    printf("이름을 입력하세요: ");
    gets(buffer); // 안전하지 않은 함수: 입력 길이 제한 없음
    printf("입력된 이름: %s\n", buffer);
    return 0;
}

위 코드는 사용자가 10자 이상의 문자열을 입력하면 버퍼를 넘쳐 인접 메모리가 덮어써집니다. 이로 인해 프로그램 충돌이나 잠재적 권한 탈취로 이어질 수 있습니다. 과거 많은 보안 사고가 이러한 취약점에서 발생했으므로 주의가 필요합니다.

입력 검증과 안전한 함수 사용

버퍼 오버플로우 예방의 핵심은 '입력 검증'과 '안전한 함수 사용'입니다. 안전하지 않은 함수는 가능한 대체 함수로 바꾸고, 동적 메모리 사용 시에도 항상 길이를 확인해야 합니다.

안전하지 않은 함수와 권장 대체

  • gets()fgets(): fgets는 버퍼 크기를 인자로 받아 초과 입력을 방지합니다.
  • strcpy()strncpy() 또는 strlcpy() (환경에 따라): 복사 길이를 제한합니다. 다만 strncpy는 널 종료를 보장하지 않으므로 사용 후 명시적 널 추가 필요.
  • strcat()strncat(): 붙이기 할 때 남은 공간을 고려합니다.
  • sprintf()snprintf(): 출력 길이를 제한하여 버퍼 초과 방지.

예시: fgets 사용

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

int main() {
    char buffer[10];
    printf("이름을 입력하세요: ");
    if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
        buffer[strcspn(buffer, "\n")] = '\0'; // 개행 제거
        printf("입력된 이름: %s\n", buffer);
    }
    return 0;
}

동적 메모리와 snprintf 활용

동적 할당을 사용할 때는 필요한 크기를 정확히 계산하고, snprintf로 실제 기록된 길이를 확인하면 안전성이 높아집니다.

size_t needed = snprintf(NULL, 0, "%s %s", first, last) + 1;
char *buf = malloc(needed);
if (buf) {
    snprintf(buf, needed, "%s %s", first, last);
}

이 방식은 포맷팅 결과의 길이를 미리 계산해 정확한 크기를 할당하므로 오버플로우 위험을 줄입니다.

방지 전략과 보안 코딩 습관

버퍼 오버플로우를 예방하려면 단순히 안전한 함수 몇 개를 사용하는 것을 넘어서, 프로젝트 차원의 규칙과 도구, 테스트를 체계적으로 적용해야 합니다.

코딩 규칙과 코드 리뷰

  • 프로젝트 규칙으로 "gets(), scanf("%s"), strcpy(), strcat() 금지"를 명시합니다.
  • 코드 리뷰 시 입력 처리와 버퍼 크기 계산을 반드시 확인하도록 체크리스트를 둡니다.
  • 매직 넘버 대신 상수(#define 또는 const)를 사용해 버퍼 크기 의도를 명확히 합니다.

컴파일러와 런타임 보호 기법

  • 스택 프로텍터: gcc -fstack-protector 또는 -fstack-protector-all로 스택 가드(칸ary)를 활성화해 스택 버퍼 오버플로우를 탐지합니다.
  • 주소 공간 배치 난수화(ASLR): 운영체제 차원에서 메모리 주소를 랜덤화해 익스플로잇 난이도를 높입니다.
  • DEP/NX: 데이터 영역에서 코드 실행을 방지하는 하드웨어/OS 기능을 활용합니다.

테스트와 퍼징(Fuzzing)

예상치 못한 입력에 어떻게 반응하는지 테스트하는 것이 중요합니다. 퍼저(fuzzer)를 사용하면 무작위 혹은 특수하게 조작된 입력을 대량으로 주어 취약점을 쉽게 발견할 수 있습니다. AFL, libFuzzer 같은 도구를 CI에 통합하면 자동으로 잠재적 문제를 찾아낼 수 있습니다.

정적/동적 분석 도구 활용

  • 정적 분석: clang-tidy, Coverity, Cppcheck 등으로 버퍼 경계, 포인터 오류 등 잠재적 문제를 사전에 탐지합니다.
  • 동적 분석: AddressSanitizer(-fsanitize=address), Valgrind 등으로 런타임 오류를 탐지합니다.

개발자 교육과 문화

보안 코딩은 도구만으로 해결되지 않습니다. 모든 개발자가 취약점의 심각성을 이해하고 안전한 입력 처리, 포맷 문자열 취약점(printf 계열) 회피, 적절한 에러 처리 습관을 갖추도록 교육해야 합니다. 코드가 "돌아가면 된다"가 아니라 "안전하게 동작해야 한다"는 철학을 팀 문화로 정착시키는 것이 핵심입니다.

결론: 안전한 C 코딩은 습관과 과정의 문제

버퍼 오버플로우는 오래된 문제지만 여전히 많은 시스템에서 위험을 초래합니다. 단기적으로는 fgets, strncpy, snprintf 같은 안전한 API를 사용하고, 장기적으로는 다음을 실천해야 합니다:

  • 입력 값을 항상 검증하고, 길이와 범위를 체크한다.
  • 안전하지 않은 표준 함수를 금지하고 대체 API를 사용한다.
  • 정적/동적 분석 도구와 퍼징을 빌드 파이프라인에 포함한다.
  • 컴파일러/OS의 보호 기능을 활성화하여 추가 방어층을 만든다.
  • 팀 차원의 보안 규칙과 교육을 통해 안전한 코딩 문화를 정착시킨다.

결국 안전한 C언어 코딩은 일회성 노력이 아니라, 설계·코딩·테스트·배포 전 과정에 걸친 지속적인 습관과 체계의 문제입니다. 작은 입력 검증 하나가 큰 사고를 막을 수 있음을 잊지 마세요.