How do I even pwn anything? Part 5 — Creating Reliable Shellcode
Explore how to perform Linux Binary Exploitation from Capture-the-Flag (CTF) competitions.
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
.
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.
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.