[Pwn] Pwnable · 작성: 2026-07-19 19:28:48 · 조회 2
Pwnable(바이너리 익스플로잇)을 시작하기 전에 반드시 먼저 알아야 하는 게 프로그램이 메모리를 어떻게 쓰는지다. 이 개념 없이 익스플로잇 코드를 따라 치기만 하면 뭘 하고 있는지 이해가 안 된다. 이 글은 그 전제 지식을 정리한다.
프로그램이 실행되면 메모리는 대략 이렇게 나뉜다 (주소가 낮은 곳 → 높은 곳 순).
낮은 주소
+------------------+
| Code (Text) | 실행할 명령어들 — 보통 읽기 전용
+------------------+
| Data / BSS | 전역/정적 변수
+------------------+
| Heap | malloc()으로 동적 할당한 메모리, 낮은 주소 → 높은 주소로 자람
| ↓ |
| ↑ |
| Stack | 함수 호출마다 쌓이는 지역 변수/리턴 주소, 높은 주소 → 낮은 주소로 자람
+------------------+
높은 주소
힙과 스택이 서로를 향해 자라는 구조라서, 옛날 방식으로는 이 둘이 충돌하는 것도 하나의 공격 아이디어였다 (지금은 여러 보호 기법으로 막혀있다).
레지스터는 CPU 안에 있는 아주 빠른 저장 공간이다. 익스플로잇에서 자주 보게 되는 것들만 추려보면:
| 레지스터 | 역할 |
|---|---|
RIP |
다음에 실행할 명령어의 주소 (Instruction Pointer). 이걸 공격자가 원하는 값으로 바꾸는 게 대부분의 익스플로잇의 최종 목표다. |
RSP |
스택의 맨 위(top) 주소 (Stack Pointer) |
RBP |
현재 함수의 스택 프레임 시작 주소 (Base Pointer) |
RDI, RSI, RDX, RCX |
함수 호출 시 첫 4개 인자를 순서대로 담는 레지스터 (x86-64 System V 호출 규약) |
RAX |
함수의 리턴값, 시스템콜 번호 |
"RDI에 원하는 문자열 주소를 넣고 system 함수를 호출한다" 같은 표현이 이후 글들에 계속 나오는데, 이게 바로 함수 호출 규약(calling convention)을 이용해서 함수에 원하는 인자를 넘겨주는 방법이다.
함수가 호출되면 스택에 다음이 순서대로 쌓인다.
[ 지역 변수들 (buf 등) ] <- 낮은 주소
[ saved RBP ]
[ 리턴 주소 ] <- 함수가 끝나면 RIP가 여기로 복원됨
[ 인자들 (일부는 레지스터로) ] <- 높은 주소
함수가 ret 명령어로 끝날 때, CPU는 스택에 저장된 "리턴 주소"를 읽어서 그 주소로 실행 흐름을 옮긴다. 이 리턴 주소가 담긴 스택 영역을 덮어쓸 수 있다면, 함수가 끝나는 순간 실행 흐름을 원하는 곳으로 보낼 수 있다 — 이게 스택 버퍼 오버플로우의 핵심 아이디어이고, 다음 글에서 바로 다룬다.
pip install pwntools
checksec ./binary
다음 글부터는 이 개념들을 바탕으로 실제 취약점 하나씩 들어간다.