아래의 모든 내용의 원본은 Corelan.be에 corelanc0d3라는 저자에 의해 포스팅 된 Exploiit Development 관련 글이며, 해당 해석은 sinun.tistory.com/156에 있는 내용임을 분명히 밝히며, 저작권 침해 등이 있을 경우 즉각 조치할 수 있도록 할 것이다.
모든 프로그램 충돌이 Exploit 생성이 가능하다는 것을 의미하는 것은 아니다. 많은 경우에 응용 프로그램의 Crash는 Exploit 성공으로 이어지지 않지만, 때로는 가능하다. Exploitation은 본인이 작성하는 코드를 실행하는 경우와 같이 의도되지 않은 행위를 하게 만드는 것을 의미한다.
프로그램이 의도되지 않은 행위를 하게 만드는 가장 쉬운 방법은 응용프로그램의 흐름을 컨트롤 하는 것이다. 이것은 다음 명령어를 실행할 위치를 저장하고 있는 CPU 레지스터, 즉 IP (현재는 EIP, Extended Intruction Pointer)를 컨트롤함으로써 가능하다.
응용프로그램이 Parameter와 함께 함수 호출 시 해당 함수로 이동 전에 현재 Instruction Pointer에 있는 값을 저장한다. (따라서 함수가 모두 수행된 뒤, 어디로 리턴해야하는지 알 수 잇다.) 만약 저장된 Instruction Pointer를 변경할 수 있고, 직접 작성한 코드를 가르키게 할 수 있다면 프로그램의 흐름을 변경할 수 있고, 다른 실행 결과를 얻을 수 있다. (악성 행위 혹은 기타 원하는 행위)
프로그램 흐름을 변경한 뒤에 실행 대상이 되는 것은 주로 "Shell Code" 이다. 프로그램이 우리가 작성한 Shell Code를 실행하게 되면 우리는 그것을 Working Exploit 이라 한다. 대부분의 경우 프로그램의 흐름을 바꿀 수 있는 이 포인터는 EIP (Extended Instruction Pointer) 레지스터를 의미한다. 해당 레지스터는 4byte 이며, 이 4byte 값을 바꿀 수 있다면 프로그램을 장악할 수 있는 것이다.
용어 정리
모든 윈도우 응용 프로그램은 메모리를 사용하며 프로세스 메모리는 크게 3개의 요소로 구성
1) Code Segment : 프로세서가 실행할 명령어들이 저장. EIP는 다음 명령의 주소를 갖고 있게 된다.
2) Data Segment : 변수 및 동적 할당 버퍼
3) Stack Segment : 함수에 전달되는 데이터나 인자 또는 변수가 저장되는 공간. 스택은 가상 메모리의 끝에서 시작되며 밑으로 자란다. (Top - Down) Push 명령어는 스택의 최상위 주소에 어떤 값을 저장하며, Pop 명령어는 스택에서 한 개의 값(4byte)을 제거한 뒤 그 값을 레지스터에 저장. 스택 메모리에 직접적으로 접근하기 위해서는 스택의 최상위 주소를 가르키고 있는 ESP (Stack Pointer) 레지스터를 사용할 수 있다.
- 함수나 서브루틴에 진입 시에 새로운 Stack Frame이 생성된다. Stack Frame은 부모 함수의 Parameter를 보관하거나 서브루틴 시 인자를 전달하는 역할을 한다. 현재 스택의 주소는 스택 포인터 (ESP)를 통해서 접근할 수 있으며, 현재 함수의 base는 베이스 포인터 (EBP, or Frame Pointer)에 저장된다.
CPU (x86) 범용 레지스터 종류
1) EAX : Accumulator - 연산 과정에서 사용되거나 함수 호출의 결과 값을 저장하는데 주로 사용
기본 연산 (덧셈, 뺄셈, 비교)시 이 범용 레지스터를 이용
2) EBX : Base - 범용적인 목적이 없는 레지스터로 데이터 저장에 사용될 수 있다.
3) ECX : Counter - 반복 연산에 주로 사용. ECX 값이 감소하면서 카운트하게 된다.
4) EDX : Data - EAX 레지스터의 확장 기능을 담당. 추가적인 데이터 저장이 필요한 보다 복잡한 연산 (곱하기, 나누기)
등에 주로 사용
5) ESP : Stack - Stack Pointer, 스택을 알고 싶으면 이 레지스터를 찾아라.
6) EBP : Base - Stack Frame의 기반, 베이스 포인터
7) ESI : Source Index - 입력 값의 위치를 저장
8) EDI : Destination Index -
연산 결과의 값이 저장될 주소를 가르킴
9) EIP : Instruction Pointer
Process Memory
응용 프로그램이 Win32 환경에서 실행될 경우, 프로세스는 할당된 가상 메모리를 생성한다. 32bit 프로세스에서 주소 범위는 0x00000000 ~ 0xFFFFFFFF 이며, 0x00000000 ~ 0x7FFFFFFF까지는 사용자 영역, 0x80000000 ~ 0xFFFFFFFF까지는 커널 영역으로 할당된다. 윈도우는 CPU가 세그멘테이션과 페이징을 사용하지 않고, 직접적으로 동시에 선형적으로 메모리에 flat memory model을 사용한다. 커널 영역의 메모리는 OS에 의해서만 접근이 가능하다.
프로세스가 생성될 때, PEB (Process Environment Block)와 TEB (Thread Environment Block)가 생성된다.
PEB - 현재 프로세스의 사용자 영역의 파라미터 포함
- 메인 실행 모듈의 위치
- 프로세스에 로드되는 DLL과 모듈에 대한 포인터
- Heap 메모리 정보에 대한 포인터
TEB - 쓰레드의 상태를 표시
- 메모리 상의 PEB의 위치
- 쓰레드가 소유한 스택의 위치
- SEH chain의 첫번째 엔트리의 포인터
프로세스 내부의 각 쓰레드는 하나의 TEB를 갖고 있다.
프로그램 이미지와 DLL의 Text segment는 오직 프로그램 코드를 포함하므로 읽기 전용 속성을 갖는다. 이는 사용자가 프로그램 코드를 변경하는 것을 방지한다. text segment는 고정된 크기를 갖는다.
Data segment는 지역 변수 및 전역 변수를 저장한다. data segment는 초기화된 전역 변수, 문자열, 다른 변수들에 의해서 사용되어 진다. data segment는 쓰기 가능하며 크기가 정해져 있다.
Heap Segment는 나머지 프로그램 변수에 의해 사용되어 진다. Heap segment는 의도했던 것보다 커지거나 작아질 수 있다. Heap의 모든 메모리 영역은 할당 (Allocate) 알고리즘에 의해 관리되어 진다. Heap 메모리는 높은 주소로 증가한다. DLL에서 코드와 Import (DLL or 다른 DLL 또는 프로그램에서 사용되는 함수 리스트), exports는 .text segment의 일부분이다.
The Stack
Stack은 LIFO (Last In First Out) 데이터 구조를 갖는 프로세스 메모리의 일부분이다. Stack은 Thread 별로 OS에 의해 할당된다. Thread 종료 시에 Stack 또한 없어진다. Stack은 생성 시에 크기가 정해지며 크기가 변하지 않는다. LIFO 구조와 Stack 관리시 복잡한 구조나 매커니즘이 필요하지 않기 때문에 Stack은 매우 빠르다. 다만 크기에 한계가 존재한다. LIFO는 가장 최근에 할당된 (Push 명령에 의해) 데이터가 가장 먼저 제거 (Pop 명령에 의해) 된다는 의미이다. Stack이 생성될 경우 Stack Pointer는 Stack의 최상위를 가르킨다 어떤 정보가 Stack에 Push 될 경우 Stack Pointer는 감소하게 된다 따라서 본질적으로 Stack은 낮은 주소로 자라게 된다.
Stack은 지역 변수, 함수 호출과 같이 많은 시간 동안 저장할 필요가 없는 데이터들이 저장된다. 데이터가 Stack에 추가될 경우 Stack Pointer는 감소하게 되며 낮은 주소의 값을 가르키게 된다. 함수가 호출될 경우 함수의 파라미터는 Stac에 Push되며 EBP, EIP 레지스터의 값을 저장한다. 함수가 리턴될 경우 Stack에 저장되었던 EIP 값을 참조하여 EIP 레지스터를 복구한다. 따라서 프로그램의 흐름이 복구될 수 있다.
(복구라는 표현이 어울리는 것 같다. 함수가 끝난 뒤 내가 실행할 위치를 복구! 시킨다!)
Stack 동작에 대해서 아래와 같은 간단한 코드를 통해 살펴보자.
---------------------------------------------
#include <string.h>
void do_something(char *Buffer){
char MyVar[128];
strcpy(MyVar, Buffer);
}
int main(int argc, char *argv[]){
do_something(argv[1]);
}
---------------------------------------------
main() 함수 내에서 do_something(param1) 함수가 호출될 경우 다음과 같은 일이 발생된다.
부모(상위) Stack의 상단에 새로운 Stack Frame이 생성된다. Stack Pointer(ESP)는 새로 생성된 Stack의 최상단을 가르키게 된다. 이것이 "Top Of the Stack"이다.
위 소스 코드를 조금 수정하여 아래와 같이 변경해보자.

