Dangerous Standard Library Functions and Safer Alternatives

The previous chapters explained different buffer overflow exploitation techniques. However, the overflows themselves did not happen in self-written code, but in standard library functions which were called with insufficiently validated parameters. We can see that one does not only need to be careful about writing safe code but also about using provided functions that make assumptions about the passed input. The standard library of the C programming language builds the foundation for larger applications but contains several functions which expect the user to pass only validated parameters and can - if these validations do not happen - cause buffer overflows1).

Functions Writing Memory of Unspecified Length

Following functions of the C standard library are easily used incorrectly as they do not receive any information about the size of the destination buffer. Instead, they expect the user to take care of sufficient buffer sizes and correctly terminated strings. Each function description includes a demonstration of incorrect usage.

gets

gets() reads a string from the standard input and writes it to the passed buffer. There is no possibility to restrict the length of the input. This implies that every usage of gets() is vulnerable to a buffer overflow.

stdlib/gets.c
// gcc -std=c99 gets.c
#include <stdio.h>
 
int main()
{
    char buffer[10];
    gets(buffer);
    return 0;
}

:!: As there is no way to use gets() safely, it was removed from the C standard with C11.

strcpy / wcscpy

strcpy() copies a string to another memory location. The destination buffer might be smaller than the source buffer, causing an overflow at the location of the destination buffer.

stdlib/strcpy.c
// gcc strcpy.c
#include <string.h>
 
int main()
{
    char dst_buffer[10];
    char const *src_buffer = "This string is too long for the dst_buffer";
    strcpy(dst_buffer, src_buffer);
    return 0;
}

With wcscpy(), the same vulnerability applies to wide strings.

strcat / wcscat

strcat() concatenates two strings. The destination buffer might be too small to also contain the content of the source buffer in addition to the existing string. Similar to strcpy(), the destination buffer can potentially be overflown.

stdlib/strcat.c
// gcc strcat.c
#include <string.h>
 
int main()
{
    char dst_buffer[10] = "This ";
    char const *src_buffer = "string is too long for dst_buffer";
    strcat(dst_buffer, src_buffer);
    return 0;
}

With wcscat(), the same vulnerability applies to wide strings.

scanf

scanf() and its related functions (sscanf(), fscanf(), etc.) read formatted input. By default, there is no length restriction on the input which has similar effects as the gets() function.

stdlib/scanf.c
// gcc scanf.c
#include <stdio.h>
 
int main()
{
    char buffer[10];
    scanf("%s", buffer);
    return 0;
}

sprintf / vsprintf

sprintf() and vsprintf() write a formatted string to a buffer. By default, there is no length restriction on the generated string written to the destination buffer.

stdlib/sprintf.c
// gcc sprintf.c
#include <stdio.h>
 
int main()
{
    char buffer[10];
    sprintf(buffer, "Hallo %s!\n", "World");
    return 0;
}

wctomb / wcrtomb

wctomb() converts a wide character to multibyte encoding. The developer needs to make sure that at least MB_CUR_MAX bytes are available in the destination buffer, otherwise it might overflow depending on the used encoding scheme.

stdlib/wctomb.c
// gcc wctomb.c
#include <stdlib.h>
#include <locale.h>
 
int main()
{
    wchar_t wc = L'\u00df';
    char buffer[1];                     // not large enough
 
    setlocale(LC_ALL, "en_US.utf8");
    wctomb(buffer, wc);
 
    return 0;
}

wcrtomb() behaves like wctomb() but uses a narrow multibyte encoding.

mbtowc / mbrtowc

mbtowc() converts a multibyte character to its wide character representation. The actual size of wchar_t is at least 8 bits but is compiler-dependent. This means the developer is not allowed to make any other assumptions on the size of the result than being of type wchar_t. Using a smaller type than required for the conversions results in a memory corruption.

stdlib/mbtowc.c
// gcc mbtowc.c
#include <stdlib.h>
#include <locale.h>
 
int main()
{
    char wc;                            // should be wchar_t
    char buffer[] = {0xc3, 0x9f};       // ß
 
    setlocale(LC_ALL, "en_US.utf8");
    mbtowc(&wc, buffer, 2);
 
    return 0;
}

mbrtowc() behaves like mbtowc() but uses a narrow wide character representation.

Functions Writing Memory of Specified Length

