← Back to Portfolio
Systems Security & Exploit Development

Bypassing 64-bit `strcpy` Constraints: A Partial Overwrite Stack Overflow

View Exploit Code on GitHub

While standard buffer overflows on 32-bit architectures are straightforward, porting these exploits to modern 64-bit systems introduces a significant hurdle: The Null Byte Wall. Memory addresses in 64-bit space are heavily padded with leading \x00 bytes, which instantly terminate string-copying functions like strcpy.

In this research, I constructed a vulnerable-by-design environment to demonstrate how to bypass this restriction using a surgically precise Partial Overwrite technique to hijack the Instruction Pointer (RIP).

1. The Vulnerable Target

The core of the vulnerability lies in the improper handling of user input via command-line arguments. Here is the source code of our target binary:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// The Target: We want to hijack execution flow to land here
void secret_function() {
    printf("\n[+] SUCCESS! You entered the secret function!\n");
    exit(0);
}

void echo_input(char *input) {
    char buffer[64]; // Fixed-size 64-byte container
    
    // VULNERABILITY: strcpy does not check the length of 'input'
    strcpy(buffer, input);
    printf("You entered: %s\n", buffer);
}

int main(int argc, char **argv) {
    if (argc != 2) return 1;
    echo_input(argv[1]);
    return 0;
}

The vulnerability is textbook: strcpy copies data from argv[1] into a 64-byte buffer until it hits a null byte. If the input exceeds 64 bytes, it spills over into adjacent stack memory.

2. Lab Setup & Security Posture

To analyze the raw mechanics of the stack frame, I compiled the binary with GCC while explicitly disabling modern compiler mitigations:

gcc -fno-stack-protector -z execstack -no-pie -o vuln vuln.c
Checksec output verifying disabled protections

Using checksec, we verify the environment. NX is disabled (stack execution allowed), Canary is absent, and PIE is off.

3. Dynamic Analysis & Offset Calculation

To hijack the control flow, we must overwrite the Return Address (RIP) stored on the stack. But how much padding is required to reach it? I utilized GDB and a cyclic pattern to find the exact offset.

Running GDB with a cyclic pattern

By feeding a cyclic pattern into the program, a SIGSEGV (Segmentation Fault) is triggered upon returning from echo_input.

Segmentation fault in GDB Examining the top of the stack in GDB

Inspecting the stack pointer (x/1xg $rsp) at the moment of the crash reveals the value 0x6341356341346341. Using msf-pattern_offset, this exact hex value equates to a length of 72 bytes.

Examining the top of the stack in GDB

4. Understanding the 72-Byte Offset

The math behind the 72 bytes perfectly aligns with the x86_64 stack frame architecture:

[ Buffer Space ] 64 bytes
[ Saved Base Pointer (RBP) ] 8 bytes
[ Return Address (RIP) ] Target

Our input fills the 64-byte buffer, overflows into the 8-byte Saved RBP (64 + 8 = 72), and stops exactly at the precipice of the Instruction Pointer.

5. The 64-bit Constraint: Partial Overwrites

Here lies the architectural challenge. The address of our target, secret_function, is 0x0000000000401156.

If we inject the full 8-byte address in Little Endian format (\x56\x11\x40\x00\x00\x00\x00\x00), strcpy will read the \x56, \x11, \x40, and then instantly stop at the first \x00. The rest of the address is discarded, and the stack alignment is ruined.

Finding the target function address

The Solution

We do not overwrite the entire address. We examine the stack before corruption and observe that the upper 5 bytes of the original Return Address are already 0x0000000000. Therefore, we only append the lower 3 significant bytes (\x56\x11\x40) to our payload.

Crucially, strcpy automatically appends exactly one null byte at the end of a copied string. This terminates our overwrite perfectly, fusing our 3 bytes with the existing zeros on the stack to form the complete, valid 64-bit address.

6. Weaponization & Execution

To exploit this, I used Bash to generate a payload containing exactly 72 bytes of padding, followed by the 3-byte partial address.

# Generate 72 bytes of padding
printf 'A%.0s' {1..72} > payload

# Append ONLY the 3 significant bytes in Little Endian
printf '\x56\x11\x40' >> payload
Payload creation via command line

Finally, we pass the raw bytes into the binary as argv[1]. The program executes, the buffer overflows, strcpy adds its termination byte, and the control flow bends to the hidden function.

Successful exploit execution

7. Mitigation: The `strncpy` Trap

This experiment highlights the mechanical precision required to exploit 64-bit applications. While strcpy acts as a barrier by terminating on null bytes, understanding memory layout allows for sophisticated bypasses like the Partial Overwrite.

In the real world, the obvious fix is to replace the unbounded strcpy with a bounded function like strncpy. However, this introduces a dangerous trap that catches many junior developers.

If you patch the code like this:

// DANGEROUS PATCH
strncpy(buffer, input, sizeof(buffer));

You have successfully stopped the buffer overflow, but you may have accidentally created a Memory Leak (a Buffer Over-Read vulnerability).

Here is why: If a user inputs exactly 64 bytes (or more) of data, strncpy will fill the entire container, but it will not append a Null Terminator (\x00) at the end. When the program later runs printf("You entered: %s", buffer);, the printf function won't know where the string ends. It will read right past your 64 bytes and print whatever raw, sensitive memory is sitting next to it on the stack directly to the user's screen!

The Correct Patch

To use strncpy safely, you must intentionally leave one byte empty at the end, and then manually force it to act as a wall (a null terminator):

// 1. Copy up to 63 bytes, leaving the 64th byte untouched
strncpy(buffer, input, sizeof(buffer) - 1);

// 2. Manually lock the absolute last byte as a null terminator
buffer[sizeof(buffer) - 1] = '\0'; 

Alternatively, modern secure C programming often relies on snprintf, which mathematically enforces boundary checks and guarantees a null terminator in a single, safe step:

// The safest modern approach
snprintf(buffer, sizeof(buffer), "%s", input);

Conclusion

This experiment highlights the mechanical precision required to exploit 64-bit applications. While strcpy acts as a barrier by terminating on null bytes, understanding memory layout allows for sophisticated bypasses like the Partial Overwrite.

In production environments, vulnerabilities like this are mitigated by strict boundary enforcement. Developers must entirely avoid functions like strcpy or gets, opting instead for bounded alternatives like strncpy or snprintf.