5. Bypassing NX with Return Oriented Programming
Bypassing NX with Return Oriented Programming
Since it is assumed that all participants have the gone through the introductory video on return oriented programming set out in the pre-requisites, we will jump straight into developing our exploits. If you are not clear on the basics of ROP, please revisit the video.
Enabling NX
Let's start increasing protections on the binaries we play with. We can start simple by only enabling the NX protection on the binaries we compile. For this section we will take a look at the following binary compiled from the following source code.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
void vuln() {
char buffer[128];
char * second_buffer;
uint32_t length = 0;
puts("Reading from STDIN");
read(0, buffer, 1024);
if (strcmp(buffer, "Cool Input") == 0) {
puts("What a cool string.");
}
length = strlen(buffer);
if (length == 42) {
puts("LUE");
}
second_buffer = malloc(length);
strncpy(second_buffer, buffer, length);
}
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
puts("This is a big vulnerable example!");
printf("I can print many things: %x, %s, %d\n", 0xdeadbeef, "Test String",
42);
write(1, "Writing to STDOUT\n", 18);
vuln();
}Since the binary is not big enough to give us a decent number of ROP gadgets, we will cheat a bit and compile the binary as a statically linked ELF. This should include library code in the final executable and bulk up the size of the binary. We also mark the writable regions of memory as non-executable.
We can verify that the binary has the NX protection enabled by using the checksec script. We can also check that the file is statically compiled with file.
Obtaining EIP Control
First of all, we need to determine the offsets for EIP control. For the sake of brevity, I will use the offset of 148 bytes. When you follow along in the lesson, please do try obtaining the offset yourself. A rough skeleton exploit script is given as follows:
Running the script and attach gdb to the process allows us to verify that the EIP control works.
Code Gadgets
Now, let's take a step back and think about how to proceed from this point. We cannot use the previous strategy of placing shellcode on the stack and jumping to it because the stack is now non-executable. One possible technique we can use is to reuse existing code in the binary.
If you have gone through the pre-requisite watching, you may realise that these snippets of useful code sequences that end in ret instructions are useful to construct a ROP chain. Some of these sequences might look like the following:
These are called gadgets. We can automate searching for these gadgets using a tool called ROPgadget.
Now, some combination of a subset of these 12307 gadgets should surely yield us a shell. Before we start mixing and matching, lets take an aside to discuss Linux syscalls.
Linux Syscalls
Linux system calls or syscalls are interfaces between the user space application and the Linux kernel. Functionality performed by the Linux kernel can be invoked by placing parameters into the right registers and passing control to the interrupt vector 0x80 using the int 0x80 opcode. Typically, this is not done by the program directly but by calling glibc wrappers.
We will not go too deep into describing how the system calls work and go straight to the system call that interests us the most: execve. The execve system call runs an executable file within the context of the current process.
If we take a look at the libc function, we get the following signature:
int execve(char const *path, char const *argv[], char const *envp[]);
Typically, we invoke this function in the following manner to spawn shells.
execve("/bin/sh", {0}, {0})
If we take a look at the syscall reference located here, we can see that some parameters are expected in the eax, ebx, ecx, and edx registers.
eax - holds the number of the syscall to be called
ebx - a pointer to the string containing the file name to be executed
ecx - a pointer to the array of string pointers representing argv
edx - a pointer to the array of string pointers representing envp
For our purposes, the value that each of the registers should contain are:
Generating the ROP Chain
Automatically finding the ROP gadgets to perform the execve syscall can be done by ROPgadget. It actually even generates the the output as a python script that you can embed into the skeleton.
Integrating the generated code is as easy as copy and pasting into the final exploit.
When we run the exploit, we get our shell.
Exercises
6.1 Using Ropper to Generate ROP Chains
There are alternative tools to ROPgadget that perform gadget searching and automatic chain generation. One such tool is ropper. You can generate an execve rop chain with the following command.
However, using this payload in a modified script does not work. Can you figure out why and fix it?
Last updated
Was this helpful?