BOF 기초 정리 0

hywn·2021년 8월 16일
0

BOF 기초

목록 보기
1/1
post-thumbnail

들어가기 전에

FTZ의 level9 부터는 BOF(Buffer Overflow)와 관련한 내용이 등장한다. 따라서 level9에 들어가기 앞서 BOF의 기초를 정리하는 시간을 가져볼 것이다.

와우해커 소속의 달고나님께서 작성한 문서가 BOF 기초의 바이블로 통용되는 것 같아서 해당 문서 및 구글링으로 공부하였다.

1. 8086 Memory Architecture

8086 시스템의 기본적인 메모리 구조

시스템 초기화 시작 시 커널이 메모리에 적재된다. 운영에 필요한 기본적인 명령어 집합은 커널에서 찾게되므로 커널 영역은 반드시 해당 위치에 있어야 한다. 커널은 기본적으로 64KBytes 영역에 자리 잡았지만 오늘날은 더 큰 영역을 사용한다.

1개의 프로세스가 실행되기 위한 메모리 구조

OS는 하나의 프로세스를 실행시키면 해당 프로세스를 segment(단위)로 묶어서 가용 메모리 영역에 저장한다.

시스템은 멀티 테스킹이 가능하므로 메모리에 여러개의 프로세스가 저장되어 병렬적으로 작업을 수행한다. 가용한 메모리 영역에는 여러개의 여러개의 segment들이 저장될 수 있고, 이는 실행 시점에 실제 메모리의 어느 위치에 저장될 지 결정 된다.

segment는 code segment, data segment, stack segment로 나뉜다. 시스템에는 최대 16383(21412^{14}-1)개의 segment가 크기 및 타입이 모두 다양하게 생성될 수 있다. 하나의 segment는 최대 2322^{32} bytes의 크기를 가질 수 있다.

code segment

컴파일러가 생성해낸 기계어 코드로 된 명령어(instruction)들이 들어있다.

명령어들이 명령을 수행할 시 수행하게 되는 분기와 점프의 경우 메모리 상의 특정 위치에 있는 명령을 지정해 주어야 한다. 그러나 segment는 자신이 현재(컴파일 과정) 메모리 상 어느 위치에 저장될 지 알 수 없으므로 정확한 주소를 지정할 수 없다. 따라서 segment에서는 실제 메모리 상의 주소(physical address)와 맵핑 되어있는 logical address를 사용한다.

segment는 segment selector에 의해 offset(자신의 시작 위치)을 찾을 수 있고, offset으로 부터의 위치(logical address)에 있는 명령을 수행할 지 결정하게 된다. 실제 메모리 주소인 physical address는 offset + logical address 가 된다.

physical address = offset + logical address

data segment

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

data segment는 다시 4개의 data segment로 나뉜다.

  • 현재 모듈의 data structure
  • 상위 레벨로부터 받아들이는 데이터 모듈
  • 동적 생성 데이터
  • 다른 프로그램과 공유하는 공유 데이터

stack segment

현재 수행되고 있는 handler, task, program이 저장하는 데이터 영역이다. 지역 변수들이 저장되는 공간이며, 사용하는 buffer가 stack segment에 자리잡게 된다.

프로그램이 사용하는 multiple 스택을 생성할 수 있고, 각 스택들 간의 switch 또한 가능하다.

스택은 처음 생성될 때 필요한 크기만큼 할당된다. stack pointer(sp)라고 하는 레지스터가 스택의 맨 꼭대기를 가리키고 있으며, 스택에 데이터를 저장하고 읽어 들이는 과정은 push와 pop에 의해 수행된다.

2. 8086 CPU 레지스터 구조

레지스터란?

CPU가 프로세스를 실행하기 위해서는 프로세스를 CPU에 적재시켜야 한다. 흩어져 있는 instruction set과 데이터들을 적절하게 집어내고 읽고 저장하기 위해서는 여러가지 저장 공간이 필요한데, 이들은 CPU가 재빨리 읽고 쓰기를 해야하는 것들이므로 CPU 내부에 존재하는 메모리를 사용한다. 이때 CPU 내부에 존재하는 메모리(저장 공간)을 레지스터(register)라고 한다.

레지스터의 구성

레지스터는 다시 그 목적에 따라 네 가지로 나뉜다.

  • 범용 레지스터(General purpose register): 논리 연산, 수리 연산에 사용되는 피연산자와 주소를 계산하는데 사용되는 피연산자, 그리고 메모리 포인터가 저장된다.
  • 세그먼트 레지스터(Segment register): code segment, data segment, stack segment를 가리키는 주소가 저장된다.
  • 플래그 레지스터(Program status and control register): 프로그램의 현재 상태나 조건 등을 검사하는데 사용되는 플래그들이 저장되어 있다.
  • 인스트럭션 포인터(Instruction pointer): 다음으로 수행해야하는 명령(instruction)이 존재하는 메모리 상 주소가 저장되어 있다.

범용 레지스터

프로그래머가 임의로 조작할 수 있게 허용되어있는 레지스터이다. 일종의 4개의 32bit 변수라고 생각 할 수 있다. EAX, EBX, ECX, EDX 레지스터(16bit 시절에는 AX, BX, CX, DX였으나 32bit 체제로 바뀌며 앞에 E(Extened)가 붙게 되었다.)들은 프로그래머의 필요에 따라 아무렇게나 사용할 수 있지만, 각각 원래의 목적이 있으므로 목적대로 이용하는 것이 바람직하다. 해당 4개 레지스터의 상위 부분은 끝에 H가 붙고, 하위 부분의 경우에는 끝에 L이 붙는다. (예시: AX - AH, AL)

