Hacking/Pwnable

x86 메모리 구조

GunP4ng 2024. 3. 18. 23:41

x86 메모리 구조 (8086 Memory Architecture)


1. 메모리 구조

1. 8086 시스템 메모리 구조

8086 시스템 메모리 구조

인텔 32비트 시스템, 8086 시스템의 메모리 구조는 위와 같다

  • 커널 (Kernal) 영역 : OS의 시스템 코드가 로드 되는 부분이다. 건드릴 수 없는 영역이다
  • Off-Limit 영역 : 사용자가 커널 영역에 접근하지 못하도록 할당해둔 공간이다
  • 유저 영역 : 유저가 실제 사용하는 영역이다. 코드, 스택, 힙 등이 포함된다
  • Null Pointer 할당 영역 : 모두 0이고 변경 불가능하다. 시스템 보호 차원에서 만들어둔 영역이다.

위의 메모리 구조는 윈도우 기준으로, 리눅스는 커널 (Kernal) 영역이 1GB 이다.

 

운영체제는 하나의 프로세스를 실행시키면 이 프로세스를 세그먼트 (segment)라는 단위로 묶어서 유저 영역에 저장한다.

세그먼트 (segment)

유저 영역에는 여러 개의 세그먼트들이 저장될 수 있다. 세그먼트는 하나의 프로세스를 묶은 것으로 실제 실행 시점에 실제 메모리의 어느 위치에 저장될 지가 결정된다. 왜 그런지는 아래에서 자세히 설명하겠다.

세그먼트의 각 영역

세그먼트는 위의 그림처럼 스택, 공유 라이브러리, 힙, bss, 데이터, 코드 영역으로 나뉘어져 있다.

 

 

1. Code Segment (Text Segment)

1. 코드 세그먼트 (텍스트 세그먼트)

코드 세그먼트는 실제 실행되는 기계어 명령어들, 기계어 코드가 저장되는 곳이다. 프로그램이 실행되면 코드 세그먼트에 있는 기계어가 해석되며 실행된다. 이 때 명령어들은 명령을 수행하면서 많은 분기 과정과 점프, 시스템 호출 등을 수행하게 된다. 하지만 세그먼트는 자신이 현재 메모리 상에 어느 위치에 저장될지 컴파일 과정에서는 알 수 없기 때문에 정확한 주소를 지정할 수 없다.

 

따라서 세그먼트에는 논리적 주소 (Logical Address)를 사용한다. 논리적 주소는 실제 메모리 상의 주소  물리 주소 (Physical Address)와 매핑되어 있다. 세그먼트는 세그먼트 셀렉터 (segment selector)에 의해 시작 위치 (offset)를 찾을 수 있고, 논리적 주소에 있는 명령을 수행할 지를 결정하게 되는 것이다.

(세그먼트 셀렉터에 관해서는 운영체제의 메모리 관련 기법인 세그먼테이션 기법과 x86-64 관련 하드웨어를 알아야 하므로 나중에 설명하겠다)

 

실제 메모리 주소 (physical address)는 offset + logical address 라고 할 수 있다.

 

코드 세그먼트는 프로그램을 실행할 수 있어야 하므로 읽기 권한과 실행 권한이 부여된다. 쓰기 권한이 있다면 공격자가 악의적인 코드를 삽입하기 쉬워지므로, 대부분의 현대 운영체제는 쓰기 권한을 제거한다.

int main() { return 31337; }

위의 main함수가 컴파일 되면 554889e5b8697a00005dc3 라는 기계 코드로 변환된다.

이 기계 코드가 코드 세그먼트에 저장된다.

 

 

2. Data Segment

1. 데이터 세그먼트

프로그램에서 사용되는 전역변수, 정적 변수 등 각종 변수들이 저장된다. 컴파일 시점에 값이 정해진 전역 변수 및 전역 상수들이 저장된다. 즉, 초기화된 변수들이 저장된다.

 

CPU가 데이터 세그먼트의 데이터를 읽을 수 있어야 하므로, 읽기 권한이 부여된다.

 

데이터 세그먼트는 쓰기가 가능한 세그먼트와 불가능한 세그먼트로 다시 분류된다. 쓰기가 가능한 세그먼트는 전역 변수와 같이 프로그램이 실행되면서 값이 변할 수 있는 데이터들이 저장된다. data 세그먼트라고 한다.

쓰기가 불가능한 세그먼트는 프로그램이 실행되면서 값이 변하면 안되는 데이터들이 저장된다. 전역으로 선언된 상수가 포함된다. rodata (read-only data) 세그먼트라고 부른다.

 

int data_num = 31337;                       // data
char data_rwstr[] = "writable_data";        // data
const char data_rostr[] = "readonly_data";  // rodata
char *str_ptr = "readonly";  // str_ptr은 data, 문자열은 rodata

주의 깊게 봐야할 변수는 str_ptr이다. str_ptr 은 readonly 라는 문자열을 가리키고 있는데, 이 문자열은 상수 문자열로 취급되어 rodata에 저장된다. 이를 가리키는 str_ptr 은 전역 변수로 data에 저장된다.

 

 

