[1] BufferOverflow Attack의 기초 ( 스택기반의 버퍼오버플로우 )

2017. 6. 1. 15:07SystemHacking/System



해당 게시글의 내용은 해커스쿨-도서관 에서 다운받은 파일을 기반으로 저의 공부를 위해 작성했습니다

" 해커 지망자들이 알아야 할 Buffer Overflow Attack의 기초 " By 달고나 님의 pdf의 사진과 글을 요약 정리했습니다




[1] 목적

Buffer Overflow 공격이 어떻게 이루어지는지를 설명하고 이러한 공격이 가능하게 되는 원리와 컴퓨터 시스템의 기본구조 설명



[2] 8086 Memory Architecture


< 그림 1 >


8086 시스템의 기본적인 메모리 구조는 그림1 과 같다. 시스템은 " 커널 " 을 메모리에 적재시키고 " 가용메모리 "를 확보한다

운영체제는 하나의 프로세스를 실행시키면 그 프로세스를 Segment라는 단위로 묶어서 가용메모리 영역에 저장시킨다

구조는 그림2 와같다


< 그림 2 >


Segment1 , Setment2 ... 등등 각각 저장하여 시스템의 멀티태스킹이 가능하게 된다

그리고 하나의 Segment는 Stack Segment , Data Segment , Code Segment 세가지로 구성 되어진다


1> Code Segment

시스템이 알아들을 수 있는 명령어, 즉 instruction들이 들어있다. ( 기계어 코드로써 컴파일러가 만들어낸 코드 )

Segment안에서 각각의 명령어들의 위치가 지정되어져 있는데 상대적인 위치개념을 사용하고 있다

먼저 Segment Selector에 의해서 Segment 의 위치를 알 수 있다. 해당 Segment의 위치를 " offset " 이라한다

그리고 Setment의 시작주소로 부터 해당 명령어까지의 거리를 " Logical Address " 라고 한다

따라서 실제 메모리에서의 주소 " Physical Address = offset + Logical Address " 식이 성립된다


< 그림 3 >


"0x80010000" 주소를 Segment의 시작주소이다. Code Segment내부에 있는 instruction IS1의 주소 "0x00000100" 이 보인다

실제 메모리에서 instruction IS1의 주소는 0x80010000 + 0x00000100 = 0x80010100 이 된다

따라서 segment가 메모리의 어떤 주소에 저장되어도 사용할 instruction의 정확학 위치를 찾아낼 수 있다


2> Data Segment

프로그램이 실행 될 때 사용되는 데이터가 들어간다. ( 데이터 == 전역변수 )


3> Stack Segment

현재 수행되고 있는 handler, task, program이 저장하는 데이터 영역으로 우리가 사용하는 버퍼가 들어있다



[3] 8086 CPU 레지스터 구조

위에서 가용메모리에 저장된 하나의 Segment가 세가지 Segment로 나누어지는 구조를 공부했다

그러면 이제 CPU가 프로세스를 실행하기 위해 프로세를 CPU에 적재시켜야 한다

각 프로세스에 필요한 instruction들과 데이터들을 적절하게 읽고 저장하기 위해서는 저장 공간이 필요하다.

CPU내부에 존재하는 메모리를 사용하는데 이 저장공간을 레지스터(register) 라고 한다. ( 속도가 빠름 )

레지스터는 목적에 따라서 4가지로 나뉘어진다 ( 범용, 세그먼트, 플래그 레지스터, 인스트럭션포인터 )


1> 범용 레지스터 ( General-Purpose Register )

논리 연산, 수리 연산에 사용되는 피연산자, 주소를 계산하는데 사용되는 피연산자, 그리고 메모리 포인터가 저장된다


< 그림 4 >


범용레지스터에는 EAX,EBX ... ESP 레지스터로 구성되어 있으며 각 레지스터는 목적을 가지고 있다


EAX    - 피연산자와 연산 결과의 저장소

EBX    - DS segment안의 데이터를 가리키는 포인터

ECX    - 문자열 처리나 루프를 위한 카운터

EDX    - I/O 포인터

ESP    - SS 레지스터가 가리키는  Stack segment의 맨 꼭대기를 가리키는 포인터

EBP    - SS 레지스터가 가리키는 스택 상의 한 데이터를 가리키는 포인터

 

2> 세그먼트 레지스터 ( Segment Register )

