포맷 스트링이란?
외부로부터 입력된 값을 검증하지 않고 입·출력 함수의 포맷 문자열로 그대로 사용하는 경우 발생할 수 있는 보안 약점이다. 공격자는 포맷 문자열을 이용하여 취약한 프로세스를 공격하거나 메모리 내용을 읽거나 쓸 수 있다. 그 결과, 공격자는 취약한 프로세스의 권한을 취득하여 임의의 코드를 실행할 수 있다.
포맷 스트링의 종류에는 여러 가지가 있으며 그 중 C언어에서 일반적으로 변수를 입·출력문에서 일정한 형태로 받아들이거나 출력하기 위하여 사용하는 기호로는 다음과 같은 것들이 있다.
포맷 스트링 | 설명 |
%d | 정수형 10진수 상수 |
%f | 실수형 상수 |
%lf | 실수형 상수 |
%c | 문자값 |
%s | 문자 스트링 |
%u | 양의 정수(10진수) |
%o | 양의 정수(8진수) |
%x | 양의 정수(16진수) |
%n | 쓰인 총 바이트 수 |
%n 은 이전까지 입력되었던 문자열의 길이(Byte)수 만큼 해당 변수에 저장시키기 때문에 메모리의 내용도 변조 가능하므로 프맷 스트링 취약점에서 가장 중요하게 살펴보아야 한다. 문자열의 길이를 변조시키고 싶은 값의 길이만큼 만든 후 %n을 써주게 되면 메모리상에 공격자가 원하는 값을 넣을 수 있게 되기 때문이다.
포맷 스트링 점검
점검 내용 | 웹 애플리케이션에 포맷 스트링 취약점 존재 여부 점검 |
점검 목적 | 공격자의 포맷 스트링 취약점을 통한 악의적인 행위를 차단하기 위함 |
점검 대상 | 웹 애플리케이션 소스코드, 웹 기반 C/S 프로그램 |
1. 웹 사이트에서 사용자가 입력한 파라미터 값에 아래와 같은 패턴 입력 후 전송 시 다른 값을 입력했을 때는 발생하지 않는 에러 반응이나 멈추는 등 이상 반응을 보이는지 확인
- 패턴1 - %n%n%n%n%n%n%n%n%n%n
- 패턴2 - %s%s%s%s%s%s%s%s%s%s
- 패턴3 - %1!n!%2!n!%3!n!%4!n!%5!n!%6!n!%7!n!%8!n!%9!n!%10!n!
- 패턴4 - %1!s!%2!s!%3!s!%4!s!%5!s!%6!s!%7!s!%8!s!%9!s!%10!s!
패턴 1을 입력해 보았다.

별다른 이상 반응이 나타나지 않았다.

패턴 2를 입력해 보았다.

별다른 이상 반응이 나타나지 않았다.

패턴 3을 입력해 보았다.

별다른 이상 반응이 나타나지 않았다.

패턴 4를 입력해 보았다.

별다른 이상 반응이 나타나지 않았다.

보안 설정 방법
- 컴파일러에서 문자열 입력 포맷에 대한 자체적인 검사를 내장하고 있으므로 문자열 입력 포맷 검증 후 소스 코드에 적용한다.
- GCC 컴파일러는 문자열 입력 포맷과 실제 입력이 맞지 않는 경우 경고 옵션이 존재하나 해당 방식은 컴파일 과정에서만 검증 가능하며, 런타임 상황에서는 Fuzz testing을 이용하여 포맷 스트링 버그가 존재하는지 검증이 필요
- 웹 서버 프로그램 최신 보안패치를 적용한다.
- 웹 사이트에서 사용자가 입력한 파라미터 값 처리 중에 발생한 경우 사용자 입력 값의 유효성에 대한 검증 로직을 구현한다.
printf(), snprintf() 등 포맷 문자열을 사용하는 함수를 사용할 때는 사용자 입력값을 직접적으로 포맷 문자열로 사용하거나 포맷 문자열 생성에 포함시키지 않는다. 포맷문자열을 사용하는 함수에 사용자 입력값을 사용할 때는 사용자가 포맷 스트링을 변경할 수 있는 구조로 쓰지 않는다. 특히, %n, %hn은 공격자가 이를 이용해 특정 메모리 위치에 특정값을 변경할 수 있으므로 포맷 스트링 매개변수로 사용하지 않는다. 사용자 입력값을 포맷 문자열을 사용하는 함수에 사용할 때는 가능하면 %s 포맷 문자열을 지정하고, 사용자 입력값은 2번째 이후의 파라미터로 사용한다.
코드 예제
포맷 스트링 보안약점은 C 언어에 국한된 것은 아니다. 아래 예제 코드는 입력 자료의 유효성을 검증하지 않은 Java 프로그램에서도 발생할 수 있음을 보여준다. 이 프로그램에서 공격자는 %1$tm, %1$te, 또는 %1$tY과 같은 문자열을 입력하여 포맷 문자열에 포함시킴으로써, 실제 유효기간 validDate가 출력되도록 할 수 있다.
// 외부 입력값에 포맷 문자열 포함 여부를 확인하지 않고 포맷 문자열 출력에 값으로 사용
// args[0]의 값으로 “%1$tY-%1$tm-%1$te"를 전달하면 시스템에서 가지고 있는 날짜 (2014-10-14) 정보가 노출
import java.util.Calendar
......
public static void main(String[] args) {
Calendar validDate = Calendar.getInstance();
validDate.set(2014, Calendar.OCTOBER, 14);
System.out.printf( args[0] + " did not match! HINT: It was issued on %1$terd of some month", validate);
}
사용자로부터 입력 받은 문자열을 포맷 문자열에 직접 포함시키지 않고, %s 포맷 문자열을 사용함으로써 정보유출을 방지한다.
// 외부 입력값이 포맷 문자열 출력에 사용되지 않도록 수정
import java.util.Calendar
:
public static void main(String[] args) {
Calendar validDate = Calendar.getInstance();
validDate.set(2014, Calendar.OCTOBER, 14);
System.out.printf("%s did not match! HINT: It was issued on %2$terd of some month", args[0], validate);
}
이 예제의 msg는 신뢰할 수 없는 사용자 입력을 포함하고 있고 fprintf() 호출에서 포맷문자열 인자로 전달되기 때문에 포맷스트링 삽입에 취약하다.
void incorrect_password(const char *user) {
static const char msg_format[] = "%s cannot be authenticated.₩n";
size_t len = strlen(user) + sizeof(msg_format);
char *msg = (char *)malloc(len);
if (msg == NULL) {
/* 오류 처리 */
}
int ret = snprintf(msg, len, msg_format, user);
if (ret < 0 || ret >= len) {
/* 오류 처리 */
}
fprintf(stderr, msg);
free(msg);
msg = NULL;
}
이 예제는 fprintf() 대신에 fputs()를 사용하여, msg를 포맷문자열처럼 취급하지 않고 그대로 stderr로 출력한다.
void incorrect_password(const char *user) {
static const char msg_format[] = "%s cannot be authenticated.₩n";
size_t len = strlen(user) + sizeof(msg_format);
char *msg = (char *)malloc(len);
if (msg == NULL) {
/* 오류 처리 */
}
int ret = snprintf(msg, len, msg_format, user);
if (ret < 0 || ret >= len) {
/* 오류 처리 */
}
if (fputs(msg, stderr) == EOF) {
/* 오류 처리 */
}
free(msg);
msg = NULL;
}
참고 자료
소프트웨어 개발보안 가이드(2021.12.29)
주요정보통신기반시설 기술적 취약점 분석 평가 방법 상세가이드