Secure Coding Practices
Here we discuss a problem that is inherent to C: the fact that the language is not memory-safe. You can access the slides 🖼️ for this lecture. All the code samples given here can be found online, alongside instructions on how to bring up the proper environment to build and execute them here.
Here we will give a brief overview of some good C coding practices which minimise the amount of bugs one may introduce when writing software with that programming language.
Context
We have seen that memory safety and undefined behaviour issues are common in software written in C, and that these issues lead to security vulnerabilities that can be exploited by attackers to do various forms of bad things. What can we do about it? We can mainly do three things:
- When we develop we need to adhere to good coding practices to minimise as much as possible the changes of introducing such bugs.
- We have techniques that can help analyse our code during development and detect some bugs.
- We also have techniques that can help protect our programs in production, making exploitation more difficult and limiting the damage from successful exploits.
Here we focus on the first point, while the two others are covered next.
Array/Buffer/Integer Overflows
To prevent array buffer overflows, in C remember that they do not embed their sizes so make sure to keep track of the size of each array/buffer you use. You need to know the size of an array to know when to stop iterating, and the size of a destination buffer to know how many bytes you can copy in there.
When manipulating integers, make sure to be aware of the size reserved by the compiler to hold them in memory according to the architecture you are compiling for.
You can use sizeof() to determine these sizes.
While an unsigned integer will never overflow but rather wrap around, overflowing a signed integer leads to undefined behaviour and must be avoided. The compiler has some builtin functions that can tell you if an integer operation overflows. Here is an example of such function:
bool __builtin_add_overflow (type1 a, type2 b, type3 *res)
This function sums the integers a and b and place the result in *res.
It returns true if the addition resulted in an overflow, and false if that is not the case.
These builtin function that check for overflows are available for integer addition, subtraction, and multiplication.
See here for more details.
Unsafe Libc Functions
| Unsafe Function | Why It Is Unsafe | Safe Alternative(s) |
|---|---|---|
gets() | No bounds checking; allows buffer overflows | fgets() |
strcpy() | No bounds checking; can overflow destination buffer | strncpy(), strlcpy() (if available) |
sprintf() | No bounds checking; leads to buffer overflows | snprintf() |
scanf() | No bounds checking e.g., %s with no width | fgets() + sscanf() with width specifiers |
memcpy() | No bounds checking; can cause overflows | Use with care; consider memmove() for overlapping memory |
bcopy() | Obsolete; unsafe due to no bounds checking | memmove() |
strlen() | Not inherently unsafe, but must not be used on untrusted or unterminated buffers | Ensure string is null-terminated before use |
Above you can find a few functions of the C standard library which use should be avoided as much as possible.
You can see the reason why they are unsafe, as well as safe alternatives.
We have seen the strcpy and friends, that does not check for overflow on the buffers they write to.
You should use the safe versions as much as possible, which all have a way to indicate the size of the receiving buffer to avoid overflows.
To move memory you should not rely on bcopy but rather use memcpy with care if the source and target areas do not overlap, and memmove if they do.
Be careful with strlen, it can return numbers larger than the size of a string if that string is not properly terminated.
These are not the only functions to avoid, see this page for more.
Libc: String Manipulation Functions
Once again, regarding string manipulation functions, make sure to use the n versions that force you to indicate a maximum number of characters to process.
These are not perfect though, for example strncpy won't add the termination character '\0' at the end of the target buffer.
See for example this code in which we wish to replace string2 that is composed of 32 x's with hello world:
char string1[] = "hello, world";
char string2[32] = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
strncpy(string2, string1, strlen(string1));
printf("%s\n", string2); // prints "hello, worldxxxxxxxxxxxxxxxxxxx"
Because strncpy is not adding the termination character, we end up with a mix of both strings which is probably not what the programmer intended.
Dynamic Memory Allocation
When using dynamic memory allocation, make sure to always check malloc's return value, for reasons we have previously discussed.
Remember that after free is called upon a pointer, that pointer is invalid and should not be reused in any way.
It should obviously not be dereferenced, but its value should not be used for anything else too.
Be careful with realloc: this function will return NULL upon failure so make sure it does not overwrite the original pointer to the buffer you want to increase the size, otherwise you'll get a leak.
For example consider this code:
ptr = realloc(ptr, new_size)
This guarantees a leak if realloc fails, which is always a possibility: ptr will be overwritten with NULL, and if you don't have another pointer referencing the buffer ptr was pointing to, you will never be able to call free on that buffer.
malloc does not zero out memory returned to allocation request, so if you initialise only partially a data structure located in a dynamically allocate buffer, and you pass that data structure to a context that you do not trust, for example by sending it through the network, you may be leaking memory content to that untrusted party.
So for buffers sent to untrusted context it is better to use calloc which will zero out memory allocated.
Of course, note that this comes at the cost of a performance slowdown.
Further Readings
What we saw here are just a few examples of secure coding practices, and we do not have the time to cover them all exhaustively. Here is a list of further resources, make sure to check them out if you want to learn more:
- SEI CERT C Coding Standard
- Robert C. Seacord, Secure coding in C and C++ (book)
- ISO/IEC TS 17961 (C Secure Coding Rules)
- NASA JPL C Coding Standard
- Fedora's Defensive Coding Guide
Applying these secure coding practices is unfortunately insufficient for preventing 100% of the bugs we may introduce. After all as programmers we are human, and we can't assume every line of code we write is bug free. So we need more automated tools to detect programming mistakes.