뉴비에욤

Shellcode 만들기 - 3 (메모리 세그먼트 구조) 본문

System Hacking(OS)/Shellcode

Shellcode 만들기 - 3 (메모리 세그먼트 구조)

초보에욤 2015. 10. 1. 01:04

일반적으로 운영체제는 하나의 프로세스를 실행하게 되면 이 프로세스를 세그먼트(Segment) 단위로 메모리에 저장한다. 이 세그먼트는 다시 세 영역으로 구분되어, 컴파일러(Compiler)에 의해 기계어 코드로 변환된 명령어 집합이 저장되는 코드 세그먼트(Code Segment), 프로그램이 참조하는 데이터(주로 전역(global) 변수들)가 저장되는 스택 세그먼트(Stack Segment)로 나뉜다.

 

이 때 메모리에 저장되는 위치는 보통 실행 시간(Run Time, 실행되는 순간)에 정해지기 때문에, 컴파일 과정에서 각 명령어의 정확한 위치를 지정해 줄 수 없다. 따라서 각 세그먼트는 가상 주소인 논리적 주소(Logical Address)를 사용하여 상대적인 위치를 지정한 후, 실행 시간에 정해지는 자신의 시작 위치(Offset)를 더하는 방식으로 실제 메모리의 물리적 주소(Physical Address)를 인지한다.

 

 

 

 

 

 

물리적 주소 = 논리적 주소 + 시작 위치

 

 

새로운 프로세스가 생성되었을 때 메모리의 변화를 통해 프로그램의 세부 동작 과정을 알아보기 위하여, 다음과 같은 간단한 프로그램을 작성한다.

* OS 환경

커널 버전 : 2.6.32-504.16.2.el6.i686

gcc 버전 : gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-16)
gcc 컴파일 옵션 : gcc -fno-stack-protector -mpreferred-stack-boundary=2 -z execstack

 

 

본 프로그램을 컴파일 한 후, 디버깅을 위해 GDB(GNU Debugger)로 실행시킨다.

 

 

 

 

각 함수를 어셈블리어 코드로 변환(Disassemble)한 결과와 각 코드의 상대 주소가 나타나는 것을 확인할 수 있다. 해당 코드는 컴파일러에 따라 다르게 나타나기 때문에 위 사진과 다를 수 있으며, 현재 main 함수의 주소가 func 함수의 주소보다 높은 주소에 위치하고 있다.

 

 

 

프로그램 실행 시 레지스터의 값을 확인하기 위해 위 그림과 같이 main 함수에 브레이크포인트(Breakpoint)를 설치하고 진행한다.

 

현재 main 함수에 브레이크포인트를 설치하였기 때문에 다음에 실행할 명령어의 주소가 저장되는 EIP 레지스터에는 "0x08000000" 부터 시작하는 코드 세그먼트 내 main 함수의 첫 번쨰 코드 주소(0x080483a8)가 저장되어 있다. 한편 현재 스택의 꼭대기를 가리키고 있는 ESP 레지스터의 값을 통해 "0xBFFFFFFF"까지 확장된 스택 세그먼트 내 현재 스택 포인터의 위치(0x0bffff67c)를 확인할 수 있다. 또한 스택의 바닥을 가리키는 EBP 레지스터는 일반적으로 이전 함수의 시작 위치를 가리키며, 이전 함수의 데이터를 보존하기 위한 목적으로 사용된다. 현재의 세그먼트 구조를 간략하게 그림으로 나타내면 아래와 같다.

 

 

 

지금부터 변환된 어셈블리어 코드를 하나씩 따라가면서 본 프로그램의 동작 과정을 자세히 살펴볼 것이다.

 

 

 

(gdb) disas main
Dump of assembler code for function main:
   0x080483a8 <+0>: push   ebp
   0x080483a9 <+1>: mov    ebp,esp
   0x080483ab <+3>: sub    esp,0x8
   0x080483ae <+6>: mov    DWORD PTR [esp+0x4],0x2
   0x080483b6 <+14>: mov    DWORD PTR [esp],0x1
   0x080483bd <+21>: call   0x8048394 <func>
   0x080483c2 <+26>: mov    eax,0x0
   0x080483c7 <+31>: leave 
   0x080483c8 <+32>: ret   
End of assembler dump.
(gdb)

 

* main 함수 시작 과정

 

