[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는 존재하지도 않는 인자를 스택에서 순서대로 읽어와 출력한다. 즉 공격자가 스택에 쌓인 임의의 값을 하나씩 읽어낼 수 있다.
%x: 스택 값을 16진수로 읽기 (정보 유출 — 스택 카나리, 리턴 주소, 라이브러리 주소 leak에 사용)%s: 스택 값을 주소로 취급해서 그 주소가 가리키는 문자열을 읽기%n: 그 시점까지 출력한 바이트 수를 지정한 주소에 써넣는다 — 즉 임의 주소에 임의 값을 쓸 수 있다는 뜻이다. 이게 이 취약점을 "읽기"에서 "쓰기(=코드 실행)"까지 확장시키는 지점이다.입력: %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으로 GOT(Global Offset Table) 엔트리를 원하는 함수 주소로 덮어써서, 특정 함수가 호출될 때 공격자가 원하는 코드가 실행되게 만드는 게 전형적인 마무리다. 정확히 원하는 바이트 수만큼 출력을 맞춰야 해서(%N$n에서 N은 원하는 값에 맞춰 패딩 조정), 실제로는 fmtstr_payload() 같은 pwntools 헬퍼로 자동 계산한다.
payload = fmtstr_payload(offset, {got_addr: target_func_addr})
printf("%s", input)처럼 항상 고정 포맷 + 인자 형태를 지킨다.-Wformat -Wformat-security 경고로 이런 패턴을 잡아준다. 빌드 시점에 경고를 에러로 취급하도록 설정해두면 근본적으로 막을 수 있다.