Monday, August 17, 2020

Safety and the programming languages - Buffer overflow errors

Buffer overflow errors are one of the most common sources of vulnerabilities in reallife systems. They usually occure when the programmer doesn't check the length of some unreliable input and just copies it into a short buffer. In the following example, we have a buffer called userinput and after the buffer we have a flag. The program asks for a password and if the password is correct, it sets the authenticated flag to true.

#include <stdio.h>
#include <string.h>

struct {
   char userinput[10];
   char authenticated;
} user;

const char *password = "aaaaa"; // Comes from a secret source

int main() {
    
    printf("Best rocket controller shell\n\n");
    while (1) {
        if (user.authenticated) {
            printf("Should I start the rockets? ");
            scanf("%s", user.userinput);
            if (strcmp(user.userinput, "yes") == 0) {
               printf("Rockets started\n");
            }
        } else {
            printf("Give me the password! ");
            scanf("%s", user.userinput);
            if (strcmp(user.userinput, password) == 0) {
               user.authenticated = 1;
            }
        }
    }
}
The problem is that the user can write as long password as he/she wants. If the input is longer than 9 bytes (don't forget about the zero delimiter in C), scanf will continue to copy it to the memory and it will overwrite the variables after the userinput. So, if the attacker writes at least 11 bytes, he/she can modify the authenticated flag, because its memory address is after userinput. These kind of errors cause crash most of the time (the program tries to modify an invalid memory address or its state becomes invalid), but sometimes an attacker can get elevated privilege or run custom code with a correctly crafted input.
Fixing this code is very easy after you found the error. You only have to replace scanf("%s", user.userinput); with scanf("%9s", user.userinput);. Also, every buffer overflow error could be eliminated by checkig the length of the data every time you want to copy it into a buffer. The fact that security experts find buffer overflow vulnerabilities in almost every week proves that saying it is easier than doing it. Luckly for us, most languages (every interpreted language and most newer compiled language) wont let you reach indexes outside of your array.
Python, Java, C# and Rust for example will produce a runtime error if you try to write outside of your array. You can catch it and handle it, but if you don't say explicitly what to do, your program just interrupts. JavaScript follows a different path, it will simply increase your array in this situation.
There are some cases, where the compiler can see that there will be a runtime error. For example, this code shouldn't be compiled:

#include <stdio.h>

int main() {
    int a[4] = {0, 1, 2, 3};   
    printf("Value of nonexisting index: %i", a[10]);
}
Of course, C will compile it without any problem and you can run the generated binary. We can write this program in Kotlin, where compiling it is also possible:

fun main() {
    val a = arrayOf(0, 1, 2, 3);
    println("Value of nonexisting index: ${a[10]}");
}
Luckly, we can't run the produced binary, it will throw a runtime error. (ArrayIndexOutOfBoundsException)
Lets write this example in Rust, too:

fn main() {
    let b = [1, 2, 3, 4];
    println!("{}", b[4]);
}
The Rust compiler won't even compile this program:

error: this operation will panic at runtime
 --> src\main.rs:3:20
   | 
 3 |     println!("{}", a[10]);
   |                    ^^^^^ index out of bounds: the len is 4 but the index is 10
   | 
   = note: `#[deny(unconditional_panic)]` on by default
This feature can protect you from some runtime errors, but please, keep in mind that the compiler is not an oracle. You can easly produce runtime errors, if you want (or if you don't check every input):

use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();

    let a = [1, 2, 3, 4];
    let b = rng.gen::<usize>();
    println!("{}", a[b]);
}
So, the conclusion: Buffer overflow errors can be very serious, hard to detect, easy to fix (most of the time) and (luckly) most modern programming languages can detect them at least runtime. Of course, runtime detection has its cost, usually every array access needs at least one comparision (that's why C and C++ won't do it for you just if you explicitly ask for it).

No comments:

Post a Comment