Step 187. 보호 기법: NX, ASLR, Canary, PIE — 사중 방어막의 지도

Step 187. 보호 기법: NX, ASLR, Canary, PIE — 사중 방어막의 지도

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

전제: Step 186을 마쳤다. 무방비 바이너리에서 RET 덮어쓰기 공격을 성공시켜 봤고, 그때 우리가 끈 옵션 세 개(-fno-stack-protector, -z execstack, -no-pie)를 기억한다.

⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.

  • 준비물: WSL 우분투 터미널, gcc, gdb, readelf, Step 186의 vuln.c. 실측 환경은 Ubuntu 24.04, gcc 13.3.0, x86-64입니다.
  • 주의: 오늘은 [개념] 챕터지만 개념을 눈으로 검증합니다. 같은 소스를 보호 옵션별로 컴파일해 Step 186의 공격이 어느 문에서 막히는지 직접 봅니다. ASLR을 끄는 실험에는 시스템 설정이 아닌 프로세스 속성(setarch -R)만 씁니다.

Step 186에서 우리는 방어막 세 겹을 일부러 벗기고 공격했습니다. 오늘은 그 방어막의 정체를 하나씩 만납니다. 현대의 바이너리는 기본적으로 사중 방어 — NX, Canary, ASLR, PIE — 를 두르고 나옵니다. 각각이 Step 186 공격의 어느 고리를 끊는지, 그래서 공격자들이 어떤 우회 개념을 만들었는지를 정리하는 날입니다. 공격의 첫걸음은 언제나 "이 바이너리에 무엇이 켜져 있는가"를 읽는 것입니다.


1. 학습 목표

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

  • NX, Canary, ASLR, PIE 네 보호 기법의 동작 원리를 각각 한 문장으로 설명한다
  • readelf와 nm으로 바이너리에 켜진 보호 기법을 직접 판별한다
  • 같은 소스를 옵션별로 컴파일해 차이를 관찰하고 기록한다
  • Step 186의 공격이 각 보호 기법 앞에서 어떻게 실패하는지 실험한다
  • 각 보호 기법에 대응하는 우회 개념(ROP, leak)을 개념 수준에서 말한다

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

오늘의 도구 한눈에 보기

구분 내용
언어·환경 C 언어, WSL 우분투 bash, gcc 13.3.0, gdb 15.1, binutils(readelf·nm)
오늘의 명령어·옵션 readelf -h(ELF 타입 — PIE 판별), readelf -lW | grep GNU_STACK(NX 판별), nm | grep stack_chk(카나리 판별), setarch -R(이 프로세스만 ASLR 끄기), 컴파일 옵션 -fstack-protector-strong, -no-pie, -z execstack
필요한 개념 NX 비트, 스택 카나리, 주소 공간 무작위화(ASLR), 위치 독립 코드(PIE), 정보 유출(leak), ROP

2-1. NX — 스택에서 실행 금지

NX(No eXecute)는 "데이터 영역의 바이트는 실행될 수 없다"는 하드웨어·OS 수준의 규칙입니다. 스택에 쉘코드(실행할 기계어)를 심고 그쪽으로 점프하는 고전 공격이 이 한 장치로 불가능해집니다 — 스택은 읽고 쓸 수는 있어도(RW) 실행은 안 되니까요.

공격자의 대응이 ROP(Return-Oriented Programming) 입니다. 새 코드를 심을 수 없다면, 프로그램 안에 이미 있는 코드 조각(gadget)들의 주소를 스택에 나열해 ret 사슬처럼 이어 붙입니다. NX가 "새 요리 금지"라면 ROP는 "냉장고 속 재료만으로 요리하기"입니다.

2-2. Canary — 덮기를 탐지하는 감시 값

스택 카나리(stack canary)는 컴파일러가 버퍼와 RET 사이에 심어 두는 난수 감시 값입니다. 함수가 끝날 때 이 값이 바뀌어 있으면 "넘쳤다"고 판단하고 프로그램을 스스로 중단시킵니다. Step 62에서 만난 *<strong> stack smashing detected </strong>*의 정체입니다.

