Running your own code
In real exploits, it's not particularly likely that you will have a win()
function lying around - shellcode is a way to run your own instructions, giving you the ability to run arbitrary commands on the system.
Shellcode is essentially assembly instructions, except we input them into the binary; once we input it, we overwrite the return pointer to hijack code execution and point at our own instructions!
I promise you can trust me but you should never ever run shellcode without knowing what it does. Pwntools is safe and has almost all the shellcode you will ever need.
The reason shellcode is successful is that Von Neumann architecture (the architecture used in most computers today) does not differentiate between data and instructions - it doesn't matter where or what you tell it to run, it will attempt to run it. Therefore, even though our input is data, the computer doesn't know that - and we can use that to our advantage.
ASLR is a security technique, and while it is not specifically designed to combat shellcode, it involves randomising certain aspects of memory (we will talk about it in much more detail later). This randomisation can make shellcode exploits like the one we're about to do more less reliable, so we'll be disabling it for now using this.
Again, you should never run commands if you don't know what they do
Let's debug vuln()
using radare2
and work out where in memory the buffer starts; this is where we want to point the return pointer to.
This value that gets printed out is a local variable - due to its size, it's fairly likely to be the buffer. Let's set a breakpoint just after gets()
and find the exact address.
It appears to be at 0xffffcfd4
; if we run the binary multiple times, it should remain where it is (if it doesn't, make sure ASLR is disabled!).
Now we need to calculate the padding until the return pointer. We'll use the De Bruijn sequence as explained in the previous blog post.
The padding is 312 bytes.
In order for the shellcode to be correct, we're going to set context.binary
to our binary; this grabs stuff like the arch, OS and bits and enables pwntools to provide us with working shellcode.
We can use just process()
because once context.binary
is set it is assumed to use that process
Now we can use pwntools' awesome shellcode functionality to make it incredibly simple.
Yup, that's it. Now let's send it off and use p.interactive()
, which enables us to communicate to the shell.
If you're getting an EOFError
, print out the shellcode and try to find it in memory - the stack address may be wrong
And it works! Awesome.
We injected shellcode, a series of assembly instructions, when prompted for input
We then hijacked code execution by overwriting the saved return pointer on the stack and modified it to point to our shellcode
Once the return pointer got popped into EIP, it pointed at our shellcode
This caused the program to execute our instructions, giving us (in this case) a shell for arbitrary command execution