[시스템해킹] 01. 리눅스 메모리 구조

본격적으로 시스템 해킹을 배우려면, 그전에 필수적으로 알고 있어야 하는 Computer Science(CS) 지식들이 몇몇 있습니다. 이러한 CS 지식들을 모른다면 앞으로의 설명을 100% 이해하기는 어렵습니다.

 

하지만 많은 분들이 실습에 흥미를 가지기 때문에 이론 공부를 생략하는 경향이 있고, 어느 순간 막혀버리면서 포기해 버리는 모습들을 많이 봐왔습니다. 여러분들은 "꼭!" 바닥부터 차근차근 쌓아나가서 중간에 포기하지 않으시길 바라겠습니다!! 

 

아무튼 서론은 짧게 끝내도록 하고, 이번 글에서는 그 배경 지식 중 하나인 메모리 구조에 대해 알아보겠습니다.

 

Linux Memory Layout

일반적으로 리눅스 메모리 구조를 설명할 때 세그먼트(segment)섹션(section)이라는 용어를 사용합니다.

 

이 둘은 분명 다른 개념이지만, 비슷한 의미를 지녀서 많은 사람들이 혼용하더라고요. 이번 글에서는 세그먼트와 섹션이라는 단어를 구분해서 사용할 텐데, 만약 헷갈리신다면 일단은 둘 다 "영역"이라는 개념으로 받아들이셔도 괜찮습니다.

 

일단 리눅스에서 실행되는 프로그램의 전체적인 Memory Layout은 아래와 같습니다.

 

 

(1) CODE Segment

CODE Segment는 .text section이 존재하는 세그먼트로, "현재 실행하고자 하는 파일의 코드"가 저장된 부분입니다. 특정 프로그램을 실행시켰을 때, 컴퓨터가 어떤 동작을 해야 하는지에 대한 정보가 담겨있는 영역이라고 보시면 돼요.

 

아래와 같이 C언어로 작성된 간단한 프로그램을 실행하는 상황을 가정해 봅시다.

// 컴파일 : gcc -o test test.c 
#include<stdio.h>
int main(void){
    printf("Welcome to System Hacking");
    return 0;
}

제가 위 프로그램을 컴파일하게 되면 test 라는 실행파일이 하나 생깁니다. 해당 파일을 실행하게 되면 컴파일된 코드(Assembly Code)가 메모리에 올라가게 되는데, 이때 해당하는 Assembly Code가 CODE segment에 위치하게 됩니다. 그 후, 운영체제는 CODE segment에 있는 코드를 한 줄씩 실행하면서 Welcome to System Hacking 이라는 문자열을 출력하고 프로그램을 종료시키게 됩니다.

 

CODE segment는 일반적으로 읽기 권한과 실행 권한이 부여됩니다. 그 이유를 한번 생각해 보면, 운영체제가 프로그램을 실행하기 위해 Assembly Code를 읽어야 하기 때문에 읽기 권한이 부여되고, 이를 곧바로 실행해야 하기 때문에 실행 권한이 부여되는 것입니다. 메모리에 로드된 이후에 실행할 코드를 변경할 일은 없기 때문에 일반적으로 write 권한은 부여되지 않습니다.

 

(2) Initialized Data Segment

Initialized Data Segment는 프로그램을 실행할 때 사용되는 데이터 중 "이미 초기화된 전역변수"가 저장되는 영역입니다. 내부를 더 세부적으로 분류해 본다면 (a) 읽기 권한만 부여되는 .rodata section과 (b) 읽기 및 쓰기 권한이 모두 부여되는 .data section으로 나눌 수 있습니다.

이번에도 예시 코드를 통해 살펴보도록 하겠습니다.

// 컴파일 : gcc -o test test.c 
#include<stdio.h>
int value = 10;
const char name[16] = "1nteger_c";
int main(void){
    printf("Welcome to 'System Hacking'");
    return 0;
}

위와 같은 코드가 있을 때, initialized data segment에 들어갈 값은 총 3가지입니다.

  1. value (10)
  2. name ("1nteger_c")
  3. printf에서 출력할 문자열 ("Welcome to System Hacking")

일반적으로 프로그램이 정상적으로 실행된다면 printf 내부에 있는 문자열이 수정될 이유는 전혀 없습니다. 이처럼, 코드 내부에서 사용하는 문자열들은 주로 읽기 권한만을 가진 영역인 .rodata section에 포함됩니다.

다음으로 name이라는 문자열을 보면 const, 즉 상수로 정의되어 있기 때문에 해당 변수 또한 수정될 수 없습니다. 이처럼 const 형으로 정의된 모든 값들도 .rodata section에 저장됩니다.