덮어쓰기가 RET에 닿으려면 카나리를 반드시 밟고 지나가야 하므로, 공격은 "성공" 대신 "안전한 죽음"이 됩니다. 우회 개념은 leak(유출) — 카나리 값을 다른 취약점(예: 포맷 스트링)으로 먼저 읽어 낸 뒤, 그 값을 그대로 포함해 덮는 것입니다.

2-3. ASLR — 주소를 섞는 운영체제

ASLR(Address Space Layout Randomization)은 실행할 때마다 스택·힙·라이브러리의 주소를 무작위로 섞는 OS의 장치입니다. Step 186 공격의 전제는 "win의 주소를 안다"였는데, 스택 주소가 매번 바뀌면 스택에 심은 것으로 점프하기가 어려워집니다.

우회 개념 역시 leak입니다. 실행 중인 프로그램이 주소를 한 번이라도 출력해 주면(정보 유출 취약점), 섞인 지도의 현재 좌표를 알게 되니까요. 그래서 실전 익스플로잇은 "유출 → 계산 → 덮기"의 두 단계로 이뤄지는 경우가 많습니다.

2-4. PIE — 코드까지 섞는다

PIE(Position-Independent Executable)는 프로그램의 코드 자체가 매번 다른 주소에 올라가게 하는 컴파일 방식입니다. ASLR이 스택·라이브러리를 섞는다면, PIE는 내 코드까지 섞습니다. Step 186의 nm으로 읽은 win의 주소 0x401196은 PIE 바이너리에서는 더 이상 유효하지 않습니다 — 매번 배이스가 바뀌니까요.

우회 개념은 상대 오프셋입니다. PIE 바이너리 안에서 함수 간의 거리(오프셋)는 컴파일 시 고정됩니다. 어떤 함수의 현재 주소 하나만 유출되면, 거리 계산으로 win의 현재 주소가 나옵니다. "지도가 통째로 움직여도 지도 안의 거리는 변하지 않는다."

2-5. 판별법 — 공격은 정찰에서 시작된다

실전에서는 checksec이라는 도구가 이 넷을 한 번에 보여 줍니다(Step 188에서 pwntools와 함께 만납니다). 오늘은 그 내부 원리 — 각 항목을 readelf와 nm으로 손수 확인하는 법 — 을 배웁니다. 도구가 보여 주는 것의 근거를 알아야 도구 없이도 판별할 수 있습니다.


3. 따라 하기

3-1. 네 개의 바이너리 — 같은 소스, 다른 방어

Step 186의 vuln.c를 네 가지 옵션으로 컴파일합니다.

gcc -g -O0 -fno-stack-protector -z execstack -no-pie vuln.c -o vuln_alloff
gcc -g -O0 vuln.c -o vuln_default
gcc -g -O0 -fstack-protector-strong -no-pie vuln.c -o vuln_canary
gcc -g -O0 -fno-stack-protector vuln.c -o vuln_pie

읽는 법: 넷의 차이를 표로 정리하면 이렇습니다.

바이너리 NX Canary PIE
vuln_alloff 꺼짐 (-z execstack) 꺼짐 꺼짐 (-no-pie)
vuln_default 켜짐 켜짐 켜짐 (우분투 gcc 기본값)
vuln_canary 켜짐 켜짐 꺼짐
vuln_pie 켜짐 꺼짐 켜짐

-fstack-protector-strong은 카나리를 명시적으로 켜는 옵션이고, 네 번째는 카나리만 끄고 나머지는 기본값(PIE 포함)에 맡깁니다.

3-2. 판별 실습 — readelf와 nm

PIE 판별 — ELF 헤더의 Type을 봅니다.

readelf -h vuln_alloff | grep Type
readelf -h vuln_default | grep Type
  Type:                              EXEC (Executable file)
  Type:                              DYN (Position-Independent Executable file)

