What Is a Buffer Overflow? The Bug That Keeps Driving Critical CVEs

A buffer overflow happens when a program writes more data into a memory buffer than the buffer can hold. The extra bytes land in adjacent memory, corrupting whatever was there. If an attacker controls what those bytes contain, they can overwrite a function’s saved return address, function pointer, or heap metadata to redirect the program to code of their choosing. That is arbitrary code execution, and it has been the root cause of critical vulnerabilities in operating systems, network devices, and ubiquitous libraries for three decades.

The bug pattern has not changed. What has changed is where it keeps showing up. In May 2025, Fortinet disclosed CVE-2025-32756: a stack-based buffer overflow in FortiVoice, FortiMail, FortiNDR, FortiRecorder, and FortiCamera that an unauthenticated attacker could trigger with a single HTTP request to reach root-level code execution. The vulnerability was already being exploited before the advisory went out. Understanding how buffer overflow works is not optional for anyone reading security advisories seriously.

What Is a Buffer and Why Does It Overflow?

In C and C++, a buffer is a contiguous block of memory allocated to hold a fixed amount of data. You decide the size at declaration: char hostname[256] reserves 256 bytes for a hostname. Keeping writes within that boundary is entirely the programmer’s responsibility. C provides no automatic protection.

The dangerous functions are well known but still common in production codebases. gets() reads input until a newline with no length limit. strcpy() copies a string until a null byte with no destination size check. sprintf() formats into a buffer with no bound. Any of these can write past the end of the allocation they target. The safe alternatives, fgets(), strncpy(), and snprintf(), accept an explicit maximum length and stop there.

Stack-Based Buffer Overflow: How the Return Address Gets Hijacked

The call stack is where function execution state lives. When a function is called, the processor stores the return address (where execution should resume after the function exits), the saved frame pointer, and local variables including buffers. In a typical stack frame, the buffer sits below the saved frame pointer and return address in memory.

Write past the end of the buffer and you reach the saved frame pointer. Write further and you overwrite the return address. Overwrite it with the address of your shellcode or a ROP gadget and, when the function executes its ret instruction, execution jumps there instead of to the legitimate caller.

Early exploitation placed shellcode directly in the overflowing input and overwrote the return address to point back into it. A NOP sled, a run of no-operation instructions before the shellcode, gave the attacker tolerance for imprecision: any jump into the sled would slide forward to the shellcode. Modern defences made that approach unreliable, but they did not eliminate the underlying bug class.

Heap Overflow: Harder to Exploit, Same Outcome

Heap overflows corrupt memory allocated dynamically at runtime. Because there is no return address adjacent to a heap buffer, attackers instead target heap metadata structures, adjacent object fields, or function pointers stored nearby. The exploitation path requires understanding the allocator’s internal layout, which varies by platform and implementation. But the end result, arbitrary code execution, is the same.

CVE-2023-38545, the curl SOCKS5 heap overflow, shows how subtle these bugs can be. When a SOCKS5 proxy handshake took long enough to flip a state variable incorrectly, libcurl called memcpy() to copy a hostname of up to 65,535 bytes into a 256-byte buffer. The overflow path was reachable only under specific timing conditions: a slow enough connection could trigger the branch. libcurl 8.4.0, released 11 October 2023, corrected the flaw.

How Attackers Get Past Modern Defences

Three mitigations changed the exploitation landscape significantly: stack canaries, DEP/NX, and ASLR. Each one raised the bar. None eliminated the problem.

Stack canaries are random values the compiler places between local buffers and the return address. Before the function returns, the runtime checks whether the canary is intact. A corrupted canary triggers immediate process termination. GCC’s -fstack-protector-strong flag enables canaries; most Linux distributions compile with it on by default. Canaries stop naive overwrite attacks but do not help if the attacker can read the canary value before crafting the payload, or if the overflow skips over the canary to hit something else.

DEP and NX mark memory regions as either writable or executable, not both. Shellcode injected into a data buffer cannot run. This pushed attackers toward code-reuse techniques.

Return-Oriented Programming

ROP chains together small sequences of existing executable instructions, called gadgets. Each gadget ends in a ret instruction. By stacking gadget addresses on the stack and pointing the hijacked return there, an attacker builds arbitrary logic using only code already present in the binary. No new code needs to be executable. This technique works even when the stack and heap are fully non-executable.

