How do I even pwn anything? Part 5 — Creating Reliable Shellcode

Daryl 🥖
3 min readMay 1, 2024

--

Explore how to perform Linux Binary Exploitation from Capture-the-Flag (CTF) competitions.

Click here for part 4!

Course Link

You can find all the files required for the exercises on GitHub.

Reliable Shellcode

From the last part, we learnt that we can make our own win function with Shellcode. However, jumping to Shellcode feels too random. Is there a more reliable way?

Well there is! We can use a JMP ESP gadget! What this does is instruct the instruction pointer to jump directly to the value of ESP. Any instruction placed after the gadget will be executed as code. This is because there is no distinction between instructions and data.

Of course, we may not have the luxury of having space to write the Shellcode after the JMP ESP gadget.We can solve this by injecting SUB ESP and JMP ESP instructions.

You can adjust how much to subtract ESP by based on the size of your Shellcode + JMP ESP Gadget + SUB ESP .

JMP ESP Gadget + SUB ESP

You can use the following functions from Pwntools to find the JMP ESP gadget.

>>> from pwn import *
>>> elf = context.binary = ELF("./binaryfile")

# Find JMP ESP Gadget
>>> next(elf.search(asm("jmp esp")))
136533428

# Convert to little endian
>>> p32(136533428)
b'\xb4U#\x08'

You can also use these functions to generate the machine code from assembly.

>>> from pwn import *

# SUB ESP and JMP ESP instructions
>>> asm("sub esp, 0x10; jmp esp;")
b'\x83\xec\x10\xff\xe4'

You can read more about reliable Shellcodes here!

Exercise

Return To Shellcode — Hijacking the return pointer to execute custom shellcode reliably

You can find the binary and source code in at ret2shell64. If you deployed the services locally, this exercise can be accessed using nc localhost 30003

For this exercise, remember to turn ASLR back on if you have not.

echo 2 | sudo tee /proc/sys/kernel/randomize_va_space

Notice how there is a \xff\xe4 ? That’s machine code for JMP ESP. It will help you in solving this exercise.

#include <stdio.h>
#include <stdlib.h>

void vuln() {
char buffer[256];
gets(buffer);
}

int main() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);

puts("Guess my name");
vuln("\xff\xe4");
puts("Wrong!");

return 0;
}

Solution

If you didn’t manage to pwn it, do not worry, look at the solution and see if you can replicate it for yourself.

Let’s make use of Pwntools to automate the boring stuff. If you are unsure about any specific function in the code, simply use ChatGPT (or any fancy LLM) to explain it to you.

Injecting JMP RSP Gadget and SUB RSP

Similar to the previous exercise, we change from the 32-bit to 64-bit Shellcode. Then we adjust the amount of padding accordingly. Remember to sandwich the Shellcode like we did previously.

Now, we use the elf.search function to find the location of the JMP RSP gadget. We do not specifically have to find the instruction (as its pretty rare), just the bytes \xff\xe4 will do.

Since the JMP RSP gadget instructs RIP to move to the location of RSP, any instructions we place afterwards. Now all we have to do is calculate the difference between the current location of RSP and the start of the Shellcode, and subtract RSP accordingly.

Finally, we perform JMP RSP which will move RIP to where the Shellcode is, and execute our Shellcode. This allows us to somewhat bypass ASLR (there is PIE, but we will not be covering this for now).

Solution can be found here.

No eXecute (NX)

Unfortunately, there are protections against executing instructions on the stack. This means we can no longer execute instructions that are written to the stack.

Click here for part 6!

--

--