Stack Crashing, Dangerous Functions, and Safe Alternatives

 

Beware of Buffer Overruns
Chapter 38.9 โ€” Stack Crashing, Dangerous Functions, and Safe Alternatives
๐Ÿ’ฅ Stack Smashing
๐Ÿšซ gets() / strcpy()
โœ… snprintf()

The Most Common Security Vulnerability in History

Buffer overruns (also called buffer overflows) are consistently the single most common source of security breaches on computer systems. A buffer overrun occurs when a program writes more data into a memory buffer than the buffer can hold, overwriting adjacent memory.

In privileged programs, this is catastrophic. An attacker who can trigger a buffer overrun can potentially overwrite the function return address on the stack and redirect the program to execute arbitrary code โ€” as root. This technique is known as stack crashing or stack smashing.

๐Ÿ’ฅ How Stack Crashing Works

Every function call creates a stack frame in memory. This frame contains local variables, the saved return address (where execution should continue after the function returns), and the saved frame pointer.

When a fixed-size local buffer is overflowed, the attacker’s extra bytes overwrite adjacent data on the stack โ€” including the return address. When the function returns, instead of going back to the caller, it jumps to the attacker’s code.

Normal Stack Frame
… caller’s frame …
Return Address โ†’ caller
Saved frame pointer
buf[128] (safe)
low address โ†‘
After Overflow Attack
… caller’s frame …
Return Addr โ†’ SHELLCODE! ๐Ÿ”ด
Overwritten (junk)
buf[128] filled with
shellcode + padding
low address โ†‘

When vulnerable_function() returns, instead of returning to the legitimate caller, it jumps to the shellcode placed in the buffer โ€” executing arbitrary code with the process’s (root) privileges.

๐Ÿšซ Dangerous Functions โ€” Never Use These in Privileged Code
Dangerous Function Why It’s Dangerous Safe Alternative
gets() No length limit whatsoever. Reads until newline. Always overflows. REMOVED from C11. fgets(buf, sizeof(buf), stdin)
strcpy() Copies until null byte with no destination size check. strncpy() or strlcpy()
strcat() Appends without checking if destination has room. strncat() or strlcat()
sprintf() Format string with no output buffer size limit. snprintf(buf, sizeof(buf), …)
scanf(“%s”, buf) Reads string with no length limit into fixed buffer. scanf(“%127s”, buf) โ€” specify max

๐Ÿ’ป Code Example 1: Dangerous vs. Safe String Handling
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define BUFSIZE 64

/* ============================================================
 * DANGEROUS: Classic buffer overrun examples
 * ============================================================ */

void dangerous_copy(const char *src)
{
    char buf[BUFSIZE];
    strcpy(buf, src);    /* BUG: no bounds check โ€” overflow if src > 63 chars */
    printf("Copied: %s\n", buf);
}

void dangerous_input(void)
{
    char buf[BUFSIZE];
    gets(buf);           /* NEVER USE: no bounds check at all โ€” always dangerous */
    printf("Got: %s\n", buf);
}

/* ============================================================
 * SAFE: Length-limited alternatives
 * ============================================================ */

void safe_copy(const char *src)
{
    char buf[BUFSIZE];

    /* strncpy: copies at most BUFSIZE-1 chars, but MAY NOT null-terminate! */
    strncpy(buf, src, BUFSIZE - 1);
    buf[BUFSIZE - 1] = '\0';  /* Always explicitly null-terminate after strncpy */

    printf("Safely copied: %s\n", buf);

    /* Check if truncation occurred */
    if (strlen(src) >= BUFSIZE) {
        fprintf(stderr, "WARNING: input was truncated from %zu to %d chars\n",
                strlen(src), BUFSIZE - 1);
        /* In production code, this may be an error โ€” handle it! */
    }
}

void safe_build_path(const char *dir, const char *file)
{
    char path[BUFSIZE];
    int written;

    /* snprintf: safe sprintf โ€” writes at most n-1 chars, always null-terminates */
    written = snprintf(path, sizeof(path), "%s/%s", dir, file);

    if (written < 0) {
        perror("snprintf");
        return;
    }

    /* Check for truncation โ€” snprintf returns how many bytes WOULD have been
     * written (not counting null). If >= sizeof(path), output was truncated. */
    if (written >= (int)sizeof(path)) {
        fprintf(stderr, "ERROR: path was truncated! %d chars needed, %zu available\n",
                written, sizeof(path));
        return;  /* Don't proceed with truncated path */
    }

    printf("Path: %s\n", path);
}

void safe_input(void)
{
    char buf[BUFSIZE];

    /* fgets: reads at most BUFSIZE-1 chars, always null-terminates */
    printf("Enter input: ");
    if (fgets(buf, sizeof(buf), stdin) == NULL) {
        perror("fgets");
        return;
    }

    /* Remove trailing newline if present */
    size_t len = strlen(buf);
    if (len > 0 && buf[len - 1] == '\n')
        buf[len - 1] = '\0';

    printf("Got: '%s'\n", buf);
}

int main(void)
{
    printf("=== Safe string operations ===\n");

    safe_copy("short string");
    safe_copy("this is a very long string that exceeds sixty-four characters and would overflow");

    safe_build_path("/home/user", "document.txt");
    safe_build_path("/extremely/long/path/that/might/exceed/buffer", "filename.txt");

    return 0;
}

๐Ÿ’ป Code Example 2: Safe String Concatenation and Format Strings
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define BUFSIZE 128

/*
 * Demonstrates safe concatenation using snprintf offset trick.
 * Builds a string incrementally without overflow.
 */