ASLR randomises the base addresses of the stack, heap, and loaded libraries each time a process starts. Without knowing where a gadget lives, an attacker cannot build a working ROP chain. Bypasses combine ASLR with a separate information-disclosure vulnerability that leaks an address, then feed that value into the overflow. On 32-bit targets the address space is small enough that brute-force attempts succeed in a few hundred requests.

Once code execution is achieved, the attacker’s next steps depend on the process context. A service running with limited privileges typically triggers a privilege escalation exploit to reach root or SYSTEM. On a remote network service, deploying a reverse shell establishes persistence so the attacker can continue working interactively while staying inside the network perimeter.

CVE-2025-32756: A Current Case Study

The Fortinet overflow illustrates how these bugs reach production in network-facing code. The vulnerable code handled the enc parameter in the /remote/hostcheck_validate endpoint. A call to EVP_DecodeUpdate() decoded an authentication hash into a buffer that was undersized for the decoded output, writing past its end on the stack. The endpoint required no authentication. An attacker could send a crafted HTTP request and gain root code execution on the device.

Before Fortinet published the advisory, threat actors were already scanning device networks through the FortiVoice interface, erasing crash logs to cover their tracks, and enabling debug logging to capture SSH login credentials in cleartext. The vulnerability was added to CISA’s Known Exploited Vulnerabilities catalogue on 14 May 2025.

What Actually Reduces Risk

The most durable fix is to write new code in a memory-safe language. Rust enforces bounds checking at compile time for most operations and adds runtime checks where static analysis cannot prove safety. Go, Python, Java, and C# all enforce bounds at runtime. CISA and the NSA have each published guidance recommending that organisations avoid C and C++ for new development where memory-safe alternatives exist.

For existing C and C++ code, the priority actions are: replace unsafe functions (gets, strcpy, strcat, sprintf) with their length-limited equivalents; compile with -fstack-protector-strong, -D_FORTIFY_SOURCE=2, PIE, and full RELRO; and run fuzzing with AddressSanitizer before release. These do not eliminate the bug class but catch the most straightforward instances before they reach production.

Static analysis identifies calls to dangerous functions automatically. Fuzzing with ASAN finds overflows that reach runtime. Neither substitutes for code review, but both run continuously in a CI pipeline at low cost.

Frequently Asked Questions

Is buffer overflow still relevant in modern software?

Yes. Buffer overflow vulnerabilities appear in current security advisories at high severity. CVE-2025-32756 and CVE-2024-49138 (Windows CLFS heap overflow, exploited in the wild before the December 2024 patch) are recent examples. The prevalence has declined in code written in memory-safe languages, but C and C++ remain dominant in network infrastructure, firmware, and systems software.

What is Return-Oriented Programming?

ROP is an exploitation technique that bypasses non-executable memory protections. Instead of injecting shellcode, an attacker chains together short instruction sequences (gadgets) that already exist in the process’s executable memory. Each gadget ends in a ret instruction, which pops the next address off the stack and continues the chain. With enough gadgets, an attacker can build arbitrary logic without writing a single byte of new executable code.

What is the difference between a buffer overflow and a stack overflow?

In security contexts, a buffer overflow refers to writing past the end of any memory buffer. A stack overflow specifically means exhausting the stack’s memory limit, typically through deep or infinite recursion. The two are often confused because stack-based buffer overflows are the most common exploitable variant, but the terms describe different conditions.

Which functions in C are most dangerous for buffer overflow?

gets() is the worst: it accepts unbounded input and has no safe use case. strcpy(), strcat(), sprintf(), and vsprintf() all lack destination length parameters. Replace them with fgets(), strncpy(), strncat(), and snprintf() respectively. Compile with -D_FORTIFY_SOURCE=2 to instrument remaining calls with runtime size checks.

How do you detect buffer overflows before deployment?

AddressSanitizer (-fsanitize=address) instruments the binary to detect out-of-bounds reads and writes at runtime with low false-positive rates. Combine it with a fuzzer such as AFL++ or libFuzzer to generate boundary-testing inputs automatically. Static analysis tools like CodeQL and Coverity flag unsafe function calls without running the code. All three are most effective when run continuously in CI rather than as one-off audits.

Related posts

Programming Languages for Cyber Security: What the Tools Actually Use

Linux Server Hardening: What to Do First and Why It Matters

VPN Internals Explained: Protocols, Leaks, and What the Kill Switch Actually Does