(2026-09-09 실측.)

읽는 법: EXEC는 주소가 고정된 전통적 실행 파일(PIE 꺼짐), DYN + "Position-Independent" 표시가 PIE입니다. 같은 글자 DYN을 공유 라이브러리도 쓰므로 괄호 안 설명까지 읽어야 합니다.

NX 판별 — 프로그램 헤더의 GNU_STACK 권한을 봅니다.

readelf -lW vuln_alloff | grep GNU_STACK
readelf -lW vuln_default | grep GNU_STACK
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RWE 0x10
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10

(2026-09-09 실측.)

읽는 법: RWE — 마지막 E가 "실행 가능". -z execstack으로 만든 바이너리의 스택은 실행까지 허용됩니다. RW만 있으면 NX가 켜진 것입니다. 한 글자 차이가 쉘코드 공격의 성사 여부입니다.

카나리 판별 — 심볼표에 감시 장치의 흔적이 있는지 봅니다.

nm vuln_alloff | grep stack_chk    # (결과 없음)
nm vuln_canary | grep stack_chk
                 U __stack_chk_fail

(2026-09-09 실측.)

읽는 법: __stack_chk_fail — 카나리 검사 실패 시 불리는 함수입니다. 이 심볼이 링크돼 있다(U는 외부에서 가져온다는 뜻)는 것은 컴파일러가 카나리 검사 코드를 심었다는 뜻입니다.

3-3. 카나리의 실물 — 스택에 심어진 감시 값

카나리 켜진 바이너리를 gdb로 열어 그 감시 값을 직접 봅니다.

gdb -q ./vuln_canary
(gdb) b vuln
(gdb) r
(gdb) disas vuln
   0x00000000004011eb <+12>:	mov    %fs:0x28,%rax
   0x00000000004011f4 <+21>:	mov    %rax,-0x8(%rbp)
   ...
   0x0000000000401274 <+149>:	mov    -0x8(%rbp),%rax
   ...
   0x0000000000401281 <+162>:	je     0x401288 <vuln+169>
   0x0000000000401283 <+164>:	call   0x401090 <__stack_chk_fail@plt>
   0x0000000000401288 <+169>:	leave
   0x0000000000401289 <+170>:	ret

(2026-09-09 실측.)

출력 읽는 법: 컴파일러가 심은 경비의 전체 동선이 보입니다.

  • +12: %fs:0x28 — OS가 프로세스마다 만들어 두는 난수 저장소에서 감시 값을 꺼내 옵니다.
  • +21: 그 값을 -0x8(%rbp) — 버퍼와 saved rbp 사이 — 에 심습니다.
  • +149~+164: 함수 끝에서 그 자리의 값을 다시 읽어 원본과 비교하고, 다르면 __stack_chk_fail을 부릅니다.

몇 걸음 진행 후 실제 값을 보면:

(gdb) x/gx $rbp-8
0x7fffffffe658:	0x11d39c12fef0ef00

(2026-09-09 실측. 값은 실행마다 바뀌는 난수입니다.)

읽는 법: 끝이 00으로 끝나는 것을 보세요. 우연이 아닙니다 — gets 같은 문자열 입력 함수는 0x00 바이트(널 문자)에서 쓰기를 멈추는 경우가 많아, 첫 바이트를 0으로 해 두면 "카나리를 그대로 유지한 채 덮기"가 어렵게 됩니다. 방어 설계의 섬세함이 한 바이트에 담겨 있습니다.

3-4. 공격 재현 — 카나리 앞에서

Step 186의 공격(패딩 24 + win 주소)을 카나리 켜진 바이너리에 그대로 넣습니다.

python3 -c 'import sys; sys.stdout.buffer.write(b"A"*24 + b"\x96\x11\x40\x00\x00\x00\x00\x00" + b"\n")' | ./vuln_canary
buf 주소: 0x7ffc885aef10
입력: *** stack smashing detected ***: terminated
받은 값: AAAAAAAAAAAAAAAAAAAAAAAA@
Aborted (core dumped)