먼저 첫 번째 코드 "push ebp"는 EBP 레지스터를 스택에 저장하는 부분으로, 새로운 함수를 시작하기에 앞서 이전 함수의 데이터를 보존하기 위한 단계이다. 다음에 수행될 "mov ebp,esp" 코드를 통해 EBP에 ESP 값을 저장함으로써, EBP는 새로운 함수 main 의 시작 위치를 기억하게 된다. 다음 "sub esp,0x8" 코드는 스택을 필요한 만큼 확장하는 과정으로, 본 프로그램에서는 func 함수 호출을 위해 필요한 4바이트 정수형(Integer, int) 인자 두 개를 저장하기 위해, 총 8바이트가 확장되었다. 참고로 스택은 워드(Word) 단위로 확장되기 때문에, 경우에 따라서는 불필요한 값(Dummy)이 추가로 확장될 수 있다.

 

지금까지의 과정을 다시 정리하면, 먼저 이전 함수의 데이터를 보존하기 위해 EBP 레지스터를 스택에 저장하고, EBP 레지스터에 새로운 함수의 시작 부분을 가리키는 ESP 레지스터를 저장한 후 스택을 확장하였다. 이 과정은 main 함수를 포함한 모든 함수에서 동일하게 이루어지는데, 이를 함수 시작(Function Prologue, 함수 프롤로그) 과정이라고 한다.

 

 

* main 함수 프롤로그 이후 세그먼트 구조

 

한편 세 번째 코드(push ebp | mov ebp,esp | sub esb,0x8)까지 진행된 후의 레지스터 상태를 고려해보면, 먼저 첫 번째 코드를 통해 ESP의 위치가 "0xBFFFF67C"로부터 한 워드 아래인 "0xBFFFF678"로 변경되고, 해당 위치에 EBP의 값 "0xBFFFF6F8"이 저장된다. 다음 코드를 통해 ESP의 위치 "0xBFFFF678"가 EBP에 저장되고, 세 번째 코드를 거치면서 ESP의 위치가 8 바이트만큼 확장되어 "0xBFFFF670"으로 변경된다.

 

 

main 함수 시작 과정이 끝난 다음 단계는 실제 코드가 실행되는 부분으로, 본 프로그램에서는 func 함수를 호출하는 과정이 진행된다.

 

(gdb) disas main
Dump of assembler code for function main:
   0x080483a8 <+0>: push   ebp
   0x080483a9 <+1>: mov    ebp,esp
   0x080483ab <+3>: sub    esp,0x8
=> 0x080483ae <+6>: mov    DWORD PTR [esp+0x4],0x2
   0x080483b6 <+14>: mov    DWORD PTR [esp],0x1
   0x080483bd <+21>: call   0x8048394 <func>
   0x080483c2 <+26>: mov    eax,0x0
   0x080483c7 <+31>: leave 
   0x080483c8 <+32>: ret   
End of assembler dump.
(gdb)

* func 함수 호출 과정

 

첫 번쨰와 두 번째 코드(+6, +14)는 func 함수를 호출하는데 필요한 인자들을 스택에 저장하는 역활을 한다. 이 때 첫번째 코드 "mov DWORD PTR[esp+0x4], 0x2"를 통하여 ESP+0x4 위치에 먼저 인자 '2'를 저장하고, 두 번째 코드 "mov DWORD ptr[esp], 0x1"을 통해 ESP의 위치에 인자 '1'을 저장한다. 이처럼 호출되는 함수의 인자 순서를 반대로 저장하는 이유는 스택의 후입선출(LIFO, Last In First Out) 구조 특성 상 함수 호출 시 스택의 데이터를 차례로 불러올 떄, 인자의 순서를 올바르게 유지하기 위함이다.

 

또한 세 번쨰 "Call 0x8048394 <func>" 코드를 거치며func 함수의 시작 주소인 "0x8048394"를 EIP에 저장하여 명령어 흐름을 호출되는 함수로 전달한다. 한편 호출된 함수의 실행이 모두 끝나고 나면 다시 돌아와 다음 명령어를 계속해서 실행해야 하기 때문에, 명령어의 흐름을 바꾸기 전 스택에 다음 명령어의 위치를 미리 저장한다. 즉 해당 코드를 의미에 따라 풀어보면 다음과 같이 표현할 수 있다.

 

push 0x08483c2

             eip := 0x08048394 <func>

 

한편 두 번째 코드까지 진행된 후의 레지스터 상태를 고려해보면, ESP+0x4의 위치 0xBFFFF674에 인자 값 '2'가 저장되고, ESP의 위치 0xBFFFF670에 인자 값 '1'이 저장된다. 세 번째 call 명령어 코드를 거치면서 의미상 push 명령어를 통하여 ESP의 위치가 "0xBFFFF670"으로부터 한 워드 아래인 "0xBFFFF66C"로 변경되고, 해당 위치에 다음 명령어의 위치 "0x080483C2"가 저장된다. 또한 EIP는 func 함수의 시작 위치 "0x0848394"를 값으로 가진다. 이상 func 함수 호출 과정 이후의 세그먼트 구조를 나타내면 아래 그림과 같다.

 

func 함수 호출 과정 이후의 세그먼트 구조

 

 

 

본 프로그램의 명령어 흐름이 func 함수로 전달되어, 다음의 코드가 연이어 실행된다.

 

 

Dump of assembler code for function func:
=> 0x08048394 <+0>: push   ebp
   0x08048395 <+1>: mov    ebp,esp
   0x08048397 <+3>: sub    esp,0x4
   0x0804839a <+6>: mov    eax,DWORD PTR [ebp+0xc]
   0x0804839d <+9>: mov    edx,DWORD PTR [ebp+0x8]
   0x080483a0 <+12>: lea    eax,[edx+eax*1]
   0x080483a3 <+15>: mov    DWORD PTR [ebp-0x4],eax
   0x080483a6 <+18>: leave 
   0x080483a7 <+19>: ret   
End of assembler dump.
(gdb)

 

* func 함수 시작 및 연산 과정

 

먼저 첫 번째 코드부터 세 번째 코드까지는 앞서 amin 함수 시작 과정과 마찬가지로 func 함수의 시작을 위한 준비 과정이다. 차례로 이전 함수(main)의 데이터를 보존하기 위해 스택에 EBP를 저장하고, EBP에 새롱누 함수(func)의 시작 부분을 가리키는 ESP를 저장한 후 스택을 확장하였다. 이상  func 함수 프롤로그 과정 이후의 세그먼트 구조를 나타내면 아래 그림과 같다.

 

func 함수 프롤로그 과정 이후의 세그먼트 구조

 

 

네 번째와 다섯 번째 코드는 해당 함수의 호출을 위해 전달된 인자를 연산에 사용할 수 있도록 각각 범용 레지스터에 저장하는 부분이다. 네 번째 "mov eax, DWORD PTR [ebp+0xc]" 코드는 EAX 레지스터에 EBP+0xC(12)의 위치 "0xBFFFF674"에 있는 인자 '2'를 저장하고, 다섯 번째 "mov edx, DWORD ptr [ebp+0x8]" 코드는 EDX 레지스터에 EBP+0x8의 위치 "0xBFFFF670"에 있는 인자 '1'를 저장한다. 그리고 여섯 번째와 일곱 번째 코드는 새로운 변수에 인자들의 할당하는 부분이다. 먼저 여섯번째 코드 "lea eax, [edx+eax*1]"를 통해 EDX와 EAX 레지스터에 저장된 두 인자를 합한 결과 3을 EAX에 저장하고, 일곱 번째 코드 "mov DWORD PTR [ebp-0x4], eax"를 통해 EAX의 값 '3'을 EBP-0x4의 위치 "0xBFFFF664"에 저장한다. 참고로 본 함수는 리턴 값이 없는 함수이지만, 리턴 값이 있는 경우 그 값은 보통 EAX에 저장된다. 이상 func 함수 연산 과정 이후의 세그먼트 구조를 나타내면 아래 그림과 같다.

 

func 함수 연산 과정 이후의 세그먼트 구조

 

 

 

여기까지의 코드 실행 상태는 다음과 같다.

 

Dump of assembler code for function func:
   0x08048394 <+0>: push   ebp
   0x08048395 <+1>: mov    ebp,esp
   0x08048397 <+3>: sub    esp,0x4
   0x0804839a <+6>: mov    eax,DWORD PTR [ebp+0xc]
   0x0804839d <+9>: mov    edx,DWORD PTR [ebp+0x8]
   0x080483a0 <+12>: lea    eax,[edx+eax*1]
   0x080483a3 <+15>: mov    DWORD PTR [ebp-0x4],eax
=> 0x080483a6 <+18>: leave 
   0x080483a7 <+19>: ret   
End of assembler dump.
(gdb)

 

* func 함수 종료 과정

 

지금부터 이어지는 과정은 해당 함수의 실행을 모두 마치고 자신을 호출한 곳으로 되돌아가는 부분으로, 흔히 함수 종료(Function Epilogue, 함수 에필로그) 과정이라 한다. 본 과정은 앞서 살펴본 함수 프롤로그 과정의 역순으로, 해당 함수를 위해 확장된 스택을 되돌리고 이전 함수의 EBP를 복원시킨다. 또한 스택에 저장된 다음 명령어 주소를 불러오며, 명령어의 흐름을 이전 함수로 전달한다. 이 과정을 통해 전체적인 스택 구조는 해당 함수가 호출되기 전의 상태로 되돌아간다.

 

먼저 첫 번째 코드 "leave"는 함수 시작 과정의 역순으로, 먼저 함수 프롤로그에서 설정한 EBP를 기준으로 확장된 스택을 되돌리고, 스택에 저장된 이전 함수 "main"의 EBP를 복원시킨다. 해당 코드를 의미에 따라 풀어쓰면 다음과 같다.

 

      mov esp,ebp

pop ebp

 

위 코드를 통해 ESP의 위치는 해당 함수 func의 시작을 가리키는 EBP의 위치 0xBFFFF668로 변경된 후, 다시 pop 명령어를 거치며 한 워드 변경되어 "0xBFFFF66C"를 가리키게 된다.  또한 EP 역시 스택에 저장된 이전 함수 main의 EBP "0xBFFFF678"로 변경된다.

 

다음 두 번째 코드 "ret"는 스택에 저장된 다음 명령어 주소를 불러와, 명령어의 흐름을 이전 함수 main으로 전달하는 부분으로, 의미에 따라 풀어쓰면 다음과 같다.

 

pop eip

 

위 코드를 통해 ESP의 위치는 다시 한 워드 변경되어 "0xBFFFF670"을 가리키게 되고, EIP에는 이전 함수 main의 다음 실행될 코드 주소 "0x080483C2"가 저장된다. 이상 func 함수 에필로그 과정 이후의 세그먼트 구조를 나타내면 아래 그림과 같다.

 

func 함수 에필로그 과정 이후의 세그먼트 구조

 

 

 

명령어의 흐름이 다시 main 함수로 전달된 이후, main 함수에서도 종료를 위한 과정이 진행된다.

 

(gdb) disas main
Dump of assembler code for function main:
   0x080483a8 <+0>: push   ebp
   0x080483a9 <+1>: mov    ebp,esp
   0x080483ab <+3>: sub    esp,0x8
   0x080483ae <+6>: mov    DWORD PTR [esp+0x4],0x2
   0x080483b6 <+14>: mov    DWORD PTR [esp],0x1
   0x080483bd <+21>: call   0x8048394 <func>
=> 0x080483c2 <+26>: mov    eax,0x0
   0x080483c7 <+31>: leave 
   0x080483c8 <+32>: ret   
End of assembler dump.
(gdb)

 

main 함수 에필로그 과정

첫 번쨰 코드(+26) "mov eax, 0x0"은 main 함수가 정상적으로 종료되었다는 의미다. amin 함수를 호출한 운영체제 내의 특정 함수에 0을 반환하는 부분으로, 리턴 값을 전달하기 위하여 EAX에 '0'을 저장하는 과정이다. 나머지 두 번째와 세 번째 코드는 함수 func 함수 에필로그 과정과 마찬가지로 main 함수의 종료를 위한 과정이다. 차례로 main 함수를 위해 확장된 스택을 되돌리고 이전 함수의 EBP를 복원시킨 뒤, 스택에 저장된 다음 명령어 주소를 불러오며 명령어의 흐름을 이전 함수로 전달한다. 이 과정을 통해 전체적인 스택 구조는 main 함수가 호출되기 전의 상태로 되돌아가며, 이상 main 함수 에필로그 과정 이후의 세그먼트 구조를 나타내면 아래 그림과 같다.

 

main 함수 프롤로그 과정 이후의 세그먼트 구조

Comments