단지 MyVar 변수의 크기를 128에서 10으로 변경해주었다.

위 그림과 같이 OllyDbg에서 Arguments와 함께 실행할 수 있도록 한다.

위 그림을 보면 PUSH EBP로 시작하는 프롤로그 부분이 2개 연속 보인다. 즉, 2개의 함수가 연달아 있다고 생각을 하면되는데 프로그램의 시작부터 Step By Step으로 진행해온 결과 아래의 프롤로그 부분은 Main 함수이며, PUSH DWORD PTR DS:[EDI+4]는 우리가 위 Arguments에서 123456789012345678 준 값을 가지고 있다. 해당 Arguments를 Stack에 PUSH 한 다음에 do_something() 를 호출하고 있다. 해당 함수를 호출하기 전과 후의 Stack의 모습을 살펴보자.
do_something() 호출 바로 직전의 stack 모습이다. 0012FF70가 Base Pointer이며, 0012FF64가 Stack Pointer로 PUSH 된 Arguments와 함수 호출 뒤에 리턴될 주소 (EIP) 를 Stack에 담고 있다.


아래는 do_something() 으로 Step Into (F7) 한 뒤의 Stack의 모습이다. 호출 전 Stack의 모습과 크게 달라진 부분은 없다. 단지 호출 전 EBP 값인 0012FF70가 do_something()의 프롤로그 과정을 통해 Stack의 최상단에 PUSH가 된 것을 확인할 수 있다.