(2026-09-09 실측. 종료 코드 134.)

출력 읽는 법: win은 실행되지 않았습니다. 넘친 입력이 카나리를 밟았고, 함수 끝의 검사가 그것을 발견해 프로그램을 중단시켰습니다. Step 186의 공격이 첫 번째 문에서 막혔습니다. FLAG 대신 Aborted — "안전하게 죽는" 프로그램의 모습입니다.

3-5. 공격 재현 — PIE 앞에서

이번엔 PIE만 켜진 바이너리에, 0x401196이라는 고정 주소로 공격합니다.

nm vuln_pie | grep " win"
00000000000011a9 T win

(2026-09-09 실측.)

읽는 법: 주소가 0x40만 대가 아니라 0x11a9 — 이것은 완성된 주소가 아니라 파일 안에서의 오프셋입니다. PIE 바이너리는 실행될 때 랜덤한 배이스 주소에 통째로 올라가고, win의 실제 주소는 "배이스 + 0x11a9"가 됩니다. 배이스는 실행마다 바뀝니다.

python3 -c 'import sys; sys.stdout.buffer.write(b"A"*24 + b"\x96\x11\x40\x00\x00\x00\x00\x00" + b"\n")' | ./vuln_pie
buf 주소: 0x7ffcd55db6d0
입력: 받은 값: AAAAAAAAAAAAAAAAAAAAAAAA@
Segmentation fault (core dumped)

(2026-09-09 실측.)

출력 읽는 법: FLAG가 없습니다. 카나리가 없으니 덮어쓰기 자체는 성공했지만, RET에 쓴 0x401196은 이 실행에서는 아무것도 아닌 주소였습니다. 덮기는 되는데 갈 곳의 주소를 모르는 상태 — 이것이 PIE가 만드는 상황입니다.

3-6. ASLR 관찰 — 매번 바뀌는 지도

같은 프로그램을 세 번 실행해 buf의 주소만 봅니다.

echo A | ./vuln_default | head -1   (×3)
buf 주소: 0x7ffc93280190
buf 주소: 0x7ffcf317f320
buf 주소: 0x7ffec50675f0

(2026-09-09 실측.)

같은 프로그램, 같은 입력인데 스택 주소가 매번 다릅니다. 이것이 ASLR입니다. 실험을 위해 이 프로세스만 ASLR을 끄면:

echo A | setarch -R ./vuln_default | head -1   (×3)
buf 주소: 0x7fffffffe670
buf 주소: 0x7fffffffe670
buf 주소: 0x7fffffffe670

(2026-09-09 실측.)

읽는 법: setarch -R은 시스템 설정을 바꾸는 게 아니라 "이 실행만" 무작위화를 끄는 프로세스 속성입니다. 실험할 때마다 주소가 달라지면 관찰이 어려우니 측정용으로 씁니다. 세 번 모두 같은 주소 — 지도가 고정됐습니다. 실전 공격자에게 이 고정은 절대 주어지지 않습니다. 그래서 leak이 필요한 것입니다.

3-7. 종합 — 사중 방어막의 교차점

오늘 실험을 표로 결산합니다 (2026-09-09 실측 종합).

보호 기법 확인 명령 켜진 모습 Step 186 공격을 끊는 고리 우회 개념
NX readelf -lW → GNU_STACK RW (E 없음) 스택에 심은 코드의 "실행" ROP (기존 코드 조각 재활용)
Canary nm__stack_chk_fail 심볼 있음 덮기의 "탐지" — ret 전에 중단 카나리 값 leak 후 그대로 포함
ASLR 반복 실행해 주소 비교 매번 다름 스택·라이브러리 "주소를 모름" 주소 leak
PIE readelf -h → Type DYN 내 코드의 "주소를 모름" 오프셋 + 배이스 leak

네 장치가 노리는 것은 결국 하나 — RET를 덮어도, 덮인 값을 몰라도, 덮는 즉시 탐지돼도, rip 탈취가 무용해지게 만드는 것 입니다.


4. 미션과 연습문제

미션 — 보호 기법 판별 리포트

  1. Step 186의 vuln.c를 다섯 번 컴파일한다: 모두 끔 / 카나리만 켬 / PIE만 켬 / 모두 켬(기본) / -z execstack만 추가
  2. 다섯 바이너리 각각에 대해 세 가지를 판별해 표로 만든다: Type(readelf -h), GNU_STACK 권한(readelf -lW), __stack_chk_fail 유무(nm)
  3. Step 186의 공격 페이로드를 카나리만 켠 바이너리PIE만 켠 바이너리에 각각 넣고, 결과(메시지, 종료 코드)를 기록한다
  4. 두 실패의 차이를 한 문장씩 쓴다 — "카나리는 ___ 때문에 막았고, PIE는 ___ 때문에 막았다"
  5. vuln_default를 setarch -R 있이/없이 세 번씩 실행해 buf 주소를 기록하고, ASLR의 효과를 숫자로 보인다
  6. 마지막에 답한다: "공격자가 이 넷을 모두 뚫으려면 어떤 정보/기술이 필요한가" — 개념 수준에서 세 줄 이상

연습문제

문제 1. NX가 켜진 바이너리에서 고전적 쉘코드 공격(스택에 기계어를 심고 점프)이 실패하는 이유를 메모리 권한으로 설명해 보세요.

문제 2. 카나리의 첫 바이트가 0x00인 이유는 무엇인가요? 어떤 종류의 입력 함수에 대한 방어인가요?

문제 3. PIE 바이너리에서 nm이 보여 주는 0x11a9 같은 값은 완성된 주소가 아닙니다. 그럼 무엇이며, win의 실제 주소는 실행 중에 어떻게 계산되나요?

문제 4. "유출(leak) 없는 익스플로잇"이 ASLR+PIE 조합에서 어려운 이유를, Step 186 공격의 전제("win의 주소를 안다")와 연결해 설명해 보세요.


5. 모범 답안과 완료 기준

미션 모범 답안

판별 리포트의 예 (2026-09-09, Ubuntu 24.04, gcc 13.3.0 기준):

[판별 표]
            Type   GNU_STACK   __stack_chk_fail
alloff      EXEC   RWE         없음
canary만    EXEC   RW          있음
PIE만       DYN    RW          없음
기본(모두)  DYN    RW          있음
execstack   DYN    RWE         있음

[공격 실험]
canary만:  *** stack smashing detected ***: terminated / Aborted (종료 코드 134)
PIE만:     Segmentation fault (종료 코드 139) — FLAG 출력 없음

[한 문장 결론]
카나리는 "덮는 행위를 탐지해 ret 전에 실행을 중단했기" 때문에 막았고,
PIE는 "덮은 주소(0x401196)가 이 실행에서는 존재하지 않는 주소였기" 때문에 막았다.

[ASLR 숫자]
일반 실행: 0x7ffc93280190 / 0x7ffcf317f320 / 0x7ffec50675f0 (전부 다름)
setarch -R: 0x7fffffffe670 ×3 (전부 같음)

[최종 답변 예]
카나리 값과 유효한 주소(배이스)를 leak할 수 있는 별도 취약점이 필요하고,
NX 때문에 실행할 코드를 새로 심을 수 없으므로 ROP로 기존 코드 조각을
이어 붙이는 기술이 필요하다. 즉 현대 익스플로잇은 "정보 유출 + 주소 계산
+ ROP 사슬"의 조합이 된다.

검증하는 법: ① 판별 표의 다섯 행이 실제 명령 출력에서 나왔는가. ② 두 공격 실패의 메시지와 종료 코드가 구분돼 있는가(134 = 카나리, 139 = 세그폴트). ③ ASLR 실험의 주소 여섯 개가 기록됐는가.

연습문제 해답

문제 1 해답. NX는 스택 메모리의 권한을 읽기·쓰기(RW)로 제한하고 실행(X)을 뺍니다. 쉘코드는 스택에 "쓸" 수는 있지만 ret로 그곳에 점프해 "실행"하려는 순간 CPU가 거부해 세그폴트가 납니다. 데이터와 코드의 경계를 하드웨어가 지키는 것입니다.

문제 2 해답. gets·strcpy·scanf %s 같은 문자열 기반 입력 함수는 0x00(널 문자)을 문자열의 끝으로 인식해 쓰기를 멈추거나 포함시키지 못하는 경우가 많습니다. 카나리의 첫 바이트가 0이면, 공격자가 카나리를 "그대로 유지하며" 덮어 넘기려 해도 첫 바이트부터 쓸 수 없어 우회가 어려워집니다. 문자열 함수류 공격에 대한 1차 방어 설계입니다.

문제 3 해답. 파일 안에서의 오프셋(상대 위치)입니다. PIE 바이너리는 실행될 때 OS가 정한 랜덤 배이스 주소에 통째로 올라가고, 실행 중 win의 실제 주소는 "배이스 + 0x11a9"가 됩니다. 오프셋은 고정이지만 배이스가 매번 바뀌므로, 배이스를 모르면 절대 주소를 알 수 없습니다.

문제 4 해답. Step 186 공격의 절반은 "덮는 기술"이었고 나머지 절반은 "덮을 값(0x401196)을 아는 것"이었습니다. ASLR과 PIE는 바로 그 "값을 아는 것"을 무력화합니다 — 매번 주소가 바뀌니 정적인 분석(nm)으로 얻은 주소가 실행 시점에는 무효가 됩니다. 그래서 실행 중인 프로그램이 주소를 흘리는 정보 유출 취약점이 필요하고, 현대 pwn 문제가 대부분 "유출 + 덮기" 2단 구성인 이유입니다.

완료 기준 체크리스트

  • [ ] NX, Canary, ASLR, PIE를 각각 한 문장으로 설명할 수 있다
  • [ ] readelf -h의 Type(EXEC/DYN)으로 PIE를 판별할 수 있다
  • [ ] GNU_STACK의 RW/RWE로 NX를 판별할 수 있다
  • [ ] nm의 __stack_chk_fail 심볼로 카나리를 판별할 수 있다
  • [ ] 카나리가 심어지고 검사되는 어셈블리 동선을 disas로 확인했다
  • [ ] Step 186의 공격이 카나리와 PIE 앞에서 각각 다르게 실패하는 것을 관찰했다
  • [ ] setarch -R로 ASLR의 효과를 숫자로 대조했다
  • [ ] 미션: 다섯 바이너리 판별 리포트를 완성했다

6. 흔한 실수와 해결

벽 1. 기본 컴파일인데 카나리가 없다고 나온다

증상: 옵션 없이 컴파일했는데 nm에 __stack_chk_fail이 없습니다.
원인: gcc의 기본 카나리(-fstack-protector-strong)는 "위험하다고 판단한 함수"에만 심습니다. 배열이 없는 함수에는 심지 않습니다.
해결: 판별 실험은 buf 배열과 gets가 있는 vuln.c처럼 명백히 위험한 함수로 하세요. 그래도 없다면 -fstack-protector-all로 모든 함수에 심게 할 수 있습니다.

벽 2. DYN인데 PIE가 아니라고?

증상: readelf Type이 DYN이라 PIE라고 적었는데 헷갈립니다.
원인: 공유 라이브러리(.so)도 Type은 DYN입니다. Type만으로는 부족합니다.
해결: 괄호 안 설명을 읽으세요 — DYN (Position-Independent Executable file)이면 PIE 실행 파일, DYN (Shared object file)이면 라이브러리입니다 (2026-09-09 실측 출력 기준).

벽 3. setarch -R인데도 주소가 다르다