3. BSS Segment (Block Started By Symbol Segment)

1. BSS 세그먼트

BSS 세그먼트는 컴파일 시점에 값이 정해지지 않은 전역변수가 위치하는 메모리 영역이다. 여기는 선언만 하고 초기화하지 않은 전역변수 등이 포함된다. BSS 세그먼트의 메모리 영역은 프로그램이 시작될 때 0이나 NULL로 초기화 된다. 이러한 이유로 C언어에서 초기화하지 않은 전역 변수의 값은 0이 된다.

 

데이터 세그먼트는 초기에 사용할 메모리를 확보하는 반면, BSS 세그먼트는 어느 정도의 메모리를 할당할 것인지에 대한 정보만 미리 할당해놓고 있는다. 그 다음 런타임 이후에 메모리를 확보한다. 즉, 메모리 사용면에서 BSS 세그먼트가 더 효율적이다. C언어에서는 전역변수를 선언한다면 초기화하지 않는 것이 좋다.

 

int bss_data;

int main() {
  printf("%d\n", bss_data);  // 0
  return 0;
}

초기화 되지 않은 전역변수인 bss_data 가 BSS 세그먼트에 저장된다.

 

 

4. Stack Segment

1. 스택 세그먼트

스택 세그먼트는 프로세스의 스택이 위치하는 영역이다. 함수의 인자나 지역 변수와 같은 임시 변수들이 실행 중에 스택 세그먼트에 저장된다. 스택 세그먼트는 스택 프레임 (Stack Frame) 이라는 단위로 사용된다. 스택 프레임은 함수가 호출될 때 생성되고, 반환될 때 해제된다. 하지만 전체 프로그램의 실행 흐름은 사용자의 입력을 비롯한 여러 요인에 영향을 받는다.

 

아래 코드에서 유저가 입력한 choice 에 따라 call_true() 가 호출되거나 call_false() 가 호출된다.

oid func() {
  int choice = 0;

  scanf("%d", &choice);

  if (choice)
    call_true();
  else
    call_false();

  return 0;
}

어떤 프로세스가 실행될 때, 이 프로세스가 얼마 만큼의 스택 프레임을 사용할 지를 미리 계산하는 것은 일반적으로 불가능하다. 그래서 운영체제는 프로세스를 시작할 때 작은 크기의 스택 세그먼트를 할당하고, 부족할 때마다 이를 확장해준다. 스택은 높은 주소에서 낮은 주소로 확장되기 때문에 아래로 자란다는 표현이 쓰인다.

 

스택은 LIFO (Last In First Out) 마지막에 들어온 것이 가장 먼저 나가는 구조를 가진다. 스택이 높은 주소에서 앉은 주소로 자라는 이유는 Stack 위에 있는 커널 영역을 침범하지 않기 위해서이다.

 

스택 세그먼트는 CPU가 자유롭게 값을 읽고 쓸 수 있어야 하므로, 읽기와 쓰기 권한이 부여된다.

 

위의 코드에서는 지역변수 choice 가 스택에 저장된다.

 

 

5. Heap Segment

1. 힙 세그먼트

힙 세그먼트는 힙 데이터가 저장되는 세그먼트이다. 스택과 마찬가지로 실행 중에 동적으로 할당될 수 있으며, 리눅스에서는 스택과 반대방향으로 자란다. C언어에서 malloc(), calloc() 등을 호출해서 할당받는 메모리가 힙 세그먼트에 위치하게 된다. 일반적으로 읽기와 쓰기 권한이 부여된다.

 

아래 코드는 heap_data_ptr 에 malloc() 으로 동적 할당한 영역의 주소를 대입하고, 이 영역에 값을 쓴다.

heap_dataa_ptr 은 지역 변수이므로 스택에 위치하며, malloc 으로 할당받은 힙 세그먼트의 주소를 가리킨다.

int main() {
  int *heap_data_ptr =
      malloc(sizeof(*heap_data_ptr));  // 동적 할당한 힙 영역의 주소를 가리킴
  *heap_data_ptr = 31337;              // 힙 영역에 값을 씀
  printf("%d\n", *heap_data_ptr);  // 힙 영역의 값을 사용함
  return 0;
}

힙과 스택이 서로 반대방향으로 자라는 이유는, 서로 같은 방향으로 자란다고 했을 때 힙과 스택이 서로 충돌할 위험이 있기 때문이다.

 

 

6. 공유 라이브러리 영역 

1. 공유 라이브러리 영역

프로그램이 내부에서 사용하는 라이브러리 함수들과 관련된 공유 라이브러리 파일이 적재되는 영역이다.

스택과 힙 영역 중간에 위치하고, /lib/libc.so.6 등이 있다.

나중에 다시 설명할 것이라 간단히 설명하고 넘어가겠다.

 

 

Reference


https://plummmm.tistory.com/352

https://jeongzero.oopy.io/85f4601a-42ae-44bf-b59c-2e60e055bbdf

https://dreamhack.io/lecture/courses/52

와우해커 BOF 달고나 문서