← 목록으로

Format String 취약점: printf가 위험해지는 순간

[Pwn] Pwnable · 작성: 2026-07-19 19:28:48 · 조회 6

printf(buf)처럼 사용자 입력을 포맷 문자열 인자 자리에 그대로 넣으면, 공격자가 %x, %s, %n 같은 포맷 지정자를 입력해서 스택 메모리를 읽거나 심지어 쓸 수 있다. 스택 버퍼 오버플로우만큼 유명하진 않지만, 발견되면 훨씬 강력한 취약점이다.

취약한 코드

void vuln(char *user_input) {
    printf(user_input);       // 취약: 사용자 입력이 포맷 문자열 자체가 됨
    // printf("%s", user_input);  // 안전: 포맷 문자열은 고정, 입력은 인자로만 사용
}

왜 위험한가

printf("%x %x %x")처럼 실제 넘긴 인자보다 많은 포맷 지정자를 쓰면, printf는 존재하지도 않는 인자를 스택에서 순서대로 읽어와 출력한다. 즉 공격자가 스택에 쌓인 임의의 값을 하나씩 읽어낼 수 있다.

익스플로잇 흐름 (읽기)

입력: %1$x %2$x %3$x %4$x %5$x %6$x

%N$x는 "N번째 인자를 16진수로" 라는 뜻이다. 몇 번째 위치에서 스택 카나리나 리턴 주소, libc 주소가 나오는지 하나씩 순서를 세어보며 찾는다. pwntools의 FmtStr 클래스가 이 과정을 자동화해준다.

from pwn import *

def send_payload(payload):
    p = process('./vuln')
    p.sendline(payload)
    return p.recvall()

autofmt = FmtStr(send_payload)
print(autofmt.leak)  # 자동으로 찾아낸 leak 값들

익스플로잇 흐름 (쓰기, %n)

%n으로 GOT(Global Offset Table) 엔트리를 원하는 함수 주소로 덮어써서, 특정 함수가 호출될 때 공격자가 원하는 코드가 실행되게 만드는 게 전형적인 마무리다. 정확히 원하는 바이트 수만큼 출력을 맞춰야 해서(%N$n에서 N은 원하는 값에 맞춰 패딩 조정), 실제로는 fmtstr_payload() 같은 pwntools 헬퍼로 자동 계산한다.

payload = fmtstr_payload(offset, {got_addr: target_func_addr})

방어

Pwnable 카테고리의 글 (3/14)

  1. 메모리 구조 기초: 스택, 힙, 레지스터부터 이해하기
  2. 스택 버퍼 오버플로우: 가장 기본적인 메모리 손상 공격
  3. Format String 취약점: printf가 위험해지는 순간
  4. Stack Canary: 원리와 우회 방법
  5. NX/DEP 우회와 Return-to-libc
  6. ASLR과 정보 유출(Leak)
  7. ROP (Return-Oriented Programming) 완전 정복
  8. 힙 익스플로잇 기초: Use-After-Free와 Double Free
  9. pwntools 실전 활용법
  10. UDF(User-Defined Function) 기반 DB 권한 상승: 원리부터 실전까지
  11. Pass-the-Hash / Pass-the-Ticket: 원리와 실전
  12. eBPF 기반 지속성(Persistence) 기법
  13. Full RELRO 환경에서의 GOT Overwrite와 우회
  14. ret2dlresolve: 심볼 정보 없이 임의 함수 호출하기
← 스택 버퍼 오버플로우: 가장 기본적인 메모리 손상 공격 Stack Canary: 원리와 우회 방법 →