마지막으로 전역변수로 저장된 value는 현재는 비록 10이라는 값으로 초기화되어 있지만, 프로그램 내부에서 바뀔지 안 바뀔지 모르는 값입니다. 위에서 제시된 코드에서는 비록 value의 값을 바꾸지 않았지만, 중요한 것은 "해당 프로그램이 로드될 때는 그 사실을 알지 못한다"는 점입니다. 그러므로 const로 정의되지 않은 전역변수들은 read / write 권한을 가진 .data section에 저장됩니다.

 

(3) Uninitialized Data Segment

Uninitialized Data Segment에는 .bss section이 포함되는 곳으로, "초기화되지 않은 전역변수"가 저장되는 영역입니다.

위에서 사용한 예시를 조금만 수정해서 가져와보도록 하겠습니다.

// 컴파일 : gcc -o test test.c 
#include<stdio.h>
int value = 10;
const char name[16] = "1nteger_c";
unsigned int score;
char secret[24];
int main(void){
    printf("Welcome to 'System Hacking'");
    return 0;
}

앞서 설명했던 "초기화되지 않은 전역변수"를 위 코드에서 찾아보면 총 2가지가 있습니다.

  1. score
  2. secret

두 변수들은  정의는 되어있지만 값을 따로 선언해주지 않았기 때문에, .bss section에 "0으로 초기화되어서" 저장됩니다. 즉, score 변수는 0으로 초기화되어 있고 secret이라는 문자열도 모두 0으로 가득 찬 문자열로 초기화되어 있어요.

이러한 전역변수들이 저장되는 .bss section은 당연히 읽기 및 쓰기 권한이 존재하며, 굳이 실행 권한이 필요 없기 때문에 실행 권한은 존재하지 않는 영역입니다.

 

(4) HEAP Segment

HEAP Segment의 경우 "동적 할당된 메모리"가 저장되는 영역입니다. 

 

C언어를 이용하여 프로그래밍을 해본 분들이라면, malloc 이라는 함수를 사용해 보신 경험이 분명 있을 거예요. 메모리를 동적 할당하는 함수들은 stdlib.h 라는 헤더파일에 정의되어 있는데, malloc 뿐만 아니라 calloc realloc 함수가 있습니다. 해당 함수들을 이용하면 (a) 프로그램 실행 도중에 (b) 원하는 크기만큼 메모리를 할당할 수 있습니다.

 

간단한 예시를 통해 알아보도록 하겠습니다.

// 컴파일 : gcc -o test test.c 
#include<stdio,h>
#include<stdlib.h>
int main(void){
    char * mem1 = malloc(0x20);
    free(mem1);
    return 0;
}

위와 같이 프로그램 실행 도중에 malloc 함수가 실행되면, OS는 HEAP 영역에 일부 영역을 할당해 줍니다. 위 코드에서는 malloc(0x20) 이 실행되면서 0x20 만큼의 데이터가 저장될 수 있도록 영역을 만들어서 mem1이 사용할 수 있게 해 줍니다. 해당 메모리는 동적으로 할당한 메모리이기 때문에 더 이상 사용할 일이 없다면 free 함수를 호출해서 메모리 할당을 해제해 줍니다.

free를 호출한다고 해서 실제 HEAP 영역 자체가 사라지지는 않지만, 개발자 입장에서 본다면 메모리는 한정되어 있기 때문에 free를 꼭 호출해 주는 것이 좋아요 :)

HEAP 영역에 정확히 어떻게 데이터가 할당되는지 궁금할 텐데, 이에 대해서는 추후 Heap Exploit을 진행할 때 알아보도록 하겠습니다(지금 알려드리기엔 너무 복잡한 내용이라서 넘어가겠습니다 ㅎㅎ). 지금으로서는 HEAP 영역이 할당되기 시작하는 부분이 있고, 계속 할당할 때마다 아래에(높은 주소 방향) 할당된다는 사실만 알고 계시면 될 것 같아요. 가장 처음 나왔던 Memory Layout 그림에서 아래 방향으로 grow 화살표가 있는 것이 바로 이 의미입니다.

HEAP 영역도 일반적으로 .bss section과 동일하게 읽기 및 쓰기 권한이 존재하고, 실행 권한은 존재하지 않습니다.

 

(5) STACK Segment

자, 드디어 마지막인 STACK segment입니다. STACK에는 다양한 값들이 저장되는데, 가장 대표적으로 지역변수가 저장됩니다. 프로그램이 실행하는 과정에서 "임시적으로" 값을 저장해야 할 때가 많은데, 그 모든 과정이 일어나는 곳이라고 보면 될 것 같아요.

