Close
0%
0%

Simple Security Risk Examples on Arduino

Simple Arduino examples of common cyber-security risks, such as buffer overflows and stack smashing

Similar projects worth following
1.6k views
I am not a security expert, but have long been interested in the field.

As a learning exercise for me, and hopefully for others, I am putting together examples of C/C++ security risks for use on the Arduino platform. Each example is intended to be as "simple as possible and no more." Each one is set up as a challenge. You will need to understand the risk to achieve the goal. You will need some knowledge of how a computer works to succeed, but hopefully they will be understandable to most makers.

Note these are not security exploits of Arduino, but rather are common issues with C/C++ in general, with Arduinos used to show the example.

If you are a security expert I'd love reviews or expansions of my explanations or contributions of further examples!

Examples:

  1. Basic buffer overflow example
  2. Stack data leakage
  3. Simple code execution from buffer overflow

  • Example 3: Buffer overflow with function pointers

    Jake Wachlin09/19/2020 at 16:14 0 comments

    This next example is a simple way to cause code execution using a buffer overflow bug. Like all the examples it is on the Github and here.

    typedef struct
    {
      uint8_t buffer[16];
      void (*cb)(int);
    } databall_t;
    
    void other_secret_function(int a)
    {
      Serial.println("Secret function found!");
    }
    
    void secret_function(int a)
    {
      Serial.println("Secret function found!");
    }
    
    void handle_sum(int s)
    {
      Serial.print("Sum of buffer: ");
      Serial.println(s);
    }
    
    void handle_data(databall_t * db)
    {
      Serial.println("Summing data");
      int sum = 0;
      for(int i = 0; i < 16; i++)
      {
        sum += db->buffer[i];
      }
      db->cb(sum);
    }
    
    void setup() {
      Serial.begin(115200);
      delay(1000);
    
      Serial.println("Booting...");
      delay(100);
    
     
    }
    
    void loop() {
      delay(1000);
      Serial.println("Loop still running");
    
      databall_t db;
    
      char pointer_addr[128];
      snprintf(pointer_addr, 128, "Secret function address: %p", secret_function);
      Serial.println(pointer_addr);
    
      snprintf(pointer_addr, 128, "Other secret function address: %p", other_secret_function);
      Serial.println(pointer_addr);
    
      snprintf(pointer_addr, 128, "Handle sum function address: %p", handle_sum);
      Serial.println(pointer_addr);
    
      Serial.print("Size of databall: ");
      Serial.println(sizeof(db));
    
      db.cb = handle_sum;
    
      int start = millis();
      int index = 0;
      Serial.println("Enter the data");
      while((millis() - start) < 5000)
      {
        if(Serial.available())
        {
          db.buffer[index++] = Serial.read();
        }
      }
      handle_data(&db);
    
    }

    We make a new struct which has a data buffer and a function pointer for a callback function. Although contrived for this example, this setup is not uncommon. For example, a communication driver for UART, I2C, or SPI might have a buffer for data and a callback function to call when the transaction is complete for further data processing. Here, we sum the data and call the callback function with that sum.

    Yet again, we are not checking the length of the buffer as we fill it with data, so we can overflow and overwrite other variables on the stack. Because of the struct, the address of the callback function can easily be overwritten. When the summing is complete, the callback is called, which now points somewhere else. This allows an attacker to execute code they should not be able to.

    Using an Adafruit Feather M0, I see the following. We see that the size of the databall is 20, and that the secret function address is 0x2111.

    We overwrite 18 bytes, to go past the buffer onto the callback address, as shown here. The Feather M0 is little endian, so we have to provide the address as 0x11 0x21. Depending on the address, this can be difficult or impossible to enter through the Arduino serial monitor. All data through there is sent as ASCII codes, some of which can't be typed or sent.

    With this input, we see the following. The secret function is found! The hardware does not crash, since the callback function is a valid address and the hardware doesn't know any better.

  • Example 2: Stack data access

    Jake Wachlin09/19/2020 at 15:55 0 comments

    This next example shows a potential security risk in the form of leaking sensitive information. Like all these examples, the full code is on Github but also shown here.

    #include <string.h>
    
    #define BUFFER_LENGTH         16
    
    void process_secret_data(void)
    {
      char buffer[64];
      int ssn = 555555555; // Should NOT be leaked
      snprintf(buffer, 64, "SSN: %i", ssn);
    
      // Do something with SSN...
    }
    
    void do_something_else(void)
    {
      char buffer[64];
    
      Serial.println("Enter your Name");
    
      int start = millis();
      int index = 0;
      while((millis() - start) < 5000)
      {
        if(Serial.available())
        {
          buffer[index++] = Serial.read();
        }
      }
      
      Serial.print("Your name: ");
      Serial.println(buffer);
    }
    
    
    void setup() {
      Serial.begin(115200);
      delay(1000);
    
      Serial.println("Booting...");
      delay(100);
    }
    
    void loop() {
      delay(5000);
      Serial.println("Loop still running");
    
      process_secret_data();
      do_something_else();
    
    }

    Here, we have some function "process_secret_data" that does something with sensitive information. It takes some social security number and processes it. Then, we have some other function "do_something_else" that asks the user for their name. Perhaps this would be used for some sort of login.

    The main issue here is that the buffer in "do_something_else" is not zeroed out, and the buffer in process_secret_data is not cleared out before that function exits. The hardware does not clear that data either, so the buffer is not cleared out. These local variable buffers are put into similar addresses, and we can therefore access old sensitive data from a separate function.

    Without inputting anything, we get the following (on a Adafruit Feather M0). It is printing whatever is in the buffer, finds a string terminating 0, and stops.

    If we provide a short "name," we can overwrite the string terminating zero and cause it to print out more than expected. Here I provided the name "hello."

    This program now leaks the sensitive information from this other function. We really do not want this to happen. We want to compartmentalize the code so leaks like this do not occur.

  • Example 1: Simple buffer overflow

    Jake Wachlin09/19/2020 at 15:43 0 comments

    C/C++ give the developer enormous amounts of power and control. In a perfect world, that is great. The developers can exploit this to write fast, efficient code. However, it can go wrong, and often does. Many real-world security bugs are due to memory safety issues. In many cases, these are buffer overflow issues.

    This example will show a buffer overflow example that can be used to "unlock" something without knowing the "password," simply by overwritting a lock flag. Clearly this is a contrived example, but does provide some insight into the danger of these kinds of issues. The code for this example is on the linked Github repo, but is reproduced here as well.

    #define BUFFER_LENGTH   16
    
    void setup() {
      Serial.begin(115200);
      delay(1000);
    
      Serial.println("Booting...");
      delay(100);
    }
    
    void loop() {
      delay(5000);
    
      char buff[BUFFER_LENGTH] = {0};
      uint8_t unlocked_flags[32] = {0};
    
      Serial.print("Buffer address: ");
      Serial.println((uint32_t) buff);
    
      Serial.print("Unlocked flags address: ");
      Serial.println((uint32_t) unlocked_flags);
    
      int start = millis();
    
      int index = 0;
      Serial.println("Enter your password");
      while((millis() - start) < 5000)
      {
        if(Serial.available())
        {
          buff[index++] = Serial.read();
        }
      }
    
      if(strcmp("super_secret", buff) == 0)
      {
        Serial.println("Password correct!");
        unlocked_flags[0] = 1;
      }
      
    
      if(unlocked_flags[0])
      {
        Serial.println("UNLOCKED!!!!");
      }
      else
      {
        Serial.println("Still locked...");
      }
    
    }

    Here, we declare a local buffer of length 16 that will be used to be filled with user input for their password. We also set up a local array of flags used to unlock various things in the codebase. These local variables are stored on the stack. We print out the memory addresses of both of these arrays, which helps make the exploit easier. I ran this test on a Adafruit Feather M0, and I see the following:

    Here, we see that the address of the flags is 16 higher than the buffer. This means that if we can write beyond the 16 elements of the buffer, we can overwrite elements in the unlocked flags buffer. We can input a string with 18 characters, like shown below.

    Once this is entered, it is unlocked!

    There is no length checking in this example, so we keep incrementing "index" according to the input characters, and overflow the buffer.

View all 3 project logs

Enjoy this project?

Share

Discussions

sted4159 wrote 10/03/2023 at 12:27 point

Thanks for highlighting these security risks! It's crucial to safeguard our Arduino projects. One effective way to enhance security is by implementing a robust code sealer mechanism. This can prevent unauthorized access and tampering, ensuring the integrity of our Arduino applications. Great insights!

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates