Adventures in Binary Exploitation: Part 3

Past blogs in this series: Introduction | Part 1 | Part 2

Many, many months ago on the blog with buffer overflows and exploits, but basically we had to switch the difficulty level to easy to achieve it. I want to end this topic on the blog to concentrate on other things, so I’m finally coming back to it. This time around we’ll turn on a pretty major mitigation against the techniques we’ve been using: ASLR. Is this a bridge too far for me? Let’s find out...

I've cloned the binary from part2 and put it in another folder so I have somewhere clean to work in. Then we'll need to run the opposite line to the one we ran in Part 1:
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space

Getting around the problem

We’ve been hopping around memory like some RAM-obsessed bunny rabbit, running whatever functions we want because everything was nicely laid out in memory. With ASLR this isn’t the case. Libraries are loaded into memory with a randomised starting address on each execution. Any attempts to jump to a particular function (or ROP gadget) in somewhere like libc will likely fail as something different will be there instead. We could stick with ROP gadgets in our binary, but there may not be enough gadgets to do exactly what we need.

We’ll need to find some way of bypassing this, and luckily there are several. I’m going to talk about a few of them in this blog - some of which will cover things we’ve already used as you’ll see later.

Information Leaks

Clearly if the memory locations of libraries are randomised each time then we’ll need to figure out where things have been placed in real time as we exploit the executable. The usual trick for this is format string vulnerabilities. These are where you can use a badly formatted call to any of the printf functions to read values off the stack (and do a whole lot more besides). It’s not really an area I know much about, so we’ll skip them for now. I need to go and read things. :)

PLT? GOT?

Put our shellcode somewhere else

Another trick we can use if there are insufficient functions we can use in our binary via jumps or ROP gadgets AND if NX is turned on is to look for an area of memory to store some shellcode in, use some functions to actually copy the code into that space and then finally jump to it. You do need to be able to do a syscall for this, so you’ll need to have sufficient gadgets available to populate the various needed registers and perform the syscall. Alternatively if the full functions you need are in the PLT - as read() frequently is - you can do it that way too. As we've seen, PLT entries don't change.

There are a few functions we’ll need:
  • mprotect() - to make an area of memory executable, writable, and readable.
  • read() - to read our code from stdin (provided by us via the exploit) and copy it to our chosen area of memory
The good news is that both of these can be done with a syscall, so in the worst case scenario we don’t need the full functions in a library or binary, just the gadgets to set some parameters and do the syscall, but the PLT is there too. You've probably guessed that this is the plan here because I've written so much on it...

Our binary has enough gadgets to do some things, but not others. There's nothing that can set %rax for a start, so syscalls are going to be tricky. Both calls need 3 parameters, so we’ll set registers according to the calling conventions we’ve seen earlier. For the read() syscall %rax needs to be set to 0, and I found this gadget that uses %eax (the lower 32 bits of %rax) and puts 0 in it. This is done just before vuln() finishes running and returns a value of 0 to main(), the calling function. So we can use that instead as a single-shot ROP gadget - but only if we need a zero. But read() is in the PLT, so... But mprotect() isn’t. Shit.

We can use our ”pop rdi” gadget from last time, 0x400693 to populate %rdi.

There's this for %rsi, but it also has a pop %r15 in it. That's ok, we don't need that register. But it does mean that we need to give the register some junk data, so we'll remember to do this in our script later.


The last thing we need to do for both read() and mprotect() is to set %rdx. For read() we need to set %rdx to a number of bytes that we want to read in. I couldn’t find a “pop rdx” unfortunately, but thanks to a great blog post - but the URL for which I now can’t find - I discovered that puts() can set rdx as a side-effect. Perhaps if we set up a call to that first before we set up the other registers we can get a value higher than the number of bytes we want to copy into our shellcode buffer. Not sure what we’ll do for mprotect() though.

We need to find an area of memory to put our shellcode in. If we have a quick look at how memory is laid out and the different attributes that memory areas can have (NX being one) with the “vmmap” command, we can see if there’s anywhere suitable. Annoyingly, there’s a space that’s read/execute and space that’s read/write, but because of NX the stack isn’t executable. We are definitely going to have to use the mprotect() to set one of those areas as rwx.

And yet without that method to set %rax we are pretty much fucked. And sadly, that’s where this obsession with cyber security ends for me right now. I’m not at a sufficient level to finish off exploiting this binary. There may be advanced methods but it’s beyond my understanding as yet. And my personality means I flit between hobbies like a bunny on coke. So I’m gonna have to leave it for now while I cover my next obsession. I may come back to it when I’m older and wiser. It may be that there’s no way to pwn this binary as it stands, and that I need to add in another vulnerability to play with.

Sorry for the blue balls.

Comments