서론
저번 글에서는 너무 배경설명이 길었다. 어차피 복습하는건데 이미 알고 있는 내용은 안적어도 될거같다. 이번에는 짧게 할거다.
그럼 시작하겠다.
https://dreamhack.io/lecture/paths/system-hacking-fundamental
리눅스의 메모리 구조
리눅스뿐만 아니라 다른 대부분의 운영체제도 마찬가지겠지만, 프로그램을 실행하면 프로세스가 생성된다. 이 프로세스는 고유의 코드, 데이터를 가지고있는데, 데이터가 들어가는 구역을 세그먼트(Segment) 라고 한다.

보면 알겠지만 총 5개의 세그먼트가 있다.
- 코드 세그먼트: 프로그램의 기계어 코드가 저장된다. 해당 세그먼트에는 r, x 권한만 주어진다
- 데이터 세그먼트: 초기화된 전역 변수 및 전역 상수가 해당 세그먼트에 저장된다. r, w 또는 r 권한만 주어진다. 여기서 두 개의 세그먼트로 다시 한번 나뉜다
- data 세그먼트: 전역 변수가 여기에 포함된다. 변경될 수 있다.
- rodata 세그먼트: Read Only의 약자로 전역 상수가 여기에 포함된다. 변결될 수 없다
- BSS 세그먼트: Block started By Symbol Segment의 약자로, 컴파일 시점에 값이 정해지지 않은, 초기화되지 않은 전역 변수가 여기에 포함된다. r, w 권한만 주어진다.
- 힙 세그먼트: C언어의 malloc 등 코드 실행중 동적으로 할당되는 데이터가 저장된다. r, w 권한만 주어진다.
- 스택 세그먼트: 함수의 매개변수나 지역 변수같은 임시 변수들이 저장된다. r, w 권한만 주어진다.
여기서 스택 세그먼트에 대해서는 말 할 내용이 있지만, 어셈블리어에서 자세히 나오기 때문에 넘어가겠다.
Assembly (1)
JAVA나 C나 결국 컴파일하면 어셈블리어로 실행된다. 어셈블리어는 메모리와 코드가 1:1로 대응되는 언어로, 쉘 코드 작성할 때나 리버스 엔지니어링 할 때나 꼭 알아야하는 언어이다.
그렇다고 이거 명령어 하나하나 소개할수는 없다. 그냥 이 사진 하나로 끝내겠다.

알기 쉬운거 말고 몇 개만 소개해보자면
- lea: C언어의 포인터라고 보면 된다. 메모리 주소를 저장한다.
- cmp: 기본적으로 sub연산이 들어간다. 피연산자 두 개가 같을 경우 ZF에 1을 저장한다.
- test: 피연산자 두 개를 and연산 한 뒤 결과는 버리고 플래그 레지스터만 갱신한다. 보통 특정 비트가 0인지 아닌지 확인하거나, 레지스터가 0인지 검사하는데 사용한다.
- j~~: 대부분 분기문이다. jmp는 무조건 분기라는것만 기억하면 될거같다.
예를 들어보겠다.
mov rax [rdx]
여기서 대괄호가 나왔다. 대괄호의 의미는 rdx에 저장되어있는 주소 ex) 0xff142a 에서 값을 꺼내온다는 뜻이다.
그니까 이 코드는 rdx에 저장되어있는 주소의 데이터를 rax에 복사하겠다는 의미이다.
메모리를 표현하는 방법은 위처럼 암시적으로 하는 방법도 있지만, 명시적으로 나타낼수도 있다.

WORD, DWORD, QWORD 그리고 BYTE가 데이터 크기를 나타낸다.
Assembly (2)
함수에 대해서 짧게 말하겠다. Subroutine, Procedure, Function라는 용어로 혼용되는 함수는 프로그램이 처리해야할 명령어를 묶어놓은 코드를 말한다.
어셈블리어에서는 이를 Label로 구역을 표시하고 call 명령어를 통해 이동한다.
자료구조중에 Stack이라고 있다. Queue와 반대로 LIFO로 구현되며, 어셈블리어에는 이를 구현한 명령어 push, pop이 있다.
push val
========
rsp -= 8
[rsp] = val
스택에서 가장 중요한건 rsp레지스터인데, 이 rsp레지스터는 항상 스택의 가장 위를 가리킨다. 스택은 가변적인 공간이기 때문에 push를 하면 rsp도 변하게 된다.
위 코드와 같이 push를 하게되면 내부적으로 스택의 윗부분에서 8만큼 내려주고, 그 위치에는 피연산자 val을 저장한다.
pop reg
=======
reg = [rsp]
rsp += 8
pop도 마찬가지로 실행이 되면 rsp가 가지고 있던 값을 rax같은 레지스터에 넣어주고, 스택을 다시 없앤다.
이 개념을 가지고 call을 구현한다
call addr
=========
push return_address
jmp addr
call은 push와 jmp 명령어로 동작하는데, 함수가 실행된 후 돌아갈 위치를 저장하면서, 동시에 해당 주소로 rip를 이동시킨다.
함수가 종료되면 leave로 만든 스택 프레임을 정리하고 ret으로 다시 돌아온다. 여기서 스택 프레임이란, 서로 다른 함수끼리 같은 메모리 구역을 사용하지 않도록 미리 만들어놓은 단위이다.
leave
-----
mov rsp, rbp
pop rbp
코드만 보면 이해가 안되는데, 간단히 설명하자면 call로 실행했던 함수에서 leave를 만나면, 스택 포인터를 다시 함수의 시작점으로 보내고, 함수를 실행하기 전의 주소를 rbp에 다시 저장해놓는다.
ret
===
pop rip
마지막으로 ret은 rip 값을 return address로 변경하는 명령어로, 이로써 함수를 종료하고 다음 코드로 넘어간다.
후기
함수 호출과 시스템 콜 내용을 넣을까 했지만, 여기까지 했으면 굳이 적을 필요 없을거라고 생각한다. 시스템 콜은 코드 보면 알 수 있는거고, 그 인자값은 어차피 외우기 힘들다. 직접 찾아보면서 익숙해지는게 낫다고 생각이 든다.
'보안 > System Hacking' 카테고리의 다른 글
| [Dream hack] 셸 코드란? (0) | 2026.04.03 |
|---|---|
| [Dream hack] GDB/PWN 정리 (0) | 2026.04.03 |
| [Dream Hack] 컴퓨터 아키텍처 기초 정리 (0) | 2026.03.16 |