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.
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.
| … caller’s frame … |
| Return Address โ caller |
| Saved frame pointer |
| buf[128] (safe) |
| … caller’s frame … |
| Return Addr โ SHELLCODE! ๐ด |
| Overwritten (junk) |
| buf[128] filled with shellcode + padding |
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 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 |
#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;
}
#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
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.
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:
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.
Modern x86 hardware marks stack pages as non-executable. Code on the stack cannot be run, defeating classic stack crashing.
GCC’s -fstack-protector places a random “canary” value between the buffer and return address. If it’s overwritten, the program aborts.
