I’ve been playing around with a selection of fun reversing tooling recently and thought it might be fun to go through a basic challenge to escalate privileges via a vulnerable executable.
The first tool we’re going to use is
gdb but we’re going to extend its capabilities significantly with
pwndbg. The former might well already be installed on your distro, but if not it’ll be in a repo and this should be trivial to do. The latter you get from here: https://github.com/pwndbg/pwndbg. You’ll see just how much of an upgrade this is shortly.
First up, we’re going to create a vulnerable binary to play with.
Dealing with ASLR and other security defences is beyond the scope of this post, so we’ll be making a few changes to ensure all of this works on your machine.
First disable ASLR:
echo 0 > /proc/sys/kernel/randomize_va_space
Then compile our super vulnerable binary:
gcc vuln-32.c -o vuln-32 -no-pie -fno-stack-protector -m32
Finally, we’re going to set the SUID bit on the file. This allows the file to execute with the permissions of the file owner. You’ll want root to own it for our demo.
chmod +s vuln-32
We now have our executable, and it’s time to have a first look at it through our swanky new
If you’ve set everything up right, this is what you’ll see. We’re going to set a breakpoint on
main and then begin execution. To do that you’d do the following:
If you’ve ever used “naked” gdb, you’ll immediately notice how great this interface is. You can see the call to the
vuln function at
0x80491bb. Let’s take a look at that using
There’s nothing especially complex going on here. You can see the call to
0x0804919a which is where we will get the opportunity to overflow the buffer. Remember the aim of the game here is to get control of a register called the Instruction Pointer. In 32-bit land, like this is, the register is called
eip and in 64-bit land this is called
rip. We can’t just change that value directly, but if you understand a bit about x86, you’ll see how we might manipulate it in other ways. Let’s set a new breakpoint right at the beginning of the function,
0x08049172, and then use
c to continue our program.
You can see here that the instruction pointer is now where we set our breakpoint, and the address at the top of the stack (
0x080491c0. Go back and look at the the main function. You might notice something? Thats the address that our program wants to come back to when it completes this function. The stack will continue to grow during this function, but when its completed all of its work, the
ret command will finally pop this return address off of the stack and into
eip. Therefore what we need to do is overwrite that address on the stack, so that when the
ret command does pop it off the stack, it’s the address we want it to be – not the one the program wants.
Because we’ve disabled a bunch of security mechanisms here, this is pretty easy for us. First we want to find out how much we need to put into the buffer to overflow into the
eip. More fun
pwngdb tools ahead:
cyclic which will create a de Bruijn pattern that allows us to rapidly find that.
The “Invalid address” that we get here, you’ll note is what is in the
eip. It’s invalid because we’ve overwritten it with garbage – a memory address that it cannot get to. That garbage is really useful for us to know though. Time to use
cyclic with the
How cool is that? We instantly know the offset for the buffer overflow. No heavy lifting required! Thats all we’ll be using
gdb for today – now we’re going to find out some things about this binary and libc to both get a shell, and elevate permissions. At a high level what we are going to do commandeer control of the Instruction Pointer, and hand it to the
setuid function of the libc library, before we will follow up by sending the string
Step 1: The super vulnerable executable we created is dynamically linked, and we can use the built in
ldd command to tell us some information about the dependencies it needs to operate, and specifically we can use it to tell us the memory address of libc:
The address given for
libc.so.6 is called the libc base. You would want to make a note of that.
Step 2: Get the offset inside libc for
system using the
readelf command with the
The two hex addresses in the second column are the offsets for the functions we want to call. Note these down too. Step 3: Finally we want to get
system to call
/bin/sh and luckily that string can be found in libc too.
We use a CTF favourite
strings to establish where we can find where that string exists in memory. If you are familiar with C you’ll be aware of the use of pointers to strings, rather than literal strings – and thats what we’re going to be doing here. Getting the location of the string ‘/bin/sh’ and passing it to
Thats the last address you need to note down before build our exploit. We’ll build that right now…
pwntools here to assist us with this very basic ROP chain. You’ll see in a moment this works ‘by accident’ and would generate some nasty evidence in event logs. From our exploit code, you’ll note that we define our memory addresses first before putting them to use. Our chain then starts with 76 repetitions of the character “A” which take us to the edge of the Instruction Pointer – remember that was the offset we found earlier.
We then insert the address of the
setuid function, and as you can see we’re passing it the argument ‘
0x0‘ to give us root (
system is set as the return address). It is at this point we get lucky, as upon return from that function this ‘
0x0‘ entry on the stack is now what the program expects to be the return address for the upcoming
system function call. It doesn’t matter, because we (probably) don’t care about what happens to the program after we get our shell. Its worth looking at what’s happening; when we wrestle control of the IP, the stack looks like this:
<system> (return address)
<0x0> (argument for setuid)
<address of /bin/sh>
…but once the
setuid function returns it looks like this:
<0x0> (return address)
<address of /bin/sh> (argument for system)
Our code absolutely works as you can see here – we have a shell as root, but it also crashes with a thud upon exit. An exit code of -11 (SIGSEGV) which is a Segmention Fault/Violation in the screenshot indicates that all was not well, and this could be noticed by a vigilant sysadmin. You can read about this type of seg fault here – the key point being that the program tried to access an invalid memory location (
So how do we fix that? This is where we need to start using ROP properly. Firstly we’re going to have to be a lot more deliberate about our stack layout, we’ll need to manage the stack ourselves, and we need to use a proper exit function. I’m also going to, unnecessarily, add a
setgid into the mix to demonstrate how we could keep calling almost limitless functions.
I grabbed the memory address for
exit from libc the same way we did with
setuid earlier, and added those into our exploit. There was another address I needed as well, referred to as a pop-ret gadget. We’re going to use it a couple of times during the course of our exploit. To get this memory location we need another great tool called
ropper which you can obtain from here: https://github.com/sashs/Ropper.
In the example above you can see I search for a
pop of any kind, followed immediately by a
ret. So what exactly are we going to use this for? The purpose of our pop-ret gadget is to pop the argument we placed onto the stack earlier, off of it which will realign our frame ready for the next function call. By placing this gadget immediately after each function call that expects a single argument to be passed to it, we can keep control of the Instruction Pointer.
The following is the full exploit:
So now in this much improved version, instead of relying on ‘happy little accidents’ as Bob Ross might say, we now are in full control of the execution. In contrast to version 1, now when
setuid function call returns, the pop-ret cleans off the
0x0 we sent and aligns the pointer onto
setgid function call which then receives
0x0 as an argument. On return from that, we run the pop-ret again and the argument is again cleaned up. The pointer moves on to the
system function call. Hello root access. This time though if we exit from the shell the program calls our safe exit memory address. So the question is, what happens if we run this? I’m glad you asked…
From the screenshot you can see that we got exit code 0. In other words a perfectly normal exit that won’t draw unwanted administrator attention.
Admittedly this is very much a whistle-stop tour, and one I’ve been meaning to publish for over a month but keep getting distracted. I might delve deeper into this at another time, but for now,