메모리 공간은 일반적으로 Code, Data, Stack, Heap 등의 4가지의 세그먼트로 분류된다. 세그먼트 방식은 메모리의 물리적인 주소를 논리적인 주소를 사용하여 분할한다. 그래서 세그먼트의 크기가 서로 다를 수 있다.
특징
장점
크기가 다를 수 있기 때문에 메모리를 미리 분할 하지 않음 메모리가 나누어지기 때문에 메모리 보호에 좋음, 즉 메모리가 나누어지기 때문에 다른 세그먼트에 접근이 제한됨.
단점
주소 매핑과 메모리 관리가 복잡함, 필요한 만큼 공간이 쓰이니 내부 단편화는 발생하지 않지만 쓰고 버리고를 하다보면 서로다른 빈 공간이 생길 수 있어서 외부 단편화는 발생이 가능하다.
Code/text
절대 변경되지 않는 것들이 저장
- 실행 가능한 명령어가 포함된 오브젝트 파일 또는 메모리 공간을 할당받은 프로그램 섹션 중의 하나.
- 명령어를 변경하지 못하도록 읽기 전용인 경우가 많다. 상수도 저장
Data(Initialized Data Segnment)
초기화된 전역변수, 정적 변수
- 초기화된 전역 변수와 static(정적) 변수를 포함한다.
BSS(Uninitialized Data Segment)
초기화되지 않은 전역 변수, 초기화되지 않은 정적 변수
- 이렇게 분리되는 이유는 초기화되지 않은 전역 변수, 정적 변수는 초기값이 없으니 0으로 초기화된다. 그래서 초기화되지 않은 변수들의 메모리 공간을 할당하여 모아둬서 메모리를 효율적으로 관리한다.
Stack
지역변수, 매개변수
- Stack은 후입선출의 구조를 가지고 있으며 메모리 상위 주소에 위치한다. 함수를 호출 할 때 지역변수, 매개변수들이 저장되는 공간이다.(main 함수도 포함)
Heap
동적 할당
- Heap은 동적 메모리 할당이 수행되는 세그먼트 공간이다. (malloc, free 등이 쓰이는 곳)
스택은 힙에 가까이 위치하며 서로 반대 방향으로 데이터를 저장한다. 스택이 높은 주소에서 낮은 주소로 향하는 이유: 만약 스택이 낮은 주소에서 높은 주소로 쌓이다 끝을 넘어가면 그 뒤에는 커널 영역이 있는데 이 영역이 손상되면 엄청난 문제가 된다.
라이브러리는 동적 링킹을 할 경우에만 저 부분에 위치하고 정적 링킹을 하면 전부다 code 영역에 들어간다.
kernel
시스템 운영에 필요한 메모리로 운영체제가 올려져 있다. 사용자는 함부로 접근할 수 없다.
GDB
gdb란 오픈소스로 공개되어있는 무료 디버거이다. 코드에서 어떤 값이 어떤 주소로 올라가는지 등의 과정을 보여준다.
예제코드를 이용해서 gdb를 사용해보자.
// gcc -fno-stack-protector -o exam1 exam1.c
#include <string.h>
#include <stdio.h>
void func2() {
puts("func2()");
}
void sum(int a, int b) {
printf("sum : %d\n", a+b);
func2();
}
int main(int argc, char *argv[]) {
int num=0;
char arr[10];
sum(1,2);
strcpy(arr,argv[1]);
printf("arr: %s\n", arr);
if(num==1){
system("/bin/sh");
}
return 0;
}
여기서 -fno-stack-protector 옵션은 스택 보호를 위해 canary라는 것을 삽입하는데 이 보호기법을 해제한 것이다. canary는 이후에 설명하겠다.
이제 exam1을 debugging 해보자.
이러면 exam1을 debugging 할 준비가 끝났다. 어셈블리어를 확인해보자.
disassemble <함수이름> 으로 어셈블리어를 확인할 수 있다.
main에 breakpoint를 걸고 실행을 해보자.
b *<메모리 주소> 로 원하는 곳에 breakpoint를 걸 수 있다.
r로 실행을 시킬 수 있다.
breakpoint를 확인하고 싶다면
info b
로 확인할 수 있다.
이렇게 breakpoint가 걸린 부분의 주소를 확인 할 수 있다.
만약 breakpoint를 삭제하고 싶다면
d <break point>
이렇게 하면 된다.
자 이제 실행을 해보자.
함수 인자로 main의 매개변수가 들어가니 이렇게 aaaaaaaa를 인자로 넣어줄 수 있다.
현재의 실행흐름을 하나하나 확인해보자.
main에 멈춰있다.
여기서 인스트럭션을 한줄 한줄 실행해 보자.
ni
로 다음 인스트럭션을 실행할 수 있다.
ni를 3번 실행해보자. 그러면 다음과 같아진다.
여기서 => 가 가리키는 곳의 직전에 rsp값을 rbp에 저장하고 있다. 이 레지스터는 스택과 관련된 레지스터인데 그 메모리에 어떤 값이 담겨있는지 gdb로 확인해보자.
info reg로 레지스터값을 볼 수 있다.
rsp가 가리키는 곳의 메모리를 확인해보았다. 여기서 x(첫 번째)로 값을 확인할 수 있고. g는 8byte 단위로 보여준다는 것을 의미하고 x(두 번째)로 16진법으로 볼 수 있다. 아래의 옵션 표를 참고하자.
옵션
o : 8진법으로 보여줌
x : 16진법으로 보여줌
u : 10진법으로 보여줌
t : 2진법으로 보여줌
b : 1 byte 단위로 보여줌(byte)
h : 2 byte 단위로 보여줌(half word)
w: 4 byte 단위로 보여줌(word)
g : 8 byte 단위로 보여줌(giant)
i : 역어셈블된 명령어의 명령 메모리를 볼수 있음
c : ASCII 표의 바이트를 자동으로 볼 수 있다.
s : 문자 데이터의 전체 문자열을 보여준다.
위 옵션을 조합하여 사용할 수 있다.
여기서 우리는 Byte Ordering에 대해서 알아야 한다.
Intel CPU는 바이트르 배열할 때 거꾸로 쓰게 된다. 이를 Little Endian이라고 한다. 하지만 gdb는 보기 편하게 하기 위해 원래 순서로 보여준다. 이를 Big Endian이라 한다.
그럼 STACK에 대해 알아보자.
stack에 관한 레지스터는 3가지가 있다.
EBP : 스택프레임에서 시작 지점 주소가 저장된다.
ESP : 스택프레임에서 스택의 끝 지점 주소가 저장된다.
EIP : 현재 실행중인 명령어의 주소가 저장된다.
스택 프레임은 이후에 설명하겠다.
스택의 시작
스택의 시작은 아래와 같다.
push rbp
mov rbp, rsp
이로 인해 스택프레임이 형성된다.
이 두 줄의 의미는 함수가 실행될 때 이전의 rbp를 stack에 push하고 현재 rsp를 rbp에 저장한다. 이로인해 새로운 rbp가 생성되고 새로운 스택 프레임이 생성된다.
한번 sum에 brekapoint를 걸고 실행해 보자. breakpoint 가 걸린 상황에서 다음 breakpoint까지 실행시키려면 c를 누르면 된다.
call을 하면 다음 인스트럭션의 주소를 스택에 쌓고 간다. 그리고 sum안으로 들어가면 rbp의 값을 스택에 쌓는다.
이는 sum에 들어가기 직전이다.
sum에 들어가고 mov rbp, rsp를 하고나면 stack은 아래와 같다.
RSP가 가리키는 곳 위에 sum이 종료된 후 실행될 명령어 주소가 있고 그 위에 sum이 실행되기 전의 rbp 0x7777777e1c0이 쌓여있다.
스택프레임이 끝날때는 Leave와 Ret을 한다.
leave와 ret이 실행된 후를 보자.
다시 원래대로 돌아왔고 RIP는 sum이후의 명령어를 가리킨다.
'포너블 > Stack' 카테고리의 다른 글
BoF, Shellcode, Nx bit (1) | 2023.10.23 |
---|---|
핸드레이 (0) | 2023.10.17 |
스택 프레임 (0) | 2023.10.17 |
함수호출규약 (0) | 2023.10.14 |
K0n9의 heap부터 시작하는 줄 알았으나 stack을 시작해버린 이야기 (0) | 2023.10.14 |