Code segment , Date segment , Stack segment 를 가르키는 주소가 들어있는 레지스터이다


< 그림 5 >


CS레지스터    - Code segment를 가리킨다 

( DS, ES, FS, GS )레지스터    - Data segment를 가리킨다 

SS레지스터    - Stack segment를 가리킨다


* 세그먼트 레지스터가 가리키는 위치를 바탕으로 우리가 원하는 segment안의 특정 데이터나 명령어를 정확하게 가져올 수 있다

3> 플래그 레지스터 ( Program status and control Register )

프로그램의 현재 상태나 조건 등을 검사하는데 사용되는 플래그들이 있는 레지스터이다


4> 인스트럭션 포인터 ( Instruction Pointer )

다음 수행해야하는 명령(instruction)이 있는 메모리 상의 주소가 들어가 있는 레지스터이다



위의 레지스터와 메모리구조에 대해서 어느 정도 숙지하고 있다면 지금부터 설명할 buffer overflow 공격을 이해하는데 있어서 도움이 될것이다

프로그램이 실행되면 Segment에서는 어떤 일이 벌어지는 지 확인해보도록 하자 ( gdb를 통한 디버깅 )



[4] 프로그램 동작 시 Segment에서는 무슨 일이?

프로그램이 실행되어 프로세스가 메모리(Register)에 적재되고 메모리와 레지스터가 어떻게 동작하는지에 대해 알아보기위해 간단한 프로그램을 만들어보겠다


< 그림 6 >


$vi sample.c    아무의미 없는 소스파일을 하나 작성했습니다

$gcc -o sample sample.c    해당 소스파일을 컴파일 시켜서 sample 이라는 파일 하나를 생성했습니다

$gdb    디버깅하여 어셈블리코드와 메모리에 적재될 Logical address 를 관찰하기 위해 gdb를 실행한다


< 그림 7 >

# 그림 7은 sample파일의 어셈블리 코드들이다

# 앞에 붙어 있는 주소는 Logical Address 이다


지금부터 프로그램이 실행할 때 Stack Segment 와 Code Segment 가 어떤 동작을 하는지 알아보자



< 그림 8 >

1> sample프로그램이 실행될 때의 Segment의 모습이다 

ESP 레지스터는 Stack Segment의 제일 꼭대기를 가리키고 있다

해당 Stack Segment의 지점에 " PUSH " or " POP " 명령어를 수행 할 위치를 설정해주는 역할

EIP 레지스터는 다음에 실행시킬 명령어가 있는 주소값( Code Segment의 offset값 )을 가진다. 

현재 main함수의 시작점을 가리키고 있다



< 그림 9 >

2> <main+0> ~ <main+16>

push %ebp            Stack segment에 main()함수가 실행 되기 전의 " base pointer " 를 저장 시킨다

mov %esp, %ebp    ESP값을 EBP의 값으로 복사한다 - base pointer 와 stack pointer는 같은 지점을 가리키게 된다

sub $0x8, %esp       ESP = ESP - 8     8byte만큼의 공간을 생성한다    // 여기까지 3개의 과정을 함수프롤로그 작업이라 한다

sub $0x4,%esp        ESP = ESP - 4     4byte만큼의 공간을 더 생성한다



< 그림 10 >

3> <main+19> ~ <main+25>

function의 인자로 들어갈 1,2,3 이 push되었다

" call " 명령은 함수를 호출할 때 사용되는 명령으로 호출한 함수의 실행이 끝난 이 후 명령이 있는 주소를 스텍에 넣은 다음 

EIP에 함수의 시작 지점 주소를 넣는다 ( add $0x10,%esp 의 주소를 스택에 PUSH 하고 function을 실행할 것이다 )

즉, 호출된 함수가 종료되고 이후에 실행될 명령어의 주소를 POP하여 어디에 있는 명령어를 수행해야 하는지 알 수 있다는 것

이것이 바로 " Return Address " 입니다 ( = 호출된 함수 종료후 수행해야 할 명령어가 있는 주소 )



< 그림 11 >

4> <function+0> ~ <function+3>

push %ebp            function함수가 시작되기 전 function함수가 종료되고 돌아갈 base pointer를 Stack에 PUSH 해놓는다

mov %esp,%ebp     ESP의 값을 EBP에 저장시킨다

