CC Blog Design Solutions

Hands-On Buffer Overflow

Written by Colin O'Flynn

Experimenting with Embedded System Vulnerabilities

If you’ve been involved with embedded programming, you’ve certainly heard about buffer overflow attacks. But have you ever tried one yourself? In this article, we’ll explore a basic buffer overflow and demonstrate it on a low-cost Raspberry Pi Pico. The same principles apply to other embedded targets, such as Arduino or STM32 boards.


  • What is a buffer overflow and why is it dangerous?
  • How can a buffer overflow bypass password checks?
  • How does memory layout impact embedded vulnerabilities?
  • How can Python be used to test embedded exploits?
  • What are effective ways to prevent buffer overflow attacks?
  • Raspberry Pi Pico
  • Python
  • ARM GCC
  • Raspberry Pi | https://www.raspberrypi.com
  • Compiler Explorer | https://godbolt.org

Several of my articles have talked about memory safety, for example when introducing the CHERIoT memory-safe architecture and the Sonata board I helped design to demonstrate some of these features in a soft-core device (see the November 2023, Issue #400 and May 2025, Issue #418)[1]. But one question that a few people asked me in response to those articles is how do attacks on memory work in practice? While people seem to know about buffer overflows and similar attacks that put your embedded system at risk, they haven’t often run those attacks themselves, or at least looked into how they work at a low level.

In this article I’m going to demonstrate how you can experiment with a small embedded ARM target (I’m going to use a Raspberry Pi Pico) and perform a basic buffer overflow attack. Before we get into that, let’s clarify what buffer overflows are, and how they can be exploited in an attack.

Buffer Overflow Attacks

Buffer overflow attacks are closely associated with C programming, particularly in embedded systems. This is because C frequently uses pointers to arrays or strings, yet lacks built-in mechanisms to track array lengths.

The other reason C makes buffer overflow attacks so easy is how variables get arranged in memory for your standard embedded system. They will be bunched together in a relatively predictable fashion, and the end of one variable (such as a buffer) will run right into other variables. The same memory space also has program-flow control variables such as your stack, which contains the return address that tells the program where to jump to next.

The last point is the start for a more advanced version of the buffer overflow attack. In this version of the buffer overflow attack we overwrite the return addresses on the stack to change the program flow. Overwriting the return address could allow us to pass the program flow to some other function we want to run directly (such as a function that exposes an administrative interface), a program we have loaded into memory somewhere else, or building a program entirely out of existing functions.

But in this article we’ll start with just overwriting some of your memory, and not change the control flow. Figure 1 shows the stack for an example function, that has a 64-byte buffer lower in memory location than a secret password and the return address. If we try to access element 65 of the buffer, using for example an error in our C code that accesses element buffer[64] (remembering that buffer[0] is the first element, so accessing buffer[64] is a classic “off by 1” error), we’d actually be accessing an element of the password variable.

Figure 1
An example of the memory layout that we’ll have in our software.
Figure 1
An example of the memory layout that we’ll have in our software.

This sort of setup is perfect for a buffer overflow attack. Let’s transition this to an embedded system and see what our actual code looks like.

Overflowing Passwords

Listing 1 includes a simple password check that we’ll bypass with a buffer overflow attack. This example loads a secret password into the local variable correct_password, using a function called load_secret_password() which isn’t defined in Listing 1. The correct password is expected to be a simple null-terminated C string that gets loaded into the correct_password variable.

Listing 1
This password check contains a buffer overflow vulnerability, which will allow an attacker to bypass our password check.

#include <stdio.h>#include “string.h”#define PASSWORD_OK    1#define PASSWORD_FAIL  2void load_secret_password(char * buffer);void clear_secret_password(char * buffer);void flush_input(void);#define PASSWORD_OK    1#define PASSWORD_FAIL  2int receive_and_process_password(void){    //Local variables    char c;    char * buf;    int auth_level;    char correct_password[16];    char command_buffer[64];    //Password gets set to incorrect on comparison fail    auth_level = PASSWORD_OK;    //Load secret password dynamically    load_secret_password(correct_password);    //Command buffer pointer - set first character to    //something defined so loop enters below    buf = command_buffer;    c = 0;    //Receive until user hits enter (newline or CR)    while ((c != ‘\n’) && (c != ‘\r’)){        c = getchar();        *buf++ = c;    }    flush_input();    //Compare against length of stored password    //Avoid a break statement in this to reduce timing leakage    for(int i = 0; i < strlen(correct_password); i++){        if (correct_password[i] != command_buffer[i]){            auth_level = PASSWORD_FAIL;        }    }    return auth_level;}

We can assume before calling this function the user was prompted to enter the password. The user than enters a password and presses the enter key, which is received in the while() loop. Because the user password isn’t expected to be very long, this C code simply reads until it encounters either a newline (\n) or a carriage return (\r) indicating the enter key was pressed.

The fact there is no check on the length of the received data sets up our buffer overflow attack. An attacker can send arbitrary lengths of data (provided there is no newline or carriage return value), which will get written to memory beyond the buffer. While you might think this code isn’t very realistic, this exact sort of check occurs all the time in embedded codebases.

Often the checks are omitted because the transmitting side is expected by design to behave correctly. If this program was written with the assumption that a computer is transmitting the password for example, it might be part of the specification that a password if maximum length 64 is transmitted. Of course, the problem here is that an attacker can ignore those assumptions. The vulnerability may sit there for many years, since in normal operations it never triggers any crashes or other issues.

Or perhaps an original version of the system was receiving data from a length-limited input device, such as a small keyboard with an attached LCD. If later versions of the system change the input device, it may open a vulnerability up to exploitation that was always hidden behind another layer of defense.

If you want to understand how the code in Listing 1 can be exploited, it helps to compile this on our target device. Before I target the hardware, you can use the online compiler explorer (available at godbolt.org), which I used in previous articles to see how this gets converted to assembly code.

The code in Listing 1 is available in my companion repository if you’d like to save your fingers from typing it out. Using the “ARM gcc 9.2.1 (none)” compiler on Compiler Explorer, with no optimizations set (the default) gives you a nice color-coded display of assembly and C code. You can see an overview of this in Figure 2, but you’ll need to run this yourself to see it in full resolution.

Figure 2
The compiler explorer allows me to enter the C code from Listing 1 on the left, and see the resulting assembly on the right.
Figure 2
The compiler explorer allows me to enter the C code from Listing 1 on the left, and see the resulting assembly on the right.

I’ve included a relevant snippet of the resulting assembly code in Listing 2, and added some of my own annotations. What is important is that you can see the address of various variables here, which are stored relative to the address of the “frame pointer” (fp register in the assembly).

Listing 2
The start of the function shows the stack layout, with the buffer stored below (fp-100) the correct password (fp-36), where fp is the address of the current frame pointer.

receive_and_process_password():// Store current frame pointer (fp) and// link register (lr)push    {fp, lr}// Store new frame pointer (fp)add     fp, sp, #4// Set auth_level to "PASSWORD_OK" (#1)mov     r3, #1// auth_level stored at fp-16str     r3, [fp, #-16]// correct_password stored at offset fp-36 sub     r3, fp, #36// load correct_passwordmov     r0, r3        bl      load_secret_password(char*)// buffer stored at offset fp-100sub     r3, fp, #100... rest of code ...

The buffer (which we can overflow) is stored at an address of fp-100, and the correct password that will be compared against it is stored at a higher address of fp-36. If we overwrite the 64-byte buffer, it will flow directly into the correct_password string.

The reason this matters at all is because of the password comparison on Lines 44-48 of Listing 1. This password comparison uses the strlen() function to ensure the comparison happens only against the valid length of the correct password, and means the correct password (which is loaded from some secure memory elsewhere) can be of arbitrary length.

If one was to overwrite correct_password with null characters (0x00 in C), the password comparison is skipped entirely. The loop is never entered and the passwords are declared to match (which is true). You could also stuff the buffer with a null-terminated string, and then also stuff correct_password with a matching string. Because my input loop accepts null characters, it gives you a lot of flexibility in your attack!

Now that you can (hopefully) see how the attack could work, lets implement this on a real device.

A Physical Implementation

You can choose to implement this on almost any device you want. One minor caveat is that you need to ensure the stack layout stays the same, so that the buffer overflow happens. The order of stack variables is implementation-dependent, but GCC seems to allocate arrays such that the last declared array is the lowest address. You also need to implement the missing functions, and I suggest wrapping the target function in a macro to disable optimizations. The reason being that the C compiler may realize you don’t load a variable password, but are just loading a specific known password, and optimize some of these calls away. The attack will still work with optimizations on (you can see this yourself if you use the -Os flag on the compiler explorer), but the output code varies a little more so it’s harder for me to give you fixed examples in this article.

You could also do other things like force the compiler to allocate the variables to a fixed address by making them global or function-static. These sorts of fixed buffers are also common on embedded systems, where you may have a single input or command buffer. But keeping the buffers on the stack will let me build on this example in future articles when I look at stack smashing attacks.

I used a Raspberry Pi Pico to implement this for testing, as it provides a simple USB serial interface, and the very low-cost devices are perfect if you wanted to run this for a larger audience. This should also work for the sole purpose of a buffer overflow demonstration you could run this on a computer too, but the point of targeting an embedded system is to allow us to see how we interact with the system using typical hardware tools.

When building the example code, enable a listing or disassembly output as well. This will let you see how your compiler generated the output code, and you can try to find the location of the stack variables in that output. My examples in the companion repository include makefile settings to do this.

I haven’t included the full implementation in the article listings to save space, but you can see the full details in my companion repository for this article.

Python Poking

I find it easiest to experiment with a system using a simple Python script. This lets me send arbitrary data, and I can switch between normal ASCII data and binary data easily. To cut to the final solution, Listing 3 shows a simple Python script that tries sending from 1 to 100 NULL (0x00) characters followed by a newline and carriage return.

Listing 3
This Python attack script sends “\x00”, which means a null (hex 0x00) an increasing number of times followed by a simulated enter press.

import serial#Pico USB Serial - no baud needed, but need to set dtrser = serial.Serial(“com5”, timeout=2)ser.dtr = Truefor i in range(60, 200, 1):    ser.write(b”\xff”*i + b”\n\r”)    data = ser.read(100)    print(“%4s :”%i + str(data))

A snippet of the results from running this output is given in Listing 4. You can see that after sending 65 nulls we successfully “break into” the system!

 Listing 4
A snippet of the output from running Listing 3, showing a successful attack from overwriting the secret password.

 62 :b’INVALID PASSWORD\r\nPassword Please:\r\n’  63 :b’INVALID PASSWORD\r\nPassword Please:\r\nINVALID PASSWORD\r\nPassword Please:\r\n’  64 :b’INVALID PASSWORD\r\nPassword Please:\r\n’  65 :b’ACCESS GRANTED - WELCOME\r\n’

But how might you guess this system is vulnerable to such an attack in real life? The best way would be to have access to the source code, such as if you found an exploit in a library used by the device. More likely would be that you might find access to the binary and need to reverse engineer the software to find this vulnerability.

But you might be able to test for these sorts of vulnerabilities by using fuzzing. If you suspect a vulnerability exists, you could send longer and longer strings of null (zeros) or another value as a simple test. You might expect the system to crash at some point if you overwrote the stack or any other variable. In my example this bypasses the security, but normally we wouldn’t get so lucky. We’d expect first to see some other misbehavior, and observing the misbehaviour suggests some underlying memory problem we can exploit.

In fact, to trigger this behavior on this target I need to send something non-zero. The problem with using zero is if I do overwrite the return address, it will jump to address 0, which just restarts the device. Instead if I modify Listing 2 to send \xff (0xFF in hex), I’ll get the output of Listing 5.

 Listing 5
A snippet of the output when I send 0xFF instead of 0x00, where the device crashes when I overwrite the return address as it tries to branch to an invalid address.

 86 :b’INVALID PASSWORD\r\nPassword Please:\r\n’  87 :b’INVALID PASSWORD\r\nPassword Please:\r\n’  88 :b’INVALID PASSWORD\r\nPassword Please:\r\n’  89 :b’INVALID PASSWORD\r\nPassword Please:\r\n’  90 :b’’

In this example the device crashes after sending 90 bytes with 0xFF. This corresponds to the size of the buffer (64 bytes) plus the password (16 bytes) plus the additional three 4-byte local variables that get put on the stack (which gets us to 92 bytes). If some variables are stored in a local register it won’t have the stack usage, so writing 90 bytes just about perfectly aligns with when we start overwriting the return value, depending on if some variables don’t take stage usage. This also varies from the stack sizing from Listing 2 as I’m using a different compiler than I used online, and have some changes in the switches provided to the compiler.

As a final hint on this example, you can also explore how input sanitization appears to be occurring (or not occurring). For example, if I knew what the correct password is, I could try appending non-ASCII characters to see if it causes the password to be rejected. If the correct password is “circuitcellar”, I might try sending first “circuitcellar\x01” and then “circuitcellar\x00”, where \x01 means adding hex 0x01, and \x00 means adding a null (0x00).

If the password with a 0x00 character is accepted but the password with 0x01 is rejected, it means the code is not stripping “non-ASCII” characters (which a human could not type). If both are rejected, it means the code is probably stripping the characters and replacing them with something else, which is a good sign that it is sanitizing the input data. If both passwords are accepted it suggests it’s dropping non-ASCII characters, which again would make the attack more difficult to apply.

Contain your Buffers

Of course the most fundamental fix is to first have length checking on the input, so it’s impossible to overwrite the buffer. But it’s still a good idea to limit (sanitize) the input data to valid ASCII characters if you expect the password to only contain valid ASCII characters.

The reason is that by allowing a user to write arbitrary data to memory, they could actually be writing program code that will be called from somewhere else. How would the user call such a program? The answer is a buffer overflow attack! Doing this requires a bit more effort, as they need to know where the buffer lies in memory. But by overwriting the return address, they could actually jump to code they have written in memory.

That is a topic for another article, as we’ll have to explore how to build a simple dumper first. But for now I hope this article has illustrated how buffer overflow attacks can be introduced into C programs, and provided you with reference code for your own experimentation.

As always, I’ve posted copies of the programs to my companion repositories [2]. And if you’re interested in learning about more fundamental ways we might protect future systems from these sorts of attacks, you can read my introduction to the CHERIoT technology in the Issue #400 article of Circuit Cellar (also available on the Circuit Cellar website) [3]. 

RESOURCES
Raspberry Pi | https://www.raspberrypi.com
Compiler Explorer | https://godbolt.org

REFERENCES
[1] “Experimenting with CHERI on the Sonata Board”, Issue #418 of Circuit Cellar.
[2] Companion Repository (source code): https://github.com/colinoflynn/circuitcellar-EmbeddedSystemEssentials
[3] “How CHERI Helps Secure Your C/C++ Code”, Issue #400 of Circuit Cellar. https://circuitcellar.com/research-design-hub/design-solutions/how-cheri-helps-secure-your-c-c-code/

Code and Supporting Files

PUBLISHED IN CIRCUIT CELLAR MAGAZINE • JANUARY 2026 #426 – Get a PDF of the issue

Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.


Note: We’ve made the Dec 2022 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

— ADVERTISMENT—

Advertise Here

Sponsor this Article
Website |  + posts

Colin O’Flynn has been building and breaking electronic devices for many years. He is an assistant professor at Dalhousie University, and also CTO of NewAE Technology both based in Halifax, NS, Canada. Some of his work is posted on his website (see link above).

Supporting Companies

Upcoming Events


Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2026 KCK Media Corp.

Hands-On Buffer Overflow

by Colin O'Flynn time to read: 12 min