void safe_log_message(const char *username, int uid, const char *action)
{
    char msg[BUFSIZE];
    int pos = 0;
    int remaining = sizeof(msg);
    int written;

    /* Build message piece by piece, tracking remaining space */

    /* Piece 1: "User: " */
    written = snprintf(msg + pos, remaining, "User: ");
    if (written < 0 || written >= remaining) goto truncated;
    pos += written; remaining -= written;

    /* Piece 2: username */
    written = snprintf(msg + pos, remaining, "%s", username);
    if (written < 0 || written >= remaining) goto truncated;
    pos += written; remaining -= written;

    /* Piece 3: UID */
    written = snprintf(msg + pos, remaining, " (uid=%d)", uid);
    if (written < 0 || written >= remaining) goto truncated;
    pos += written; remaining -= written;

    /* Piece 4: action */
    written = snprintf(msg + pos, remaining, " performed: %s", action);
    if (written < 0 || written >= remaining) goto truncated;

    printf("Log: %s\n", msg);
    return;

truncated:
    fprintf(stderr, "WARNING: log message truncated at position %d\n", pos);
    msg[pos] = '\0';  /* Ensure null termination */
    printf("Log (truncated): %s\n", msg);
}

/*
 * Safe reading of integer from user input.
 * Demonstrates bounds checking for numeric input.
 */
int safe_read_port(void)
{
    char buf[32];
    char *endptr;
    long port;

    printf("Enter port number (1-65535): ");
    if (fgets(buf, sizeof(buf), stdin) == NULL)
        return -1;

    /* Parse as long integer */
    port = strtol(buf, &endptr, 10);

    /* Check for conversion errors */
    if (endptr == buf || (*endptr != '\n' && *endptr != '\0')) {
        fprintf(stderr, "Invalid port number: not numeric\n");
        return -1;
    }

    /* Validate range */
    if (port < 1 || port > 65535) {
        fprintf(stderr, "Port %ld out of range (1-65535)\n", port);
        return -1;
    }

    return (int)port;
}

int main(void)
{
    /* Test safe log message building */
    safe_log_message("alice", 1001, "login");
    safe_log_message("this_is_a_very_long_username_that_exceeds_expectations", 9999,
                     "attempted_privilege_escalation_via_buffer_overflow_attack");

    return 0;
}

Key Points About snprintf() Truncation

โš ๏ธ Always check for truncation after snprintf()
snprintf() returns the number of bytes that would have been written (not counting the null). If the return value is >= the buffer size, truncation occurred. A truncated string may be semantically meaningless or even dangerous (e.g., a truncated path might point to a different file). Always check and handle truncation explicitly.

๐Ÿ›ก๏ธ Kernel and Hardware Mitigations

Modern Linux and x86 hardware include mitigations that make stack crashing harder (but not impossible). These are a safety net, not a replacement for safe coding:

ASLR (Address Space Layout Randomization)
Since Linux 2.6.12: randomly varies the location of the stack (8 MB range), heap, and library mappings. Makes it hard for an attacker to predict where to jump.
NX / DEP (No-Execute bit)
Modern x86 hardware marks stack pages as non-executable. Code on the stack cannot be run, defeating classic stack crashing.
Stack Canaries (SSP)
GCC’s -fstack-protector places a random “canary” value between the buffer and return address. If it’s overwritten, the program aborts.

๐Ÿ“Œ Key Terms

Buffer Overrun Stack Crashing Stack Smashing gets() strcpy() sprintf() snprintf() strncpy() fgets() ASLR NX bit Stack Canary
๐ŸŽฏ Interview Questions โ€” Buffer Overruns
Q1. What is a buffer overrun and why is it especially dangerous in privileged programs?A buffer overrun occurs when more data is written to a buffer than it can hold, overwriting adjacent memory. In privileged programs, this can overwrite the stack frame’s return address, redirecting execution to attacker-controlled code running with root privileges (stack crashing/smashing).

Q2. Why is gets() never acceptable?gets() reads input until a newline with no limit on the number of characters read. It is physically impossible to write a safe call to gets() โ€” if the user types more than the buffer holds, it overflows. It was deprecated in C99 and removed from C11. Always use fgets() with an explicit size.

Q3. What does snprintf() return and why must you check it?snprintf() returns the number of bytes that would have been written if the buffer were large enough (not counting the null terminator). If this value is >= the buffer size, truncation occurred. A truncated string may be meaningless or dangerous and must be handled explicitly.

Q4. Why might strncpy() still be unsafe despite having a length limit?strncpy() does not guarantee null termination if the source string is longer than the specified length. The destination may not be null-terminated, causing subsequent string operations to read beyond the buffer. Always explicitly add a null terminator after strncpy().

Q5. What is ASLR and how does it mitigate stack crashing?Address Space Layout Randomization randomly places the stack, heap, and library locations at different virtual addresses each time a program runs. An attacker who hard-codes the address to jump to in their exploit will likely jump to garbage instead of shellcode, crashing the program instead.

Q6. What is a stack canary (stack protector)?A random value placed between local buffers and the saved return address on the stack. Before a function returns, the runtime checks if the canary value is still intact. If a buffer overflow overwrote the canary, the program terminates rather than executing attacker code. Enabled with gcc’s -fstack-protector flag.

Next: DoS Attacks & Check Return Values โ†’
Denial of service, load throttling, fail safely

Continue to Part 10 โ† Part 8

Leave a Reply

Your email address will not be published. Required fields are marked *