앞으로 Pwnable 공격 기법에 필요한 개념과 공격 기법을 차근차근 정리해보려고 한다...❗
Stack Buffer overflow를 살펴보기 전, 알아야할 Memory Layout과 함수 호출 규약(Calling Convention)에 대해 먼저 정리해보려고 한다.
Memroy Layout과 각 영역에 대한 권한을 같이 살펴보자.
Memory Layout은 아래와 같이 총 5개로 구분할 수 있다.
1. Text Segment
2. Initialized Data Segment
3. Uninitialized Data Segment(BSS)
4. Heap
5. Stack
첫 번째로 살펴볼 구역은 Text Segment(텍스트 세그먼트)이다.
텍스트 세그먼트는 코드 세그먼트(Code Segment)라고도 불리며, 컴파일 된 프로그램의 기계어 코드를 가지고 있다.
초기에 텍스트 세그먼트는 읽기 & 쓰기 권한이 부여되었지만, 최근 OS에는 임의로 수정되는 것을 방지하기 위해 읽기 권한(Read)만 부여한다.
다음은 초기화 된 데이터 세그먼트이다.
초기화 된 데이터 세그먼트는 일반적으로 데이터 세그먼트(Data Segment)라고 불리며, 컴파일 시점에 값이 정해진 전역 변수 및 전역 상수들이 위치한다.
데이터 세그먼트는 읽기 권한(Read)만 부여되어있다.
추가로 데이터 세그먼트는 아래의 2종류로 분류할 수 있다.
data 세그먼트
: 쓰기가 가능한 세그먼트는 전역 변수와 같이 프로그램이 실행되면서 값이 변할 수 있는 데이터들이 위치한다.
rodata 세그먼트
: 쓰기가 불가능한 세그먼트로 프로그램이 실행되어도 값이 변하지 않는 데이터(전역 상수)들이 위치한다.
다음은 초기화되지 않은 데이터 세그먼트이다.
초기화되지 않은 데이터 세그먼트는 BSS영역으로 불리며, 컴파일 시점에 값이 정해지지 않은 전역 변수가 위치한다.(선언 후 초기화하지 않은 데이터들이 위치)
BSS영역은 쓰기(Write) 및 읽기 권한(Read)이 부여되어 있다.
다음은 Heap 영역이다.
실행 중에 동적으로 할당될 수 있는 영역으로 Stack과 반대 방향으로 자라난다.
Heap영역은 쓰기(Write) 및 읽기 권한(Read)이 부여되어 있다.
마지막으로 Stack영역이다.
Stack 영역은 함수의 인자나 지역 변수와 같은 임시 변수들이 실행 중에 저장되는 공간으로 Heap 공간으로 반대 방향으로 자란다.
또한 Stack은 스택 프레임(Stack Frame)이라는 단위를 사용하고, 이 스택 프레임은 함수가 호출될 때 생성되었다가 반환될 때 해제된다.
Stack영역은 일반적으로 쓰기(Write) 및 읽기 권한(Read)이 부여되었지만, 보호기법 적용 여부에 따라 읽기(Read) 권한만 부여되어 있는 경우도 있다.
Ubuntu-18.04는 x86-64 아키텍처를 사용하며, 함수 호출 규약으로 AMD64 System V 호출 규약을 따른다.
AMD64 SystmeV 함수 호출 규약은 짧게 SYSV라고 불린다.
SYSV는 함수 호출 시, 인자를 각각 차례대로 rdi ⇨ rsi ⇨ rdx ⇨ rcx ⇨ r8 ⇨ r9 레지스터들을 사용하여 전달하고 이후 인자들은 Stack을 통해 전달한다.
또한 함수의 반환값은 rax 레지스터를 사용하며 전달하고, 스택 프레임은 rsp 레지스터를 기준으로 아래쪽으로 확장된다.
// gcc -o study study.c
#include <stdio.h>
int sub_func(char *arg1, char *arg2, char *arg3, char *arg4, char *arg5, char *arg6, char *arg7);
int main(int argc, char argv[]){
int ret;
printf("[*] AMD64 System V!\n");
ret = sub_func("Arg1", "Arg2", "Arg3", "Arg4", "Arg5", "Arg6", "Arg7");
printf("[*] Sub_function return value : %d\n", ret);
return 0;
}
int sub_func(char *arg1, char *arg2, char *arg3, char *arg4, char *arg5, char *arg6, char *arg7){
printf("[*] Sub Function is Called !\n");
printf("Arg1 : %s\n", arg1);
printf("Arg2 : %s\n", arg2);
printf("Arg3 : %s\n", arg3);
printf("Arg4 : %s\n", arg4);
printf("Arg5 : %s\n", arg5);
printf("Arg6 : %s\n", arg6);
printf("Arg7 : %s\n", arg7);
return 1+2+3+4+5+6+7;
}
위의 코드를 컴파일하여, pwndbg를 통해 살펴보자.
sub_func를 호출하는 부분으로 가서, 레지스터와 RSP를 확인한 결과는 아래와 같다.
AMD64 SYSV 함수 호출 규약에 따라,
차례대로 들어간 것을 확인할 수 있다. 그리고 Arg7만 별도로 Stack을 통해 전달되는 것을 확인할 수 있다.
마지막으로 함수의 리턴값이 rax 레지스터를 통해 전달되는지 확인해보자.
RAX 레지스터에 0x1c(=28)가 전달된 것을 확인할 수 있다!
Bye 👋
※ 참고
👉 https://www.geeksforgeeks.org/memory-layout-of-c-program/
👉 http://www.tcpschool.com/c/c_memory_structure
👉 https://dystopia050119.tistory.com/34#x86-64%ED%98%B8%EC%B6%9C%20%EA%B7%9C%EC%95%BD%20%3A%20SYSV-1