2. 8086 Memory Architecture
- 시스템이 초기화 되기 시작하면 시스템은 커널을 메모에 적재시키고 가용 메모리를 확인함.
- 시스템은 운영에 필요한 기본적인 명령어 집합을 커널에서 찾으므로 커널 영역은 반드시 해당 위치에 있어야 함.
- 기본적으로 커널은 64KB 영역에 있지만 오늘날의 운영체제는 이를 확장하여 더 큰 영역을 사용함.
- 32bit 시스템에서 메모리 영역에 주소를 할당할 수 있는 범위: 0 ~ 2^32-1
- 64bit 시스템에서 메모리 영역에 주소를 할당할 수 있는 범위: 0 ~ 2^64-1
- 오늘날 시스템은 멀티태스킹이 가능하므로 메모리에는 여러 개의 프로세스가 저장되어 병렬적으로 작업을 수행.
- 가용한 메모리 영역에는 여러 개의 segment(하나의 프로세스를 묶는 단위)들이 저장될 수 있음.
- 하나의 프로세스는 code segment, data segment, stack segment의 구조를 가짐.
- 시스템에는 최대 16,383개의 segment가 생성될 수 있고, 그 크기와 타입은 다양할 수 있음.
- 하나의 segment는 최대 2^32byte의 크기를 가짐.
code segment
- 시스템이 알아들을 수 있는 명령어(instruction, 컴파일러가 만들어낸 기계어 코드)가 들어 있음.
- instruction들은 명령을 수행하면서 많은 분기 과정과 점프, 시스템 호출 등을 수행함.
- 분기와 점프의 경우 메모리 상의 특정 위치에 있는 명령을 지정해야 함.
- segment는 컴파일 과정에서 현재 메모리 상 어느 위치에 저장될지 알 수 없으므로 정확한 주소를 지정할 수 없음.
- segment는 segment selector에 의해 자신의 시작 위치(offset)를 찾을 수 있고 시작 위치로부터의 위치(logical address)에 있는 명령을 수행할 지 결정함.
- 실제 메모리 주소(physical address) = offset + logical address
data segment
- 프로그램 실행 시 사용되는 데이터(전역 변수)가 들어감.
- 현재 모듈의 data structure, 상위 레벨로부터 받아들이는 데이터 모듈, 동적 생성 데이터, 다른 프로그램과 공유하는 공유 데이터로 나뉨.
stack segment
- 현재 수행되고 있는 handler, task, program이 저장하는 데이터(지역 변수) 영역으로, 우리가 사용하는 버퍼가 자리잡음.
- 프로그램이 사용하는 multiple 스택을 생성할 수 있고 각 스택들 간 switch가 가능.
- 스택은 처음 생성될 때 필요한 크기만큼 만들어지고 프로세스의 명령에 의해 데이터를 저장해 나가는 과정을 거치게 됨.
- stack pointer(SP)라고 하는 레지스터가 스택의 맨 꼭대기를 가리킴.
- 스택에 데이터를 저장하고 읽는 과정은 PUSH와 POP instruction에 의해 수행됨.
3. 8086 CPU 레지스터 구조
- CPU는 데이터를 재빨리 읽고 쓰기 위해 내부에 존재하는 메모리를 사용하는데, 이러한 저장 공간이 레지스터(register).
- 레지스터는 목적에 따라 범용 레지스터(General-Purpose register), 세그먼트 레지스터(Segment register), 플래그 레지스터(Program status and control register), 인스트럭션 포인터(Instruction pointer)로 구성됨.
범용 레지스터
- 논리 연산, 수리 연산에 사용되는 피연산자, 주소를 계산하는데 사용되는 피연산자, 메모리 포인터가 저장되는 레지스터
- 각 레지스터는 EAX, EBX, ECX, EDX.. 등으로 불림.
- AX 레지스터의 상위 부분을 AH, 하위 부분을 AL이라고 함.
- 각 레지스터는 프로그래머가 필요에 따라 임의로 사용해도 되지만 목적대로 사용하는 것이 좋음.
세그먼트 레지스터
- code segment, data segment, stack segment를 가리키는 주소가 있는 레지스터
- CS 레지스터는 code segment, DS, ES, FS, GS 레지스터는 data segment, SS 레지스터는 stack segment를 가리킴.
플래그 레지스터
- 프로그램의 현재 상태나 조건 등을 검사하는데 사용되는 플래그들이 있는 레지스터
- 컨트롤 플래그 레지스터는 상태 플래그, 컨트롤 플래그, 시스템 플래그로 구성.
- 초기화 시 0x00000002의 값을 가짐.
- 1, 3, 5, 15, 22~31번 비트는 예약되어 있어 소프트웨어에 의해 조작할 수 없음.
인스트럭션 포인터
- 다음으로 수행할 명령(instruction)이 있는 메모리 상의 주소가 있는 레지스터
- JMP, Jcc, CALL, RET, IRET instruction이 있는 주소값을 가짐.
- EIP 레지스터는 소프트웨어에 의해 바로 액세스 할 수 없고 control-transfer instruction(JMP, Jcc, CALL, RET)이나 interrupt와 exception에 의해 제어됨.
- EIP 레지스터는 CALL instruction을 수행하고 나서 프로시저 스택(procedure stack)으로부터 리턴하는 instruction의 address를 읽는 과정으로 읽을 수 있음.
- 프로시저 스택의 return instruction pointer의 값을 수정하고 return instruction(RET, IRET)을 수행함으로 해서 EIP 레지스터의 값을 간접적으로 지정할 수 있음.
4. 프로그램 구동 시 Segment에서 일어나는 일
void function(int a, int b, int c){
char buffer1[15];
char buffer2[10];
}
void main(){
function(1, 2, 3);
}
<simple.c>
- simple.c를 어셈블리 코드로 변환하기 위해 아래와 같은 옵션으로 컴파일함.
$ gcc -S -o simple.asm simple.c
- 이렇게 만들어지는 어셈블리 코드는 컴파일러의 버전에 따라 다르게 생성됨.
- main()함수가 위, function()함수가 아래에 자리잡고 있음.
main() 함수의 시작점: 0x08048460
- EIP는 main()의 시작점을 가리킴.
- ESP는 스택의 맨 꼭대기를 가리킴.
- 이전에 수행하던 함수의 데이터를 보존하기 위해 ebp를 저장함. (base pointer)
- 함수가 시작될 때에는 stack pointer와 base pointer를 새로 지정하는데 이러한 과정을 함수 프롤로그 과정이라고 함.
- push %ebp를 수행하여 이전 함수의 base pointer를 저장하면 stack pointer는 4바이트 아래를 가리킴.
- mov %esp, %ebp를 수행하여 ESP값을 EBP에 복사함으로써 함수의 base pointer와 stack pointer가 같은 지점을 가리키게 됨.
- sub $0x8, %esp를 수행하여 ESP에서 8을 뺌으로써 ESP는 8바이트 아래 지점을 가리키게 되고 스택에 8바이트의 공간이 생김. (8바이트 확장)
- and $0xfffffff0, %esp로 ESP와 11111111 11111111 11111111 11110000의 AND 연산을 수행함. (ESP 주소 값의 맨 뒤 4비트를 0으로 만듦)
- mov $0x0, %eax로 EAX 레지스터에 0을 넣음.
- sub %eax, %esp로 ESP에 들어 있는 값에서 EAX에 들어 있는 값 만큼 뺌.
- sub $0x4, %esp로 스택을 4바이트 확장.
- ESP는 12바이트 이동한 상태
- push $0x03, push $0x02, push $0x01을 수행하여 function()에 인자값 1, 2, 3을 차례로 넣어줌.
- call 0x0804843b로 function 명령 수행
- 함수 수행이 끝나면 다음으로 수행할 명령을 스택에서 pop하여 알 수 있게 됨. 이것이 buffer overflow에서 가장 중요한 return address.
- EIP에는 function 함수가 있는 0x0804843b 주소값이 들어감.
- EIP는 function()함수가 시작되는 지점을 가리키고 있고 스택에는 main()함수에서 넣었던 값들이 차곡차곡 쌓여있음.
- push %ebp, mov %esp, %ebp 실행
- function()함수에서도 마찬가지로 함수 프롤로그가 수행됨.
- main()함수에서 사용하던 사용하던 base pointer가 저장되고 stack pointer를 function()함수의 base pointer로 삼음.
- sub $0x28, %esp로 스택을 40바이트 확장.
- 40바이트가 된 이유는 simple.c의 function()함수에서 지역 변수로 buffer1[15]와 buffer2[10]을 선언했기 때문. (buffer1[15]에 16바이트, buffer2[10]에 16바이트, dummy 8바이트)
- function함수의 인자는 base pointer와 return address 위에 존재함.
- 보통 mov $0x41, [$esp -4], mov $0x42, [$esp -8]과 같은 형식으로 ESP를 기준으로 스택의 특정 지점에 데이터를 복사해 넣는 방식으로 동작함.
- leave instruction(함수 프롤로그 작업을 되돌림)을 수행함.
- stack pointer를 이전의 base pointer로 잡아서 function() 함수에서 확장했던 스택 공간을 없애버리고 PUSH해서 저장해 두었던 main()함수의 base pointer를 복원함.
- POP을 했으므로 stack pointer는 1word 위로 올라가고 stack pointer는 return address가 있는 지점을 가리킴.
- ret를 수행하고 나면 return address는 POP되어 EIP에 저장되고 stack pointer는 1 word 위로 올라감.
- add $0x10, %esp는 스택을 16바이트 줄임.
- leave, ret를 수행하게 되면 각 레지스터들의 값은 main()함수 프롤로그 작업을 되돌리고 이전으로 돌아가게 함.
'SYSTEM > 개념 정리' 카테고리의 다른 글
[Dream hack] Linux Exploitation & Mitigation Part 1 中 Return Address Overwrite & NOP Sled (0) | 2021.03.04 |
---|---|
[Dream hack] Memory Corruption - C (I) 中 스택 버퍼 오버플로우 (0) | 2021.03.04 |
BOF 정리 (2) (0) | 2021.02.24 |
리눅스 기초 명령어(2) (0) | 2021.02.19 |
리눅스 기초 명령어 (0) | 2021.02.15 |