위 그림은 strcpy 함수를 호출하기 직전의 Stack의 모습이다. strcpy(MyVar, Buffer); 로서 001445A8 주소에 Buffer 값 123456789012345678이, 0012FF56에는 123456789012345678이 복사될 공간이다. 자세히 살펴보면 0012FF56은 위 Stack의 아래 부분에 존재하는 주소이다. 이 주소로부터 Buffer의 값이 복사되서 들어가는데 이 Buffer의 값이 설정된 값보다 크게 되면, 아래 보이는 0012FF64 004012FC RETURN to stack.004012FC from stack.004012D4, 즉 함수가 모두 정리되고 main으로 돌아갈 주소를 저장하고 있는 부분에 침범하게 될 수 있다. 한번 위 그림과 앞으로 나오는 아래 그림을 비교해보자.

전체적인 화면을 보자면 아래와 같은 그림으로 설명할 수 있겠다.

위와 같은 과정을 거쳐서 main으로 돌아갈 주소가 저장돼있던 0012FF64 주소에는 이상한 값이 들어가 있게 된다.
ESP는 여전히 문자열의 시작을 가르키고 있고, strcpy()는 아무 이상도 없는 것처럼 연산을 완료하게 된다. strcpy() 호출 뒤에 함수는 종료될 것이다.

함수 복구를 위한 부분은 변조되어 있고, 기본적으로 ESP를 Stackㅇ 저장된 EIP의 다음 값으로 복귀할 것이고, RET 명령이 실행될 것이다. 즉, 위 그림과 같이 do_something()의 에필로그 과정이 끝나고 RETN 시에 38373635 주소로 리턴하게 된다. 실제 38373635라는 메모리 번지는 해당 프로그램에 존재하지 않는 공간이다. ㅇ
이런 원리를 이용하여, 38373635 대신 공격자가 임의로 실행하고자 하는 코드가 있는 주소 값을 세팅할 수만 있다면 공격자는 원하는 행위를 수행할 수 있게 된다. 이것이 바로 EIP를 컨트롤 하는 과정이며, 정상 흐름 (현재 함수가 호출된 후의 명령)으로 돌아가기 위한 return 주소를 변경하게 되는 것이다.
[MyVar][EBP][EIP][YourCode]
Stack에 있는 버퍼가 Overflow 될 경우를 'stack based overflow' 또는 'stack buffer overflow'라고 한다. stack frame 이전의 값을 overwrite 할 경우는 'stack overflow'라고 한다. 이 둘은 분명 틀린 것이며 혼동해서는 안된다.