This task has two parts, return-to-libc and ROP. For each part, work through a local example first, and finally complete the task against the remote target in the workspace for that part. The remote exercise has two separate targets, so start and submit each part in its own workspace.
In the previous task we executed arbitrary code straight from the stack. That is a rather simple, old method, and it was prevented a long time ago.
The NX bit (no-eXecute) was introduced 1 to separate the memory used for storing instructions from the memory used for storing data. Processors will not execute memory pages marked with the NX bit, which in practice means the pages that hold only data.
This makes it very hard to execute a payload that lives on the stack, for example when it was stored there as input to the vulnerable program.
But what if we are not executing code on the stack? This prevention method led to the rise of code reuse attack techniques (ret2libc, ROP, JOP, COOP, etc.). These techniques focus on using existing code to build the functionality we need. But where do we get that code from?
The presence of ASLR could also be bypassed with a specific implementation and combination of these techniques, but that is left outside of this task. It makes things a bit more complicated and usually requires information leakage as well. By default, it will probably prevent everything that we are doing next.
Of the techniques mentioned above, we will take a brief look at ret2libc and ROP, two of the basic and original ones.
A) Return-to-libc (aka ret2libc)
One solution for this is libraries, specifically dynamic libraries, which are loaded when the program is executed. A program that uses no libraries at all is very rare.
Library code is not marked non-executable, because it consists of ordinary instructions. By overflowing the stack suitably, we may be able to call library functions with the parameters we want. We craft the payload so that the overwritten return addresses point into the library. Functions of the vulnerable program itself can be reused in the same way (see Task 1).
One very common library is libc, which probably gave this method its name (ret2libc), and it offers a lot of flexibility.
Arguably the most useful function there is system(). Given /bin/sh as its argument, it starts a shell.
If we pass the correct argument to system(), the code is not executed on the stack, but at the address of system().
This was one of the earliest methods for bypassing NX protection.
Local example
First, make a local example of the ret2libc method, such as spawning a shell.
This time, do not disable NX protection (do not use the -z execstack flag in the compiler).
Disabling other protections is still required.
Work through the source code and commands until you can reach the shell without executing code on the stack.
To be noted:
- You should check whether ASCII Armoring is present on your system. If the address of the
systemfunction contains null bytes, take into account thatstrcpystops at the first null byte. Consider an alternative address or technique. - This task is much easier with 32-bit binaries, because the function parameters are passed differently than on a 64-bit system. (Stack vs. registers?)
A simple example implementation can be found in this paper.
Extra: The method was first published here (Solar Designer, Bugtraq, 1997).
Trying against the remote target
Start the lab in the workspace below and download its generated overflow binary.
This is a separate 32-bit build with a per-instance buffer size, so inspect this binary rather than reusing the padding length from your local example:
objdump -d -M intel --disassemble=stackoverflow ./overflow
On connection, the target prints [*] system @ <address>. This is the resolved address of system() in the target’s glibc, so use it when you build your payload.
The part A flag is readable by the player user, so a shell from system("/bin/sh") is enough to reach it.
Use that shell to list the directory and print the per-instance flag file, then submit the flag in the workspace.
Sign in required
Sign in with the GitHub account linked to your university email.B) Return-oriented programming (aka ROP)
The return-to-libc method has some limitations. We depend heavily on the functions and arguments available in the libraries and in the vulnerable program’s text segment. Sometimes the functionality we want is very hard to implement with existing whole functions alone. With the plain ret2libc technique, chaining more than a couple of function calls is awkward, because the arguments you place after a return address are themselves interpreted as further return addresses. This is explained further below.
Return-oriented programming (ROP) is the more sophisticated version of ret2libc. In addition to whole library functions, we reuse code chunks (instruction sequences) taken from the libraries and from the program itself. These live in the program’s executable memory.
In practice we use sequences that end with a ret instruction. This makes them useful to us, because we can chain these code chunks, which are usually called gadgets, to build the bigger piece of code we need. After a gadget has been executed, the execution flow can be set up so that the next gadget is executed, and so on. With some special gadgets we can control the stack and chain as many calls as we want.
A gadget can be as simple as this Intel-syntax example:
pop eax
ret
By giving enough reusable code, ROP is Turing complete 2.
But how do we find and execute these gadgets?
We could disassemble binaries by hand and look for them, but that takes a lot of effort. Luckily, there are tools we can use.
As an example we use radare2, a multipurpose reverse-engineering tool. Dedicated gadget finders such as ROPgadget and ropper are also widely used.
Let’s once again use the vulnerable program from Task 1 as our target. The tutorial for ROP, with an example of using radare2 and pwntools with it, is here.
A simple but practical demonstration of the ROP technique can be found here.
Extra: The white paper that introduced ROP can be found here 3.
Local example
Try to get the previously mentioned example (ROP_hello) here working by yourself.
Next, make a simple example implementation of the ROP technique.
This could be spawning a local shell, for example.
To keep it different from the ret2libc method, print some text before spawning the shell and also print something after exiting the shell.
That way you apply a ROP chain.
Tip: If you are a bit unlucky and face function addresses containing null bytes on a non-ASCII-Armored system, try alternative functions.
For example, the putchar function has the putchar_unlocked alternative.
Extra: What if you use symbols with pwntools and load the libc binary with it as well?
Then you can avoid hardcoded addresses altogether.
Trying against the remote target
Start the lab in the workspace below and download its generated overflow binary.
This is a separate 32-bit build with a per-instance buffer size, so inspect this binary rather than reusing the padding length from your local example:
objdump -d -M intel --disassemble=stackoverflow ./overflow
On connection, the target prints [*] system @ <address>. This is the resolved address of system() in the target’s glibc, so you can use it to derive the addresses of the functions and gadgets you need.
This part is the setuid variant.
The flag is 0400 root and the binary is setuid root, so only the process itself can read it.
The target also prints [*] flag @ <address> (<path>), the address and full path of the flag file.
A shell will not help here, because the process gives up its root privileges when it starts one.
Build a ROP chain that makes the process read and print the flag file itself, then submit the flag in the workspace.