포맷 스트링의 개념 및 구성요소 & FSB 취약점의 개념 및 원리

 

 

 

개요

 

 

 

C언어에서 데이터를 출력하기 위해 사용하는 대표적인 함수로는 printf()가 있다. 해당 함수는 데이터를 출력하기 위해 다음과 같이 포맷 스트링(Format String)이라는 형식을 사용한다.

 

 

포맷 스트링을 사용한 printf() 함수의 출력

 

 

 

이러한 포맷 스트링을 사용하는 함수들을 올바르게 사용하지 못할 경우 발생하게 되는 취약점이 포맷 스트링 버그(Format String Bug, FSB)다.

 

 

 

포맷 스트링 버그 취약점을 이용하면 레지스터 및 스택의 값을 조회할 수 있어 메모리가 노출되는 위험이 발생한다. 포맷 스트링 버그를 알아보기 전에, 우선 포맷 스트링이 무엇인지 알아보자.

 

 

 

 

 

 

 


포맷 스트링(Format String)이란?

 

 

 

포맷 스트링(Format String)은 프로그래밍 시 C언어의 scanf(), sscanf(), fscanf(), printf(), sprintf(), fprintf()와 같은 함수를 사용하여 데이터를 입·출력할 때 지정하는 형식을 말하며, 기본 형식은 다음과 같다.

 

%[parameter][flag][width][.precision]Conversion

 

 

 

 

1. Conversion

%[parameter][flag][width][.precision]Conversion

 

-> 서식 지정자(Conversion)는 데이터의 출력 형식을 지정한다. 아래의 표와 예시를 보면, 데이터의 출력 형식을 지정하는 것뿐 아니라 원하는 주소에 데이터를 입력할 수도 있다.

 

[ 종류 ]

 

%d   부호 있는 10진수 출력
%u   부호 없는 10진수 출력
%x   부호 없는 16진수 출력
%c   하나의 문자  출력
%s   문자열  출력
%p   포인터가 가리키는 주소  출력
%n   이전까지 출력한 바이트 수를 포인터가 가리키는 주소에 입력 (4byte)
%hn   이전까지 출력한 바이트 수를 포인터가 가리키는 주소에 입력 (2byte)
%hhn   이전까지 출력한 바이트 수를 포인터가 가리키는 주소에 입력 (1byte)

 

[ 예시 ]

 

#include <stdio.h>

int main(void) {
	char str[14] = "Hello, World!";
	int num = 1;

	printf("%d\n", -10);     // -10
	printf("%u\n", -10);     // 4294967286
	printf("%x\n", 'a');     // 61
	printf("%c\n", 'a');     // a
	printf("%s\n", str);     // Hello, World!
	printf("%p\n", str);     // 0x7fff413b3dfa
	printf("AAAA%n\n", &num); // AAAA
	printf("%d\n", num);     // 4

	return 0;
}

 

 

 

 

2. Parameter

%[parameter][flag][width][.precision]Conversion

 

-> 참조할 인자의 인덱스를 지정하며 해당 필드의 끝은 '$'로 표기한다.

 

[ 예시 ]

 

printf("%2$d, %1$d\n", 2, 1); // 1, 2

 

 

 

 

3. Flag

%[parameter][flag][width][.precision]Conversion

-> 출력 시 데이터를 정렬할 방식을 지정한다.

 

[ 종류 ]

 

0   출력 시 공백을 0으로 채움
+   출력 데이터를 오른쪽으로 정렬 ('+' 기호 포함 출력)
-   출력 데이터를 왼쪽으로 정렬 (생략 시 오른쪽 정렬)
,   출력 시 1000 단위로 구분 기호 삽입

 

 

 

 

4. Width

%[parameter][flag][width][.precision]Conversion

-> 출력 시 최소 너비를 지정하며, 출력 데이터가 최소 너비보다 짧을 시 공백문자로 패딩 한다.

 

[ 종류 ]

 

정수   정수의 값 만큼을 최소 너비로 지정
*   인자의 값 만큼을 최소 너비로 지정

 

 

 

 

5. Precision

%[parameter][flag][width][.precision]Conversion

-> 실수 출력 시 소수점의 자릿수를 설정하며, 해당 필드의 앞에 '.' 기호를 붙인다.

 

[ 예시 ]

 

printf("%.3f", 5.17489); // 5.174

 

 

 

 

 

 

 


포맷 스트링 버그(Format String Bug, FSB)란?

 

 

 

포맷 스트링 버그(Format String Bug, FSB)는 포맷 스트링을 사용하는 함수들을 올바르게 사용하지 못해 발생하게 되는 취약점이다.

 

 

 

