Identifying insecure C Code with Valgrind and fixing with Snyk Code
2024年9月24日
0 分で読めますC and C++ remain foundational in critical software development. These languages power a wide array of systems, from embedded devices to high-performance applications in manufacturing, operational technology (OT), and the industrial market. Their efficiency, control over system resources, and performance make them indispensable for developers working on mission-critical projects.
C and C++ are particularly prevalent in Japan, where manufacturing and industrial sectors are key economic drivers. Japanese developers rely on these languages to build robust and efficient software that underpins everything from automotive systems to factory automation. The precision and reliability of C and C++ are crucial for maintaining the high standards expected in these industries.
The importance of code security in C and C++
While C and C++ offer unparalleled control and performance, they also pose significant security challenges. The lack of built-in safety features, such as automatic memory management, makes these languages prone to vulnerabilities like buffer overflows, use-after-free, and memory leaks. While seemingly naive and basic, these vulnerabilities can have severe consequences, especially in critical software where reliability and security are paramount.
Vulnerable C code creates a memory leak
Let’s explore an example of vulnerable C code that developers might write and introduce a security vulnerability in memory leaks.
Here’s our C program code. Can you find the security vulnerability here?
#include <stdio.h>
#include <stdlib.h>
void allocateMemory() {
int *ptr = (int *)malloc(10 * sizeof(int));
if (ptr == NULL) {
printf("Memory allocation failed\n");
return;
}
}
int main() {
allocateMemory();
return 0;
}
This program1.c
file contains an example of Dynamic Memory Allocation Vulnerability (MISRA Dir 4.12, MISRA Rule 21.3). The security vulnerability with the function allocateMemory()
is that it uses malloc()
to allocate memory but does not free it, leading to a memory leak. This can be prevented by following MISRA guidelines that restrict dynamic memory allocation.
The above is a common mistake in C programming, and it can lead to your program using more memory over time. Eventually, this can cause your program to run out of memory and crash or cause other programs on your system to run out of memory.
Why can’t we just use static memory allocation, such as int arr[10]
? Because In some situations, you can’t allocate memory on the stack. For example, let's say you're writing a function to read a file into memory. You don't know the file size until runtime, so you can't use a fixed-size array. Instead, you can use malloc
to allocate exactly the right amount of memory once you know the file size.
Run the program by compiling first and then executing it:
$ gcc program1.c -o program1
$ ./program1
Is there a memory leak in the program? How can you fix it?
Find the C security vulnerability with Valgrind
Valgrind is a powerful tool for finding memory leaks in your program. We can run the program with Valgrind to see if there are any memory leaks.
Install Valgrind on your system. If you use a Linux-based system, you can install it using the package manager. Here's a Debian-based example:
sudo apt-get install valgrind
Note: If you're on macOS and using ARM-based chips, support for Valgrind is not available, so we'll skip it and run Valgrind from within a Docker container:
docker build -t "valgrind" . -f Dockerfile
Then run the container and map our current directory to the container's /tmp
directory:
docker run -it -v $PWD:/tmp -w /tmp valgrind
Then compile the program1.c
program within the container:
gcc program1.c -o program1.app
We can then run the program with Valgrind to check for memory leaks:
valgrind --leak-check=full ./program1.app
Then you should see the output from Valgrind that shows the memory leak:
/tmp # valgrind --leak-check=full ./program1.app
==29== Memcheck, a memory error detector
==29== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==29== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==29== Command: ./program1
==29==
==29==
==29== HEAP SUMMARY:
==29== in use at exit: 40 bytes in 1 blocks
==29== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==29==
==29== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==29== at 0x48E978C: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-arm64-linux.so)
==29== by 0x108823: allocateMemory (in /tmp/program1)
==29== by 0x108857: main (in /tmp/program1)
==29==
==29== LEAK SUMMARY:
==29== definitely lost: 40 bytes in 1 blocks
==29== indirectly lost: 0 bytes in 0 blocks
==29== possibly lost: 0 bytes in 0 blocks
==29== still reachable: 0 bytes in 0 blocks
==29== suppressed: 0 bytes in 0 blocks
==29==
==29== For lists of detected and suppressed errors, rerun with: -s
==29== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Valgrind is great, but as a day-to-day C developer, this workflow doesn’t scale and requires you to compile and build a full-fledged program to be analyzed.
Snyk Code will help you identify this vulnerability in your codebase without even going through the compilation process. Just installing the extension and opening the file will show you the vulnerability because Snyk Code is a static code analysis tool that applies machine learning techniques to identify static code that doesn't require a build and compilation step. This approach enables a reliable, low false-positive, and fast feedback loop when Snyk scans your code for security vulnerabilities.
Detecting Path Traversal, Buffer Overflow, and other C vulnerabilities
The SAST engine that powers Snyk Code can detect more vulnerability types than just malloc
memory leaks.
Let’s see a more complex example that glues several insecure code practices in C that introduce security vulnerabilities:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(int argc, char *argv[]) {
// get the filename from the first command line argument
char *filename = argv[1];
// append the filename to the current directory
char path[50] = "./";
strcat(path, filename);
FILE *file = fopen(path, "r");
if (file == NULL) {
printf("Error opening file!\n");
exit(1);
}
// read the contents of the file into memory and print the size of the file:
fseek(file, 0, SEEK_END);
long fsize = ftell(file);
fseek(file, 0, SEEK_SET);
char *string = malloc(fsize + 1);
fread(string, fsize, 1, file);
free(string);
printf("Size of the file: %ld\n", fsize);
printf("Contents of the file: %s\n", string);
if (string != NULL) {
free(string);
}
if (fsize == 0) {
string[0] = 'A';
}
fclose(file);
return 0;
}
The example C program above is riddled with vulnerabilities, yet it is a very concise and naive way of introducing insecure code for the sake of brevity. Still, some subtle code practices could easily make it into real-world code bases.
Once the program is compiled, you can run it:
$ ./program3.app "text.txt"
Given that you have a file named text.txt
in the same directory and it is not empty, the program will read the file and print its contents.
For example:
Size of the file: 44
Contents of the file: FROM alpine:latest
RUN apk add g++ valgrind
Looks ok. What happens if you pass a file that traverses the directory structure?
$ ./program3.app "../../../../../etc/passwd"
The program has a path traversal vulnerability, which allows an attacker to read sensitive files on the system.
To test other vulnerabilities, try to:
Pass a file that does not exist
Pass an empty file
Pass a file name or full path that is too large (more than 50 characters)
Some of these security issues extend well beyond a developer’s C programming skills, such as path traversal, which requires awareness of application security more than safe memory management and well-established C development expertise.
Lucky for us, when you paste the program’s code to the IDE, the Snyk extension analyzes the C code for any insecure coding conventions and common application security issues. It will report them fast and with in-line code context:
Secure your C and C++ code with Snyk
A security breach in the manufacturing and industrial sectors can lead to catastrophic outcomes, including operational downtime, safety hazards, and financial losses. Ensuring code security in C and C++ is not just a best practice; it's a necessity. Snyk provides developers with the means to identify and remediate vulnerabilities early in the development process.