증상: setarch -R로 실행했는데 책의 주소(0x7fffffffe670)와 다릅니다.
원인: ASLR이 꺼져도 환경 변수 개수, 경로 길이, gdb 실행 여부에 따라 스택 시작점이 조금씩 밀립니다. gdb 안에서는 gdb 자체가 환경을 바꿉니다.
해결: 절대값이 아니라 반복 실행 간의 일치를 보는 것이 이 실험의 포인트입니다. 내 환경에서 세 번 같으면 됩니다.

벽 4. 카나리 공격인데 stack smashing이 안 뜨고 세그폴트가 난다

증상: 카나리 바이너리에 공격했는데 Aborted가 아니라 Segmentation fault입니다.
원인: 입력이 카나리를 건너뛰고 다른 것(포인터 변수 등)을 덮었거나, 페이로드가 카나리까지 안 닿았는데 다른 경로로 죽었을 수 있습니다. 컴파일러가 변수 순서를 바꾸는 경우도 있습니다.
해결: gdb로 멈춰 x/gx $rbp-8로 카나리 자리와 내 페이로드의 도달 범위를 확인하세요. 어떤 죽음이든 현장을 읽으면 원인이 보입니다.

벽 5. 보호 기법 이름과 컴파일 옵션이 뒤섞인다

증상: -no-pie, -z execstack, -fno-stack-protector 중 뭐가 뭘 끄는지 혼란스럽습니다.
원인: 셋이 서로 다른 계층(링커 옵션, 코드 생성 옵션)의 스위치라 이름 규칙이 다릅니다.
해결: 이렇게만 연결해 두세요. -fno-stack-protector → 카나리 끄기, -z execstack → NX 끄기, -no-pie → PIE 끄기. "끄는 스위치는 실험실 전용, 배포용은 기본값"이 원칙입니다.


7. 정리

오늘의 개념

개념 한 줄 설명
NX 스택은 읽고 쓰되 실행은 안 된다 — 쉘코드 봉쇄, ROP의 탄생 배경
Canary 버퍼와 RET 사이의 난수 감시 값 — 덮으면 ret 전에 자폭
ASLR 스택·힙·라이브러리 주소를 매 실행마다 섞는 OS 장치
PIE 내 코드조차 랜덤 배이스에 올라간다 — 오프셋만 고정
leak 실행 중 주소·값을 흘려 무작위화를 무력화하는 우회의 출발점
ROP 새 코드 없이 기존 코드 조각(gadget)을 이어 붙이는 공격 기법

오늘의 명령어

명령 하는 일
readelf -h 바이너리 ELF 타입 확인 — PIE 판별 (EXEC/DYN)
readelf -lW 바이너리 GNU_STACK 권한 확인 — NX 판별 (RW/RWE)
nm 바이너리 | grep stack_chk 카나리 심볼 확인
setarch -R ./프로그램 이 실행만 ASLR 끄기 (측정용)
gcc -fstack-protector-strong 카나리 명시적 켜기

명령어보다 중요한 감각

오늘의 결산은 냉정해야 합니다. 네 방어막은 공격을 "불가능"하게 하지 않습니다. "비싸게" 만듭니다. 카나리는 leak을 요구하고, ASLR과 PIE는 주소 유출을 요구하고, NX는 ROP라는 정교한 수공예를 요구합니다. 방어의 승리 조건은 완벽이 아니라 공격자의 비용을 끌어올리는 것 — 이 관점이 보안 설계 전체를 관통하는 사고방식입니다.

그리고 공격자의 첫걸음을 기억하세요. 코드를 분석하기 전에 바이너리의 방어막부터 읽습니다. "무엇이 켜져 있는가"가 곧 "어떤 전략이 필요한가"이기 때문입니다. 다음 챕터에서 만날 pwntools의 checksec이 바로 그 정찰을 한 줄로 해 주는 도구이고, 오늘 여러분은 그 한 줄의 근거를 전부 손으로 확인한 것입니다.


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