Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
The course is designed as a continuation of the Windows Exploit Development workshops by the people at Exploit Development Intro and some pre-requisite knowledge is expected of the following topics:
An Understanding of x86-64 Assembly
Familiarity with GDB
Familiarity with C and Python
Familiarity with the Standard Jump to Shellcode Exploits
Please do view this 15 minute 'Introduction to Return Oriented Programming' video as a refresher. If you have time, please go through the lesson plan for the video.
We will be using vagrant to standardise the environment so following the lesson plan is easy. The Vagrantfile will provision the appropriate pre-requisites but please go through this document to get an understanding of the steps required to prepare the testing environment.
First, install vagrant and virtualbox. Vagrant can be downloaded from this link. Virtualbox can be downloaded from here.
Next, clone the repository onto your host machine. If you have messed up somewhere along the course and want to revert the state of the repository, just delete the entire directory and perform this step again.
Now, bring the vagrant box up.
Once the provisioning finishes, you can ssh into the vagrant box.
The course repository directory you clone previously will be mounted at /vagrant
so you can use your preferred text editor.
Now, we need to start the docker containers for the exercises you will be working on. To do this, perform the following steps:
You do not need to rebuild the docker containers after you have built them once but you may need to redeploy the docker containers if you restart the machine.
For Windows users there are two options:
Start a virtual machine containing Ubuntu 16.04 and run the provisioning script found below. Next, manually clone the course repository into the machine. Note that directory locations may be different from the code listings in the course if you go down this route. The choice of virtualisation software you choose is up to you.
Install Vagrant and Virtualbox for Windows. This allows you to follow the instructions above almost identically.
One caveat with Option 2 is that your Windows Installation might not have SSH installed previously. When you invoke vagrant ssh
, you might receive a message as follows:
In that case, simply follow the instructions to SSH into the newly provisioned system with an SSH client of your choice such as Putty or SmarTTY.
This is the entire provisioning script:
If you used vagrant to bring the machine up, this should have been done for you.
There are three classes of protection we will be discussing in this section.
No eXecute Bit
Address Space Layout Randomisation
Stack Canaries
We will discuss this using visualisations of how a classic exploitation technique attempt is stopped by the properties of each of the protections.
First let's visualise how the stack looks like before a buffer is read into:
For clarification, the value of the saved base pointer is 0xbfff0030 and the value of the return address is 0x080484f0 (an address within the binary). The numbers are reversed in the visualisation because x86 is a little endian architecture.
On a valid run of the program, the buffer is filled within its bounds. Here we have 15 As and a null byte written to the 16 length buffer.
However, since the read allows for the program to read more than 16 bytes into the buffer, we can overflow it and overwrite the saved return pointer.
When the function returns, the program will crash since the instruction pointer is set to 0x41414141, an invalid address.
To complete the technique, the attacker will fill the first part of the buffer with the shellcode, append the appropriate padding and overwrite the saved return pointer with the address of the buffer.
Now, when the function returns, the program will begin executing the shellcode contained in the buffer since the saved return pointer was overwritten by the buffer address (0xbfff0000). From this point onwards, the attacker has achieved arbitrary code execution.
Now that we understand how the classic exploitation technique works, let us start introducing protections and observing how they prevent the technique from working.
Also known as Data Execution Prevention (DEP), this protection marks writable regions of memory as non-executable. This prevents the processor from executing in these marked regions of memory.
If we look at the memory map of a program compiled with NX protection, the stack and heap are typically marked non-executable.
In the following diagrams, we will be introducing a new indicator colour for the memory regions to denote 'writable and non-executable' mapped regions. Firstly, the stack before the read occurs looks like this:
When we perform the same attack, the buffer is overrun and the saved pointers are overwritten once again.
After the function returns, the program will set the instruction pointer to 0xbfff0000 and attempt to execute the instructions at that address. However, since the region of memory mapped at that address has no execution permissions, the program will crash.
Thus, the attacker's exploit is thwarted.
This protection randomises the addresses of the memory regions where the shared libraries, stack, and heap are mapped at. The reason for this is to frustrate an attacker since they cannot predict with certainty where their payload is located at and the exploit will not work reliably.
On the first run of the program, the stack looks like this just before the read:
If we terminate the program and run it again, the stack might look like this before the read:
Notice how the stack addresses do not stay constant and now have their base values randomised. Now, the attacker attempts to re-use their payload from the classic technique.
Notice that the saved return pointer is overwritten with a pointer into the stack at an unknown location where the data is unknown and non-user controlled. When the function returns, the program will begin executing unknown instructions at that address (0xbfff0000) and will most likely crash.
Thus, it is impossible for an attacker to be able to reliably trigger the exploit using the standard payload.
This protection places a randomised guard value after a stack frame's local variables and before the saved return address. When a function returns, this guard value is checked and if it differs from the value provided by a secure source, then the program is terminated.
In the following stack diagram, an additional stack canary is added right after the buffer. The valid value of this stack canary is 0x01efcdab.
Now, the attacker attempts their exploit with the standard payload again. The stack diagram looks like this after the read:
Notice that the stack canary has been overwritten and corrupted by the padding of 'A's (0x41). The value of the canary is now 0x41414141. Before the function returns, the canary is xored against the value of the 'master' canary. If the result is 0, implying equality, then the function is allowed to return. Otherwise, the program terminates itself. In this case, the program fails the check, prints a warning message, and exits.
Thus, the attacker is not even able to redirect control flow and the exploit fails.
We were able to pick from a wealth of ROP gadgets to construct the ROP chain in the previous section because the binary was huge. Now, what happens if the binary we have to attack is not large enough to provide us the gadgets we need?
One possible solution, since ASLR is disabled, would be to search for our gadgets in the shared libraries loaded by the program such as libc. However, if we had these addresses into libc, we could simplify our exploit to reuse useful functions. One such useful function could be the amazing system()
function.
To investigate this, we can create a diagnostic binary to introspectively look at the virtual memory map and to print us the resolved system()
address. The source is as follows:
I have compiled two versions of the binary, a 32 bit version and a 64 bit version.
Running the 32 bit one:
Note the base address of libc-2-2.23.so (0xf7e10000) and the resolved address of system (0xf7e4ada0). Let's subtract the address of the base address from the address of system to get the system offset.
Take note of that offset, 0x3ada0. Next, we can disassemble the libc shared object and look for the start of the system function.
That's a bingo. Notice how the offset we calculated previously is the same as the address of __libc_system@@GLIBC_PRIVATE
.
We can repeat this with the 64 bit one:
Calculating the difference.
Finding the address of system in the 64 bit libc binary.
And we have another match.
This is useful information as now we have a way to calculate the addresses of useful functions given the base address of libc. Since shared objects are mapped at the same location without randomisation due to ASLR being disabled, we can very easily find the addresses of useful things such as the system()
function and the /bin/sh
string by examining the libc shared object on its own.
To make life easier, the 'libc-database' toolset by Niklas Baumstark, provides helper scripts to build a libc database, identify versions of libc from information leaks, and dump useful offsets. In the vagrant provisioning script, I have already added the pertinent libc versions used in the exercises.
Let's do the opposite of what we did the last section. Instead of attacking a bloated binary, we are going to attack a really lean and mean one. Something like this:
Running the binary:
It really does not do much. Here is the skeleton exploit code to achieve EIP control.
At this point, it is simply a matter of finding at which address is libc mapped onto and then calculating the addresses of useful functions from the offsets we dumped.
Try to obtain a shell by making the necessary additions to the skeleton code. Please attempt to do this on your own before looking at the solution.
In this section, we will take a closer look at the Global Offset Table. In the previous section, we learnt how to use jumping to the PLT stubs as a technique to reuse functions in libc. When jumping to PLT, the GOT entry for that corresponding function acted as a malleable space where the dynamic address would be held. We shall exploit that malleability.
Now, let's depart from the standard paradigm of stack overflows for the moment. We shall begin looking at vulnerable programs that allow for write-what-where primitives, albeit in a limited fashion.
Our first simple example is the following:
The program is vulnerable in two ways:
It provides an information leak opportunity when the now_playing.album
pointer is overwritten and the album name is printed.
It provides a write what where primitive when the now_playing.album
pointer is overwritten and input is provided to the second prompt.
Running the binary:
It's a little screwy but nothing that fancy yet. Let's begin by trying to achieve the first vulnerable condition (arbitrary read). First, we can take an easy to spot target to leak. We can use the "This is a Jukebox" string. First, we need to figure out its address.
Now, here's a skeleton exploit that will demonstrate the leaking of that string.
Testing it out:
See the "(This is a Jukebox)"? That means it worked. Now, what we are most interested in are mostly pointers. So let's make a small addition that would parse the leak and transform it into a nice number for us.
Running it:
Awesome, now we can begin thinking about our exploit.
At the moment, we do not have a target to leak and to overwrite. We must be careful to pick a suitable one because the information leak and arbitrary write has to be performed on the same address. Additionally, the write has to result in EIP control at some point of the program's execution since we do not have that yet.
If we take a look at the source code again, the following function is called last:
Interestingly, this is perfect for our uses. If we leak the address of puts in libc, we can calculate the address of the libc base and subsequently, the address of the system function. Also, once we have that, we can write the address of the system function into the puts@got entry so that when this final line executes, it will actually execute:
Which means that system will be called with a parameter that we control! How convenient!
First, let's see if we can leak the address of puts@got. First, we need the address.
Now, we can modify our earlier iterations of the exploit.
Running the script gives us a sanity check that we are reading the right thing.
Now, let's try and get EIP control. This should be as simple as sending four bytes.
It works, the program crashed at 0xdeadc0de
.
Let's gather our offsets and we can write our final exploit script.
Final exploit script:
Getting our shell:
After the mistakes of the previous Event, Kaizen has decided to secure his system. Can you find a way to exploit the new binary?
It was compiled with a stack canary.
gcc -m32 -znoexecstack -o ./build/2_event1 ./src/2_event1.c
The binary can be found here and the source can be found here. The remote target is nc localhost 1902
.
The solution script may be found here.
In this section, we will look at crafting a more complicated exploit that relies on multiple stages. Surprisingly, the vulnerable target we are looking at is the most simple of all the ones we have seen so far. It is precisely the lack of flexibility we have with such a simple target that forces us to adopt a more sophiscated exploit strategy.
It is very simple. It simply echoes your input. It is vulnerable to a standard buffer overflow but ASLR and NX are enabled which means the only things you have to work with is read
, write
, and the gadgets that are present in the tiny binary.
First, as we always do, we need a skeleton script to give us our EIP control.
Next, we would like to try and leak a libc address. We can achieve this by creating fake stack frames that execute write(STDOUT, write@got, 4)
. This will print 4 bytes of the write@got address to stdout which we can receive on our exploit script.
This works easily enough to get us that leak.
Now, remember that what we are doing is creating a rop chain with these PLT stubs. However, if we just return into functions after functions, it is not going to work very well since the parameters on the stack are not cleaned up. We have to handle that somehow.
This is where the pop pop ret
gadgets come in. They allow us to advance the stack and make sure our faked stack frames are coherent. We need a pop pop pop ret
sequence because our write
call had 3 parameters.
What should we do next then? What we want to do is overwrite a GOT entry so that we can execute system. Now, we can leverage the fact that a read
call is basically an arbitrary write primitive. So our entire rop chain sequence would look something like this:
write(1, write@got, 4)
- Leaks the libc address of write
read(0, write@got, 4)
- Read 4 bytes of input from us into the write GOT entry.
system(some_cmd)
- Execute a command of ours and hopefully get shell
Now, of course we have a possible issue. Since our ROP chain would have to include the address of the command on the first read, we have two choices:
Expend another read sequence to write "/bin/sh" somewhere in memory
Use an alternative command (such as ed)
Option 1 is not feasible as it takes 20 bytes to construct a frame for read. This is a heavily cost when we only have 72 bytes to play with. So, we have to go with Option 2 which is easy enough to get.
With all of the information in hand, we can write our exploit:
Running the exploit:
Let us revisit the classical technique of exploiting a stack overflow on a binary with no protections enabled and ASLR turned off. We will do a demonstration on a binary compiled from the following source code:
The binary was compiled with the following flags:
Before running the binary, disable ASLR with the command:
Verify that ASLR is indeed turned off.
Also, all protections are off.
The binary is simple. It reads 100 bytes from stdin into a 16 byte character buffer and prints the contents of the buffer to the user. On a benign execution, the behaviour might look like this:
It is very easy to get the binary to crash.
Let's delve into GDB to get the offset we need to place our return address at to control EIP.
We can begin writing a skeleton for our exploit.
If we run this and attach to the spawned process, we can verify that the program will crash on the address 0x41424344.
Next, we need to figure out where should we direct execution to. This would probably be somewhere in the buffer we write to with the read()
call. If we break on the call to puts()
, we can get a stack address we can use as the argument.
0xffffd5f0 is the start of buffer the user input is read into. A good place to jump to would be (0xffffd5f0 + 28 + 4). This lets us put our shellcode right after the return address. To begin with, we can test out strategy by filling that space with 'int 3' instructions (0xcc).
Now, we can run this, attach our debugger to the process and see if it breaks.
Taking some shellcode from Aleph One's 'Smashing the Stack for Fun and Profit':
Putting it all together:
Running the script.
Note that you might have to adjust the return address as the one on this machine might not match up to the one on your machines.
Actually, the title is a lie. We're not really going to discuss ASLR in that depth yet. We don't really need to. However, what we are going to do is explore the effects of ASLR on a diagnostic binary we used in the previous section.
First, make sure ASLR is turned off.
Now, play with the binaries in /vagrant/lessons/8_aslr/build/
. You should notice that the addresses the objects are mapped at are more or less constant.
Now, turn on the ASLR.
Before repeating the previous steps on the binaries again, take a look at the output from checksec
.
Notice that the last two have PIE enabled. PIE stands for Position Independent Executable. Do you notice any interesting about the results when running these binaries with ASLR turned on?
Yes, I know this is a really cliche topic but I am just covering one cool thing that you can do with pwntools. That's all, I promise. Now, we will be looking at this simple program that is vulnerable to a format string attack. The idea is to modify the token so that it contains 0xcafebabe when the check occurs.
So after playing around with the program, we figure out that the first format argument we control is at offset 5.
Next, we need the address of the token.
Now we can write our exploit script. Pwntools actually has a format string attack generator so we can beat the binary in a few quick easy lines.
Running the program.
Before you continue onto the more advanced exercises, here's something to tackle. The source code to this challenge is given:
The binary to the exercise can be found here. The remote target is nc localhost 1903
and the goal is to get a shell.
GDB with PEDA and Pwntools are two tools that we will be using extensively throughout the course. This section is designed to run through their basic use and to work out any possible kinks that might arise.
Throughout the section we will be using pre-built binaries in the build
folder. From the base repository directory, please navigate as follows:
There should be a couple of binaries already in the directory. They are standard ELF files that you can run.
PEDA (Python Exploit Development Assistance) is an extension to GDB that adds on a whole bunch of useful commands and quality of life improvements to the standard GDB experience. The provisioning script should have made the necessary additions to the GDB configuration so all you need to do to start it is launch GDB.
Let's walk through an example with the 1_sample
binary.
The prompt should show gdb-peda
. If it does not, something has gone wrong with the environment setup. To start off, let's break on main and explore what is offered by PEDA.
Notice that the default display is a made lot more informative than with the vanilla GDB. Other than making it a lot easier to step through programs and view the changes as they happen, PEDA provides a ton of other functionality as well. To view the full list of them, you can use the peda
command.
We will go through a few of the interesting commands.
The checksec
command lists the protections that are enabled for the binary. This is useful when figuring out how to craft your exploit.
Often, calculating offsets from addresses is required when crafting your payload in an exploit. This command makes it easy to find the distance between two addresses.
If you ever needed to get the address for certain symbols in a binary (if you are lucky and it is not stripped), you can use the elfsymbol
command.
The pattern generator is one of the features of PEDA I most use. What it does is generate a De Brujin Sequence of a specified length. A De Brujin Sequence is a sequence that has unique n-length subsequences at any of its points. In our case, we are interested in unique 4 length subsequences since we will be dealing with 32 bit registers. This is especially useful for finding offsets at which data gets written into registers.
Let's say we create a pattern of length 64.
Imagine that we have triggered a buffer overflow and find that the instruction pointer crashes on the address 0x48414132 ('2AAH' in ASCII). We can figure out the exact offset of our data to place our address to redirect code execution to.
This command parses information from the /proc/pid/x
directory and presents it to you.
It is particularly useful to view which file descriptors are open.
vmmap
displays the memory mapping of the process. It is simple to invoke.
What is important to glean from the listing above is the permissions flags of each of the segments. Often when developing your exploit, you will need to place some data somewhere. This data can be arguments to functions expecting a string pointer or even shellcode. What is required is that the segment that is being written to is marked writable.
Additionally, if you have a pointer from a memory leak and want to figure out where exactly the pointer is pointing to, you can drill down specifically on that address.
The find
command is an alias for the searchmem
peda command. It searches memory for a given pattern. It is particularly useful to figure out where data is or how it flows in a process.
For example, something that is often sought for is the string "/bin/sh". Perhaps it lays in memory somewhere. We can use find
to look for it.
Pwntools is a Python library that provides a framework for writing exploits. Typically, it is used heavily in CTFs. There are a ton of useful functions provided by Pwntools but I will briefly describe the process I personally use.
There are three ways you can use Pwntools:
Interactively through the python/iPython consoles
In a python script
Pwntools command line tools
Interactively through the Console
Often, you want to try things out before actually writing an actual script when developing your exploit. The iPython console is a great way to explore the Pwntools API. For convenience, we will import everything in the pwn
package to the global namespace.
iPython provides tab completion and a built-in system to look up documentation in docstrings. For example, if we want to look at what the p32
function does, we can look it up with the ?
sigil.
In a Python Script
I like to begin with the following template when starting a new exploit.
Running the script is as simple as calling python on it. Try running this script:
Running the script:
Pwntools Command Line Tools
Pwntools installs the pwn
python script in /usr/local/bin. It provides frontends to useful features of the library. To get a list of all available frontends, you can execute pwn -h
.
You can investigate the available options at your own time. Take a look at the documentation for a more detailed description of each of them.
Your target might expose itself through different vectors. Today we will focus on attacking remotely running binaries that you can connect to over the network. First, let's see how we might interact with a local copy of a binary that accepts input on stdin and returns output on stdout.
Local Copy of Binary
To begin with, we will look at the 2_interactive binary:
For completeness sake, here is the source code:
The point of the program is to check the user input against a hardcoded password. If it matches, then an interactive shell is spawned.
Now that we know how to craft the input, we can write our sample exploit using Pwntools.
Take some time to go through the code and understand what it does. Take note of the process("../build/2_interactive")
line. It starts a new process and allows you to treat the object like a socket. Run the script and verify it works:
It is very easy to turn a console-based application into a networked one and there are multiple ways to do it. The exercises that come later in the docker containers use xinetd, a server daemon, to listen for network requests and then launch the binary to serve these requests. For now, we can use socat to do the same thing.
First, we will start a new screen session so that we can background our socat terminal.
Next, we run the following command to start a listener on port 1330.
It should hang there. Now return to your original bash session by holding down the following key sequence: CTRL-A-D. If you run the command screen -ls
you should see that the socat screen session is in the background.
To verify that the listener is indeed listening on port 1330, we can run netcat.
Now, here comes the magic. To modify the first script we had to work with local binaries, we may simply comment out the process()
line and replace the line with remote("localhost", 1330)
.
Now, if we run this it should give us our shell.
Pwntools does provide GDB functionality but we will be taking a more manual approach since that is the way I am used to doing it. If you are interested in using this functionality, you can view the documentation here.
We will be using the 3_reversing binary to walkthrough this section. First, lets run it vanilla.
To gain a deeper insight into what is going on, we can use ltrace
to investigate.
Using the following script, we can print the process id before the interaction with the program happens.
First, start another ssh session and run the script. Note the process id that gets printed. Also note the use of p32()
to pack integers into little endian strings.
Return to the original bash shell and run GDB as root. Now, we can attach to the process.
At this point, the program is paused somewhere in libc. We can setup our breakpoints. Let's assume that we have done some preliminary reverse engineering and have discovered that the input gets passed to the check_creds()
. Here's the disassembly of the function.
Let's place some breakpoints on the function and step through the execution to discover what inputs we need to supply to pass the check.
Back on the terminal with the script running, press enter to trigger the data sending. On the gdb terminal, the debugger should have broken. Continue on with the standard gdb debugging process to obtain the values.
Please attempt the exercises in this section on your own before looking at the solution.
Continuing on from where we left off in the guided portion, please find the right values for the name and token and modify the exploit code such that a shell is obtained.
Solution script can be found here.
Please find the flag file on an instance of the service running at 127.0.0.1:1900.
Solution script can be found here.
Before beginning this section, please ensure you have re-enabled ASLR. You can do this by running the following command.
Finally, we have two protections enabled: ASLR and NX. To start off, this will be our first target for the section:
Running the binary:
Now that ASLR has been enabled, we have a problem. We no longer can be sure where the libc will be mapped at. However, that begs the question: how does the binary know where the address of anything is now that they are randomised? The answer lies in something called the Global Offset Table and the Procedure Linkage Table.
To handle functions from dynamically loaded objects, the compiler assigns a space to store a list of pointers in the binary. Each slot of the pointers to be filled in is called a 'relocation' entry. This region of memory is marked readable to allow for the values for the entries to change during runtime.
We can take a look at the '.got' segment of the clock binary with readelf
.
Let's take the read entry in the GOT as an example. If we hop onto gdb, and open the binary in the debugger without running it, we can examine what is in the GOT initially.
It actually turns out that that value is an address within the Procedure Linkage Table. This actually is part of the mechanic to perform lazy binding. Lazy binding allows the binary to only resolve its dynamic addresses when it needs o.
If we run it and break just before the program ends, we can see that the value in the GOT is completely different and now points somewhere in libc.
When you use a libc function in your code, the compiler does not directly call that function but calls a PLT stub instead. Let's take a look at the disassembly of the read
function in PLT.
Here's what's going on here when the function is run for the first time:
The read@plt
function is called.
Execution reaches jmp DWORD PTR ds:0x804a00c
and the memory address 0x804a00c is dereferenced and is jumped to. If that value looks familiar, it is. It was the address of the GOT entry of read
.
Since the GOT contained the value 0x08048346 initially, execution jumps to the next instruction of the read@plt
function because that's where it points to.
The dynamic loader is called which overwrites the GOT with the resolved address.
Execution continues at the resolved address.
The details of this will be important for the next section but for now, the crucial characteristic of the PLT stub is that it is part of the binary and will be mapped at a static address. Thus, we can use the stub as a target when constructing our exploit.
As per usual, here is the skeleton code to obtain EIP control of the binary.
Let's look at the available PLT stubs to choose from.
We are in luck, because system@plt
is a powerful function we can definitely use. That's one out of two things we need. The second thing is a command we can execute. Normally, we would use "/bin/sh" but it does not seem we would find that here.
Take a moment to figure out a target before taking a look at the answers.
It turns out that ed
is a valid Linux command. It actually spawns a minimalistic editor. It also turns out that there is an "ed" string available in the binary. Can you spot it?
If we take the last two characters of the string "The sheep are blue, but you see red" or "_IO_stdin_used", we can get that "ed" we are looking for.
Putting our parts together, we can come up with this final exploit.
But, now you might ask, if all we are going to spawn is a line based text editor, then how are we going to get our shell? As it so happens, the ed
program can actually run commands!
Please do these exercises without looking at the solution.
Let's start doing some difficult exercises. Here is event0. Try to solve this problem using the Ret2PLT technique.
The binary can be found here. And the remote target is at nc localhost 1901
.
If you get stuck, you can look at the following solution scripts in order of completeness.
Skeleton
Local POC
Remote POC
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.
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.
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.
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.
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 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:
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.
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?