sub $0x28,%esp      ESP = ESP - 40byte    40byte만큼의 공간을 생성한다



< 그림 12 >


< 그림 13 >

5> " leave " 

" leave " 명령은 함수 프롤로그 작업의 반대의 작업을 수행시킵니다

mov %ebp,%esp    EBP의 값을 ESP에 저장시킨다. 즉 생성했던 40byte의 공간을 없앤다

pop %ebp            PUSH 해놓았던 function의 base pointer를 꺼내온다

그림 12는 leave명령을 수행하려는 바로 직전의 모습입니다

그림 13은 base pointer를 POP으로 꺼내서 저장해두었던 main함수의 base pointer로 이동한 모습



< 그림 14 >

6> " ret "

" ret " 명령은 현재 ESP에 있는 ' add ' 명령의 주소를 스택에서 POP하여  EIP 레지스터에 넣어주는 역할을 합니다

위에서 말했듯이 add명령은 Return address 입니다

" ret " 명령은 호출한 함수가 실행을 마치면 return address 를 EIP에 넣어서 다음 실행할 명령어가 있는 주소로 이동시킵니다



< 그림 15 > 

7> " add "

main함수의 과정중에서 push $0x1 , push $0x2 , push$0x3  , sub $0x4,$esp 네개의 과정이 있다

각 과정을 위해 사용했던 16byte의 공간을 제거해주는 역할을 한다

그리고 " leave " 명령을 수행한다



8> <main+33> ~ <main+39>

" leave " , " ret " 과정이 위에서 ' 5 ' 와 ' 6 ' 과정과 같은 흐름으로 진행된다

mov %ebp %esp         생성시켰던 12byte를 제거

pop %ebp                  이전에 저장시켰던 base pointer로 돌아간다


main()함수를 실행하기 이전으로 돌아가게 될것이다. 아마도 init_process()함수로 돌아갈 것이다

이 함수는 운영체제가 호출하는 함수로 알아야 할 필요는 없다.


지금까지 Segment의 동작과정을 살펴보았고, 위 과정을 잘 숙지하고 있다면 버퍼 오버플로우 공격을 이해하기 쉬울 것이다

버퍼 오버플로우 공격은 버퍼를 Return Address 의 위치까지 데이터를 넘치게하여 RET를 변질시키는 원리입니다





[6] Buffer Overflow공격의 이해


< 그림 16 공격방법1 >


그림 16은 function()함수의 Stack Segment 부분을 나타내고 있습니다.

40byte 공간은 function함수의 시작부분에서 할당해준 지역변수의 공간입니다

그리고 SFP주소에는 현재 function()함수의 base pointer가 위치해있습니다 ( call 명령 실행 시 ebp를 PUSH )

그리고 " add " 의 명령이 있는 주소가 RET에 위치해있습니다 ( Return Address : 공격할 주소 )

마지막으로 RET주소 위의 공간에는 공격코드가 들어있습니다 


[ 방법1 ]


40byte + 4byte = 44byte만큼 아무 데이터나 넣어주고 RET주소에는 공격코드가 들어있는 주소 A를 넣어주면 됩니다

그럼 function()함수의 마지막에서 " ret " 명령이 주소A를 EIP레지스터에 넣어서 해당 공격코드를 실행시킵니다

그럼 버퍼 오버플로우 공격은 성공입니다 하지만 RET의 윗 공간에 공격코드를 넣을만큼 버퍼가 부족하다면?

그 때는 40byte의 지역변수공간을 활용하는 방법도 있습니다



< 그림 17 공격방법2 >

[ 방법2 ]


RET위의 공간이 공격코드를 작성할 만큼 충분하지 않다면 지역변수의 공간을 활용하면 된다

먼저 지역변수공간에 공격코드를 작성하고 그 주소로 알아놓는다

그리고 주소A에는 그 공격코드로 ESP를 이동시키는 코드를 작성해놓는다

" ret " 명령에 의해서 주소A가 EIP에 들어가고 주소A의 코드에 의해 ESP는 주소B를 가리키게 된다

결국, EIP는 주소B의 공격코드를 실행시키게 된다



시스템메모리의 구조와 프로그램의 동작 과정 그리고 버퍼 오버플로우공격의 원리를 공부했습니다

다음 글에서는 쉘 코드 작성법에 대해서 공부하겠습니다