// 컴파일 : gcc -o test test.c 
#include<stdio,h>
int main(void){
   int x = 0;
   int y = 1;
   int z = x + y;
   printf("x : %d, y : %d, z : %d", x, y, z);
   return 0;
}

위의 코드에서 보면 main 함수 내에서 x, y, z라는 "지역변수"가 정의된 것을 볼 수 있는데, 이 값들이 모두 stack 영역에 저장됩니다. 뿐만 아니라 printf처럼 함수를 호출할 때 "매개변수를 스택에 저장하는 경우"가 종종 있습니다. 제가 "종종"이라는 단어를 사용한 이유는 스택에 저장할 때도 있고 아닐 때도 있어서인데, 이에 대해서는 다음 강의에서 설명드리도록 할게요. 뿐만 아니라 스택의 구조를 유지하기 위해 "스택 프레임"이라는 것도 저장되는데, 이 또한 다음 강의에서 설명드리도록 하겠습니다.

 

가장 처음에 보여드렸던 그림을 보면, stack 영역은 grow 화살표를 위쪽 방향으로 표시해 두었습니다. Heap 영역과 반대로 stack은 가장 끝 메모리 주소가 정해져 있고, 점점 낮은 메모리 방향으로 데이터를 저장하는 형태를 띱니다. 자료구조에서 배우는 스택을 연상하시면 이해하기 쉬울 거예요.

 

정리하자면 stack 영역에는 지역변수, 함수 호출 시 필요한 매개변수, 그리고 스택 프레임 등이 저장되는 영역이라고 알고 있으면 될 것 같습니다. stack도 앞서 설명드린 영역들과 동일하게 읽기와 쓰기 권한만 존재하는 영역이에요.

정리

정리하자면 Linux 프로그램의 메모리는 크게 5가지 종류의 세그먼트로 분류할 수 있습니다.

  1. CODE
  2. Initialized Data
  3. Uninitialized Data
  4. Heap
  5. Stack

물론 실행 중인 프로그램의 메모리 layout을 실제로 접근해 보면 이것보다 더욱 다양한 영역들이 있지만 그것들은 차차 알아보는 것으로 하고, 다음 글에서는 Linux가 stack을 어떻게 사용하는지에 대해 살펴보도록 하겠습니다.

 

 

부록

(1) Segment vs Section

위에서 segment와 section이라는 용어를 많이 사용했는데, 둘의 가장 큰 차이점은 "언제 구분하는 영역인가"입니다.

Section은 실행되기 전, 프로그램이 빌드된 상태에서 각 영역을 분류하기 위한 용어입니다.

반면, segment는 프로그램의 실행 과정에서 영역을 분류하는 용도라고 보시면 됩니다. Segment는 운영체제를 배울 때 등장하는 segmentation에서 따온 용어인데, 궁금하신 분들은 한번 찾아보시길 바랍니다.

 

두 용어의 차이를 간략하게 이해했다면, 위에서 Heap과 Stack을 설명할 때는 왜 section이라는 용어를 사용하지 않았는지 이해할 수 있습니다. Heap과 Stack은 프로그램이 실행되기 전에는 존재하지 않은 영역이고, 프로그램이 실행될 때 OS로부터 할당되는 영역이기 때문이죠.

 

(2) 각 영역의 permission

각 영역은 메모리에 로드되는 과정에서 read, write, execute 권한을 부여받게 됩니다.

조금만 더 자세히 설명드리자면, 각 영역에 부여될 권한은 ELF 파일의 앞쪽에 정의되어 있습니다. ELF 파일의 앞쪽에는 Program Header가 존재하는데, 이곳에는 각 영역(TEXT / DATA / STACK 등)에 어떤 권한(permission)을 부여할지 저장되어 있어요. 리눅스 파일의 권한을 표기하는 법과 동일하게 read를 4, write를 2, execute를 1로 보고 0 ~ 7 사이의 정수로 권한을 표현합니다.

 

<TMI>

제가 위에서 각 영역의 권한에 대해 이야기할 때 "일반적으로"라는 단어를 많이 사용했는데, 그 이유는 사용자 마음대로 각 영역의 권한을 변경할 수 있기 때문이에요. 만약 Program Header에 있는 실행 권한 부분의 값을 바꾼다면 각 영역의 권한이 바뀌겠죠? 하지만 프로그램이 메모리에 로드되기 전에 바꾸어야 적용된다는 점 주의하시길 바랍니다~

'강좌' 카테고리의 다른 글

[시스템해킹] 00. 시작하기 전  (0) 2023.08.23
해킹 강좌 소개글  (2) 2023.08.20
  Comments,     Trackbacks