포맷스트링 공격과 C/C++ 포맷 함수 보안

포맷스트링 공격의 발생 원리와 메모리 노출·변조 위험, C/C++ 코드에서의 탐지와 예방 방법을 정리한다.

2026-08-14 · 최초 발행 2025-05-26

사용자 입력이 포맷 문자열이 될 때 생기는 문제

포맷스트링 공격(Format String Attack)은 검증되지 않은 사용자 입력을 포맷 문자열로 직접 처리할 때 발생하는 취약점이다. C/C++의 printf(), sprintf(), fprintf() 같은 함수에서 주로 나타나며, 메모리 내용 노출부터 임의 코드 실행까지 이어질 수 있다.

포맷 문자열은 출력 데이터의 형식을 지정하는 문자열이며, %로 시작하는 지시자를 포함한다.

printf("정수 값: %d, 문자열: %s", 10, "Hello");

여기서 %d%s는 각각 정수와 문자열 출력을 지정한다. 자주 쓰이는 지시자는 다음과 같다.

  • %d: 부호 있는 10진 정수
  • %u: 부호 없는 10진 정수
  • %x: 16진수(소문자)
  • %X: 16진수(대문자)
  • %s: 문자열
  • %n: 지금까지 출력한 문자 수 저장
  • %p: 포인터 주소

안전한 호출과 취약한 호출의 차이

문제는 사용자 입력 자체를 첫 번째 인자, 즉 포맷 문자열 위치에 넘기는 경우다.

// 안전한 코드
printf("%s", user_input);

// 취약한 코드
printf(user_input);

취약한 호출에서는 입력에 %s, %x, %n 등이 들어갈 경우 프로그램이 이를 출력 형식 지시자로 해석한다. 그 결과 의도하지 않은 메모리 참조나 조작이 가능해진다.

메모리 노출부터 프로그램 흐름 제어까지

포맷스트링 취약점은 단순한 출력 오류에 그치지 않는다. %x%p로 스택 값을 읽어 중요한 정보를 노출할 수 있고, %s로 특정 메모리 주소를 문자열처럼 해석해 읽을 수 있다.

%n은 지금까지 출력한 문자 수를 특정 메모리 주소에 기록한다. 이를 악용하면 메모리 값을 변경할 수 있으며, GOT나 함수 포인터처럼 중요한 영역을 덮어써 프로그램 흐름을 제어하고 임의 코드 실행으로 연결할 수 있다.

메모리 읽기와 쓰기 시나리오

다음 함수는 사용자 입력을 그대로 포맷 문자열로 사용한다.

void vulnerable_function(char *user_input) {
    printf(user_input);
}

공격자가 "%x %x %x %x"를 입력하면 스택에 저장된 4개의 값이 16진수로 출력된다. 이를 통해 스택 메모리의 내용을 확인할 수 있다.

실제 출력 예: bffff754 8048473 b7fd8304 0

%n은 메모리 쓰기에도 사용될 수 있다.

// 취약한 코드
int value = 0;
printf(user_input);
printf("value = %d\n", value);

공격자가 %08x%08x%08x%n과 같은 입력을 제공하면 스택에서 value 변수의 주소를 찾아 %n 지시자로 값을 변경할 수 있다. 중요 변수의 값도 같은 방식으로 조작될 수 있다.

코드와 실행 환경에서 취약점을 찾는 방법

정적 분석 도구인 Flawfinder, RATS, Coverity는 소스 코드에서 취약한 포맷 함수 사용 패턴을 검사하는 데 활용할 수 있다. 퍼징(Fuzzing)으로 다양한 포맷 문자열 입력을 전달해 동작을 확인하는 방법도 있다.

코드 리뷰에서는 사용자 입력이 포맷 문자열 위치에 직접 전달되는 호출을 찾아야 한다. GCC에서는 -Wformat=2 옵션으로 잠재적인 포맷스트링 취약점 관련 경고를 확인할 수 있다.

포맷 문자열을 고정하고 입력을 분리하기

예방의 핵심은 포맷 문자열을 고정하고 사용자 입력을 별도 인자로 전달하는 것이다.

포맷스트링 취약점 예방고정 포맷 문자열 사용사용자 입력 검증보안 라이브러리 사용컴파일러 보안 옵션 활성화printf('%s', user_input) 형태사용입력에 포맷 지시자 존재 여부검사strcpy 대신 strncpy 사용-Wformat=2,-D_FORTIFY_SOURCE=2 옵션적용

가장 먼저 포맷 문자열을 명시하고, 사용자 입력은 값으로만 넘긴다.

// 취약한 코드
printf(user_input);

// 안전한 코드
printf("%s", user_input);

입력에 포맷 지시자가 포함됐는지 검사하고 필터링할 수도 있다.

bool is_safe_input(const char *input) {
    return strchr(input, '%') == NULL;
}

GCC에서는 다음과 같이 보안 옵션을 활성화할 수 있다.

gcc -Wformat -Wformat-security -D_FORTIFY_SOURCE=2 program.c

포맷 문자열을 직접 다루는 API에서도 고정된 형식을 사용해야 한다.

// 안전한 대체 함수 사용
snprintf(buffer, sizeof(buffer), "%s", user_input);

알려진 취약점 사례

Wu-ftpd 2.6.0에서는 2000년 발견된 포맷스트링 취약점(CVE-2000-0573)으로 원격 코드 실행이 가능했다.

ProFTPD 1.2.0에서는 로그 기능의 포맷스트링 취약점으로 원격 공격자가 임의 코드 실행을 할 수 있었다. Samba의 특정 버전에서도 포맷스트링 취약점이 발견되어 권한 상승 공격이 가능했다.

취약한 프로그램을 읽는 관점

다음 예제는 인자를 버퍼에 복사한 뒤 printf(buffer)로 출력한다. 버퍼 크기를 제한해 복사했더라도, 출력 단계에서 사용자 입력이 포맷 문자열로 해석되는 문제가 남는다.

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

int main(int argc, char *argv[]) {
    char buffer[100];

    if (argc > 1) {
        strncpy(buffer, argv[1], sizeof(buffer) - 1);
        buffer[sizeof(buffer) - 1] = '\0';
        printf(buffer); // 취약점: 사용자 입력을 직접 포맷 문자열로 사용
    }

    return 0;
}

./program "AAAA%x%x%x%x"를 실행하면 AAAAbffff754804847367fd8304처럼 스택 메모리가 노출될 수 있다. 이후 메모리 맵을 분석해 민감한 데이터 위치를 파악하고, %n 지시자를 사용한 메모리 쓰기 공격을 시도할 수 있다.

포맷스트링 취약점은 사용자 입력을 직접 포맷 문자열로 전달하는 작은 실수에서 시작한다. C/C++ 프로그램에서는 고정 포맷 문자열을 사용하고, 코드 리뷰와 정적·동적 분석으로 해당 호출을 지속적으로 점검해야 한다. 최신 보안 패치와 컴파일러 보안 옵션은 추가 방어 계층으로 활용할 수 있다.

포맷스트링 공격C/C++ 보안메모리 보안시큐어 코딩