Episode II: The snprintf#@% Strikes Back

Firmware Developers Need To Know

Quick recap. This is the second post in a long blog series diving into implementing cybersecurity for connected devices. Most firmware developers know the risk of using sprintf()-like functions. However, there is a perception that snprintf() and its cousins are safe. Sadly, this is not always the case.

Takeaways

  • Embedded software bugs tend to have a long-life span. Bugs creep in, but they don’t creep out.
  • Your snprintf() may not be safe.
  • An allegedly safe snprintf() can lead to arbitrary file-read-access bugs.
  • Safer source code available.

Refresher for snprintf()

As we all painfully know, working with string buffers can lead to headaches. Let’s begin with a quick snprintf() refresher. snprintf() is known as a safe function for working with buffers – if a buffer would overflow, snprintf() will truncate the results to prevent an overflow:

				
					static void
testing_snprintf()
{
    char test[4];
    snprintf(test, 4, "%d", 65536);
    printf("test = %s, len = %d\n", test, strlen(test));
}
test = 655, len = 3
				
			

A couple more examples to fully grok the API:

				
					    char test[4];
    int result;
    result = snprintf(test, 4, "%d", 65536);
    printf("test = %s, len = %d, result = %d\n", test, strlen(test), result);
    test = 655, len = 3, result = 5
				
			
				
					    char test[4];
    int result;
    snprintf(test, 4, "%d", -65536);
    printf("test = %s, len = %d, result = %d\n", test, strlen(test), result);
test = -65, len = 3, result = 6
				
			

Make sure your snprintf is not actually calling sprintf – what?

Variety is the spice of life; unfortunately, the coding pantry often contains putrid food items long past their spoil date — shockingly, not all implementations of snprintf() will respect the buffer length constraint. Early libc4 snprintf() implementations ignore the length parameter and you’re actually using an unsafe sprintf().

You can verify the fitness of your snprintf():

				
					#define TEST_SNPRINTF_LONG_VALUE        “65536”
#define TEST_SNPRINTF_BUF_LEN           (4)
#define TEST_SNPRINTF_EXPECTED_STRLEN   (TEST_SNPRINTF_BUF_LEN-1)
static void
test_snprintf()
{
    char testBuf[16] = { 0 };
    snprintf(testBuf, TEST_SNPRINTF_BUF_LEN, "%s", TEST_SNPRINTF_LONG_VALUE);
    if (TEST_SNPRINTF_EXPECTED_STRLEN < strlen(testBuf))
    {
        printf("test_snprintf: snprintf() is actually an unsafe sprintf()!\n");
    }
}
				
			

Understanding your snprintf variant

Slightly milder implementations exist of snprintf(); some implementations  return -1 when a buffer is truncated. To cover all potential implementations, let’s add a sanity check to ensure snprintf() returns the expected result. Few more tests to narrow your specific snprintf() implementation:

				
					static void
test_snprintf()
{
    char testBuf[16] = { 0 };
    int  result;
    result = snprintf(testBuf, TEST_SNPRINTF_BUF_LEN, "%s", TEST_SNPRINTF_LONG_VALUE);
    if (TEST_SNPRINTF_EXPECTED_STRLEN < strlen(testBuf))
        printf("test_snprintf: snprintf() is actually an unsafe sprintf()!\n");
    else if (TEST_SNPRINTF_EXPECTED_STRLEN != strlen(testBuf))
        printf("test_snprintf: snprintf() output string is incorrect!\n");
    if (-1 == result)
        printf("test_sprintf: snprintf() return value not ISO C99 compliant!\n");
    if (strlen(TEST_SNPRINTF_LONG_VALUE) != result)
        printf("test_sprintf: required buffer size result is incorrect!\n");
}
				
			

Truncation is a Threat Indicator

When you’re unexpectedly bump on a tourist attraction, you should be wary of a pickpocket. Similarly, unexpected data truncation is a warning sign. String truncation is not a good fallback measure for production quality code, ever. Truncation implies a developer underestimated the required buffer size for valid input.

CVE 2018-13379 – Path Traversal

This snprintf() snip and similar CWE-22 (Improper Limitation of a Pathname to a Restricted Directory [‘Path Traversal’]) defects have landed products on CISA’s dreaded top exploit list.

				
					#define MAX_FILENAME_LENGTH     (64)
HTTP_getLangPack(const char *pUsrLoginLangPack)
{
    char filenameBuf[MAX_FILENAME_LENGTH];
    snprintf(filenameBuf, MAX_FILENAME_LENGTH, "/migadmin/lang/%s.json", pUsrLoginLangPack);
    /* return file... */
}
				
			

We expect this code to return a JSON file located in “/migadmin/lang/” directory. Looks safe, right? Right? Perhaps, we have many language packs in the form of “en.json”, “fr.json”. What if an attacker rather than providing a two-character language tag, sends an awkward-looking string, “///../..///////////////////////private/passwordsen”. Our filename has been mangled to redirect our fopen() to the “passwords” file in the directory “/private/” – due to string truncation excluding the narrowing “json” filename extension – en.json is stripped from the filename.

Source Code for a safer snprintf()

A few different things can help with this type of problem. I subscribe to checking input before using data; however, I am a pragmatist, many people have lives, so there is an importance to having a backup safety net for today’s development teams juggling impossible deadlines and cumbersome code bases. Strictly relying on all code to validate user input is a hindsight, “2020” solution (not sorry).

My cut at a safer snprintf() – I prefer APIs that do not overload parameters and return codes.

Share

Table of Contents

See Our Blog Posts

Enter Your Information to Access This White Paper

Enter Your Information to Access This White Paper

Enter Your Information to Access This White Paper

Enter Your Information to Access This White Paper

Enter Your Information to Access This Datasheet

Enter Your Information to Access This Datasheet