Unlike those listed in the previous section, many functions of the standard library actually require the buffer size to be specified. They are safer in the sense that the developer is forced to handle buffer sizes, but can still be misused for memory corruptions due to wrongly implemented size calculations, numerical overflows, numerical underflows or buffer overflows caused by other functions overwriting already validated parameters. Although these functions are less likely to be vulnerable than those writing memory of unspecified length, they should be used carefully.

Several functions without length specification such as strcpy() and strcat() meanwhile received a secure equivalent with an update of the language standard which gets additional information about the buffer size. To keep existing code working, these new versions have a _s suffix (e.g. strcpy_s() and strcat_s()).

Some common examples of functions writing memory of specified length are presented in detail here. Due to the high number of functions in this category, most of them are simply listed without any detailed explanation at the end of the section.

memcpy

memcpy() copies one memory region to another. The number of bytes to be copies is passed and needs to be less than or equal to the size of the destination buffer.

stdlib/memcpy.c
// gcc memcpy.c
#include <string.h>
 
int main()
{
    char *src = "Hello World\n";
    char dst[5];
 
    memcpy(dst, src, 1 + strlen(src));
    return 0;
}

fgets

fgets() is a safer alternative to the gets() or scanf() function. It allows the developer to set the size of the buffer where the read data is stored.

stdlib/fgets.c
// gcc fgets.c
#include <stdio.h>
 
int main()
{
    char buffer[10];
    fgets(buffer, 10, stdin);
    return 0;
}

strncpy / strlcpy / wcsncpy / wcslcpy

When thinking about a safer alternative for strcpy(), the most obvious solution is probably strncpy().

stdlib/strncpy.c
// gcc strncpy.c
#include <string.h>
 
int main()
{
    char dst_buffer[10];
    char const *src_buffer = "This string is too long for the dst_buffer";
    strncpy(dst_buffer, src_buffer, 10);
    return 0;
}

It is important to note that also strncpy() can cause unintended effects. If the source buffer is larger than the size passed to strncpy(), there is no '\0' termination added to the destination string.

One alternative taking care of '\0' termination is strlcpy(). However, this function is not part of the C standard and only available on some Unix systems (e.g. BSD).

stdlib/strlcpy.c
// gcc strlcpy.c
#include <string.h>
 
int main()
{
    char dst_buffer[10];
    char const *src_buffer = "This string is too long for the dst_buffer";
    strlcpy(dst_buffer, src_buffer, 10);
    return 0;
}

With wcsncpy() and wcslcpy(), the same vulnerability applies to wide strings.

strncat / strlcat / wcsncat / wcslcat

Similar to strncpy(), the obvious alternative to strcat() is strncat().

stdlib/strncat.c
// gcc strncat.c
#include <string.h>
 
int main()
{
    char dst_buffer[10] = "This ";
    char const *src_buffer = "string is too long for dst_buffer";
    strncat(dst_buffer, src_buffer, 4);
    return 0;
}

Again, also strcat() can cause problems if the existing string is not terminated by a '\0' character. Analogous to strlcpy(), there is a safer alternative called strlcat() taking the size of the destination buffer as a parameter on some Unix systems.

stdlib/strlcat.c
// gcc strlcat.c
#include <string.h>
 
int main()
{
    char dst_buffer[10] = "This ";
    char const *src_buffer = "string is too long for dst_buffer";
    strlcat(dst_buffer, src_buffer, 10);
    return 0;
}

With wcsncat() and wcslcat(), the same vulnerability applies to wide strings.

snprintf / vsnprintf

snprintf() and vsnprintf() are safer alternatives to the sprintf() and vsprintf() functions allowing to specify the size of the destination buffer.

stdlib/snprintf.c
// gcc snprintf.c
#include <stdio.h>
 
int main()
{
    char buffer[10];
    snprintf(buffer, 10, "Hallo %s!\n", "World");
    return 0;
}

Others

Following functions of the C standard library were also identified to write memory of specified lengths.

  • asctime_s
  • c16rtomb
  • c32rtomb
  • ctime_s
  • fgetws
  • fread
  • mbrtoc16
  • mbrtoc32
  • mbrtowc
  • mbsrtowcs
  • mbstowcs
  • memset
  • strftime
  • wcsftime
  • wcsrtombs
  • wcstombs
  • wcsxfrm
  • wmemcpy
  • wmemmove
  • wmemset

Additionally, all functions with added size specification (commonly marked with a _s suffix) are part of this category.

Note that other common functions like recv() are not part of the official C standard but are part of this category as well.



← Back to heap overflows Overview Continue with ASCII-armored addresses →