포맷 스트링을 사용하는 함수의 올바른 사용은 포맷 스트링을 지정하고 사용하는 수만큼 그에 따른 올바른 형식의 인자를 지정해 주는 것이다. 아래의 코드와 같이 두 개의 printf() 함수들은 같은 결과 값을 출력하나, 두 번째의 printf() 함수에는 취약점이 존재한다.

 

 

#include <stdio.h>

int main() {
    char str[10];
    
    printf("%s", str); // 올바른 사용
    printf(str); // 올바르지 못한 사용
    
    return 0;
}

 

 

 

위의 두 번째 prnitf() 함수는 포맷 스트링 형식 사용 및 인자의 지정 없이 변수 str에 있는 값을 printf() 함수의 인자로 바로 가져온다.

 

 

 

만약 아래의 코드와 같이 해당 변수에 포맷 스트링 형식의 문자열이 존재할 경우, printf() 함수는 그것을 포맷 스트링 형식으로 인식하여 인자로 잘못된 값을 가져오는 버그가 발생하게 된다.

 

 

#include <stdio.h>

int main() {
    
    char str[0x10] = "%p";
    printf(str); // 0x7ffed79723b8
    
    return 0;
    
}

 

 

 

 

 

 

 


포맷 스트링 버그의 원리

 

 

 

포맷 스트링을 사용하는 함수들은 호출 시 포맷 스트링의 값을 채우기 위해 스택에 있는 값을 인자로 가져온다.

 

포맷 스트링의 인자값을 주었을 때 스택 구조

 

 

 

구체적으로 설명하면,

 

32bit는 인자를 스택에 쌓고, 64bit는 6개의 인자를 레지스터에 넣은 뒤 나머지 인자를 스택에 쌓기 때문에,

32bit의 경우 스택의 esp부터 4byte씩 뒤에 있는 위치에서 인자의 값을 가져오며, 64bit의 경우 레지스터의 값을 가져온 뒤 스택의 rsp부터 8byte씩 뒤에 있는 위치에서 인자의 값을 가져온다.

 

32bit   |    esp+4 -> esp+8 -> esp+12 -> esp+16 -> ···
64bit   |    rdi -> rsi -> rdx -> rcx -> r8 -> r9 -> rsp+8 -> rsp+16 -> ···

 

 

 

여기서 포맷 스트링을 사용하는 함수는 인자를 가져올 때 해당 위치의 스택 값을 검사하는 루틴 없이 위치값만을 참조하여 순서대로 가져오기 때문에,  인자를 요청할 때 주어진 인자값이 없다면 해당 위치의 스택 값을 인자로 가져오게 된다.

 

포맷 스트링의 인자값을 주지 않았을 때 스택 구조

 

 

 

그렇기 때문에 변수를 printf() 함수의 인자로 그대로 사용할 시, 악의적인 사용자가 해당 변수에 값을 입력할 수 있다면 포맷 스트링을 입력해 다수의 인자를 요청하여 메모리의 값을 읽어내는 것은 물론 조작까지 할 수 있다.

 

 

 

 

 

 

 


포맷 스트링 버그 공격 예시

 

 

 

  • 인자의 포인터 값을 출력해주는 '%p' 형식 지정자
  • 입력된 값의 바이트 수를 인자에 입력하는 '%n' 형식 지정자
  • 참조할 인자의 인덱스를 지정해주는 '$' 파라미터

 

 

위의 값을 이용하면 아래의 그림과 같이 원하는 인자의 값이 가리키는 주소에 값을 입력할 수 있다.

 

형식 지정자 '%n'을 이용해 원하는 값을 입력하는 문자열

 

 

 

 

위의 그림을 응용하여 원하는 주소를 스택에 넣고 '%p' 형식 지정자를 이용해 해당 입력값의 스택 위치를 찾은 다음,

 

형식 지정자 '%p'를 이용해 입력값의 스택 위치를 출력하는 과정

 

 

 

 

새로운 입력으로 원하는 주소를 스택에 넣고 해당 인자의 인덱스를 이용하면 원하는 주소에 원하는 값을 넣을 수 있다.

 

원하는 주소에 원하는 값을 입력하는 문자열

 

 

 

 

최종적으로, FSB 공격의 입력값을 아래와 같은 형식으로 정리할 수 있다. 해당 원리를 이용하면 FSB의 취약점을 통해 원하는 주소에 있는 값을 덮어쓰는 공격을 할 수 있다.

Input : [값을 넣을 주소]%[참조할 인자의 인덱스]$n

 

 

 

 

 

 

 


 

참고) 포맷 스트링 버그(Format String Bug, FSB) 실습 링크

 

- 32bit 환경

[Dreamhack] basic_exploitation_002 풀이 - 포맷 스트링 버그(Format String Bug, FSB) 실습 / 32bit 환경

 

- 64bit 환경

[Dreamhack] Format String Bug 풀이 - 포맷 스트링 버그(Format String Bug, FSB) 실습 / 64bit 환경

 

+ Recent posts