각 레지스터의 목적을 살펴보면 다음과 같다.

  • EAX: 피연산자와 연산 결과의 저장소
  • EBX: DS segment 안의 데이터를 가리키는 포인터
  • ECX: 문자열 처리 또는 루프를 위한 카운터
  • EDX: I/O 포인터
  • ESI: DS 레지스터가 가리키고 있는 data segment 내의 어느 데이터를 가리키고 있는 포인터(문자열 처리에서 source을 가리킨다.)
  • EDI: ES 레지스터가 가리키고 있는 data segment 내의 어느 데이터를 가리키고 있는 포인터(문자열 처리에서 destination을 가리킨다.)
  • ESP: SS 레지스터가 가리키는 stack segment의 가장 꼭대기를 가리키는 포인터
  • EBP: SS 레지스터가 가리키는 stack 상의 한 데이터를 가리키는 포인터

세그먼트 레지스터

프로세스의 특정 세그먼트를 가리키는 포인터 역할을 한다. CS 레지스터는 code segment를, DS, ES, FS, GS 레지스터는 data segment를, SS 레지스터는 stack segment를 가리킨다. 세그먼트 레지스터가 가리키는 위치를 바탕으로 원하는 segment 안의 특정 데이터나 명령어를 정확하게 꺼낼 수 있게 된다.

플래그 레지스터

컨트롤 플래그 레지스터는 상태 플래그, 컨트롤 플래그, 시스템 플래그들의 집합이다. 시스템이 초기화 되면 플래그 레지스터는 0x00000002의 값을 가지며, 1, 3, 5, 15, 22~31번 비트는 예약 되어 소프트웨어에 의해 조작할 수 없게 되어있다.

각 플래그들의 역할을 살펴보면 다음과 같다.

Status Flags

  • CF(Carry Flag): 연산을 수행하며 carry 또는 borrow가 발생하면 1이 된다.
  • PF(Parity Flag): 연산 결과 최하우 바이트의 값이 짝수일 경우에 1이 된다.
  • AF(Adjust Flag): 연산 결과 carry 또는 borrow가 3bit 이상 발생할 경우 1이 된다.
  • ZF(Zero Flag): 결과가 zero 임을 나타낸다. if문 같은 조건문이 만족될 경우 set 된다.
  • SF(Sign Flag): 연산 결과의 최상위 비트의 값과 동일하다. Signed 변수의 경우 양수이면 0, 음수이면 1이 된다.
  • OF(Overflow Flag): 정수형 결과값이 너무 큰 양수이거나, 너무 작은 음수여서 피연산자의 데이터 타입에 모두 들어가지 않을 경우 1이 된다.
  • DF(Direction Flag): 문자열 처리에 있어서 DF가 1일 경우 문자열 처리 instruction이 자동으로 감소(문자열 처리가 high address에서 low address로 이루어진다.)하고, 0일 경우 자동으로 증가한다.

System Flags

  • IF(Interrupt enable Flag): 프로세서에게 mask한 interrupt에 응답할 수 있게 하려면 1을 준다.
  • TF(Trap Flag): 디버깅을 할 때 single-step을 가능하게 하려면 1을 준다.
  • IOPL(I/O Privilage Level flag): 현재 수행중인 프로세스 혹은 task의 권한 레벨을 가리킨다. 현재 수행 중인 프로세스의 권한을 가리키는 CPL(CPU의 권한 레벨)이 I/O address 영역에 접근하기 위해서는 I/O privilege level 보다 작거나 같아야 한다.
  • NT(Nested Task flag): interrupt의 chain을 제어한다. 1이 되면 이전 실행 task와 현재 task가 연결되어 있음을 나타낸다.
  • RF(Resume Flag): exeption debug를 위해 프로세서의 응답을 제어한다.
  • VM(Virtual-8086 mode flag): Virtual-8086 모드를 사용하려면 1을 준다.
  • AC(Alignment Check flag): 이 비트와 CR0 레지스터(컨트롤 레지스터 0, 운영모드를 제어하는 레지스터이며 리얼모드에서 보호모드로 전환하는 역할과 캐싱, 페이징 기능을 활성화한다.)의 AM(Alignment Mask) 비트가 set 되어 있으면 메모리 레퍼런스의 alignment checking이 가능하다.
  • VIF(Virtual Interrupt flag): IF flag의 가상 이미지이다. VIP flag와 결합시켜 사용한다.
  • VIP(Virtual Interrupt Pending flag): 인터럽트가 pending 되었음을 가리킨다.
  • ID(Identification flag): CPUID instruction을 지원하는 CPU인지 나타낸다.

Instruction Pointer

다음 실행할 명령어가 있는 현재 code segment의 offset 값을 가진다. EIP 레지스터는 소프트웨어에 의해 바로 액세스 할 수 없고 Control-Transfer Instruction(JMP, Jcc, CALL, RET)이나 interrupt와 exception에 의해서 제어된다.

EIP 레지스터를 읽을 수 있는 방법은 CALL 명령을 수행하고 나서 procedure stack 으로부터 리턴하는 명령의 address를 읽는 것이다. procedure stack의 return instruction poiner의 값을 수정하고 나서 return instruction(RET, IRET)을 수행함으로 EIP 레지스터의 값을 간접적으로 지정해 줄 수 있다.

0개의 댓글