포맷스트링 공격과 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(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++ 프로그램에서는 고정 포맷 문자열을 사용하고, 코드 리뷰와 정적·동적 분석으로 해당 호출을 지속적으로 점검해야 한다. 최신 보안 패치와 컴파일러 보안 옵션은 추가 방어 계층으로 활용할 수 있다.