Mitigations and Secure Coding

A number of mitigations have been added in compilers and operating systems to make buffer overflow exploitation harder

  • Non eXecutable stack (NX)
    Support: hardware (NX bit)/operating system
  • Address space layout randomization (ASLR)
    Support: operating system
  • Stack protector (canary)
    Support: operating system

Non eXecutable stack (NX)

NX prevents execution of injected code on the stack in order to prevent shellcode injection. However, programs might disable it if they need to execute code on the stack. We can use scanelf to check if NX is enabled:

$ gcc shellcodetest.c -o shellcodetest
$ scanelf -e prog shellcodetest
 TYPE STK/REL/PTL FILE
ET_EXEC RW- R-- RW- shellcodetest
$ gcc shellcodetest.c -o shellcodetest -z execstack
$ scanelf -e prog shellcodetest
 TYPE STK/REL/PTL FILE
ET_EXEC RWX R-- RW- shellcodetest

Even with NX enabled, an attacker can:

  • Return to program code
  • Return to library code

In general, NX does not prevent returning to code in segments that are (necessarily) executable!

Example: return to system

The return address is overwritten with the address of function system, followed by a (fake) return address for the function and a pointer to the parameter of system, e.g., “/bin/sh”. When the function returns it will jump to system that will execute “/bin/sh”:

                          stack
         buffer->   |       ...      |
                    |       ...      |
        old EBP->   |    overwrite   |
    return ADDR->   |     &system    |
                    |   ret addr     | (for system)
                    |   first prm    | --> “/bin/sh”

NOTE: system “thinks” it has been called and looks for its parameter on the stack (“/bin/sh”)

Address space layout randomization (ASLR)

ASLR randomizes the address space of

  • stack
  • library functions

This requires brute-forcing to get useful addresses! However ASLR does NOT prevent jumping to program code (for example ROP, described later). Moreover, if addresses are leaked (e.g. recent timing side-channels attacks) it becomes void.

Return Oriented Programming (ROP) “composes” a shellcode by putting together small pieces of code (gadgets) that ends with ret:

When the function returns it jumps to the first gadget that executes some code and then returns, jumping to the second gadget, and so on. In principle, it is possible to concatenate an arbitrary number of gadgets so to obtain any functionality. Complex programs will in fact exhibit a variety of exploitable gadgets. Moreover, if it is possible to recover (or brute force) the library addresses then all of the gadgets “offered” by libraries would become available.

Stack protector (canary)

Stack protector rearranges variables, as we have seen in previous class. It also adds a control value just before function return address, as follows:

  1. When function call happens, after pushing return address and ebp on the stack, the program pushes a special value called ‘canary’. This value is different for each running process, and is picked at random by the kernel when a process starts. With this protection enabled, the stack appears as follows:
    pwd_buffer
    ...
    canary
    saved base pointer (ebp of the calling function)
    return address
  2. When a function completes, before returning to the caller, the canary value is checked:
    • If the value is not the expected one program aborts (with the message you have observed), detecting a stack corruption. Since the canary is in between variables and return address, if we overwrite the latter through an overflow we necessarily overwrite the canary. In our specific case we have:
      pwd_buffer
      ... (overwritten)
      canary (overwritten)
      saved base pointer (overwritten)
      return address (overwritten)
    • If the value matches, the program jumps to the return addess

NOTE 1. In Linux the canary value starts with a null 0x00 byte. This prevents reading or writing it using overflows operating on string (as they would stop at the 0x00 byte). Thus, only the remaining bytes are picked at random.

NOTE 2. The origin of name ‘canary’ comes from the (sad) use of canaries to check for toxic gas in mines. A dead canary indicated a dangerous situation and the mine was evacuated.

Secure Coding

We have seen various vulnerabilities that are triggered by bugs in C programs such as overflowing an array or printing a string directly, without the “%s” format string.

It is thus important to write programs that are not vulnerable, but languages such as C are subtle and it is hard to write secure programs without following coding rules and recommendations.

The SEI CERT C Coding Standard

The SEI CERT C Coding Standard of the Carnegie Mellon University is an important resource that provides rules and recommendation from the security coding community.

  • Rules are meant to provide normative requirements for code;
  • Recommendations are meant to provide guidance to improve the safety, reliability, and security of software systems. A violation of a recommendation does not necessarily indicate the presence of a defect in the code.

Risk Assessment

Each guideline in the CERT C Coding Standard contains a risk assessment section that attempts to provide software developers with an indication of the potential consequences of not addressing a particular rule or recommendation in their code (along with some indication of expected remediation costs).

Each rule and recommendation has an assigned priority. Three values are assigned for each rule on a scale of 1 to 3 for severity, likelihood, and remediation cost.

Severity

How serious are the consequences of the rule being ignored?

Value

Meaning

Examples of Vulnerability

1 Low Denial-of-service attack, abnormal termination
2 Medium Data integrity violation, unintentional information disclosure
3 High Run arbitrary code

Likelihood

How likely is it that a flaw introduced by violating the rule can lead to an exploitable vulnerability?

Value

Meaning

1 Unlikely
2 Probable
3 Likely

Remediation Cost

How expensive is it to comply with the rule?

Value

Meaning

Detection

Correction

1 High Manual Manual
2 Medium Automatic Manual
3 Low Automatic Automatic

Priorities and Levels

The three values are then multiplied together for each rule. This product provides a measure that can be used in prioritizing the application of the rules. The products range from 1 to 27, although only the following 10 distinct values are possible: 1, 2, 3, 4, 6, 8, 9, 12, 18, and 27. Rules and recommendations with a priority in the range of 1 to 4 are Level 3 rules, 6 to 9 are Level 2 , and 12 to 27 are Level 1 .

The following picture (from here) provides possible interpretations of the priorities and levels:

Examples

We show a few examples of rules and recommendations.

Rule 06. Arrays (ARR): “Do not form or use out-of-bounds pointers or array subscripts”. This rules states that you should never let array indexes go out of boundaries: it is crucial that indexes are always checked. For example:

int const TABLESIZE = 100;
#include 

static int table[TABLESIZE];
 
int *f(int index) {
	if (index < TABLESIZE) {
		return table + index;
  	}
	return NULL;
}

Is not compliant as a negative index would correspond to a memory address before the array. It is necessary to fix the code by also checking that the index is non-negative:

int const TABLESIZE = 100;
#include 

static int table[TABLESIZE];
 
int *f(int index) {
	if (index >=0 && index < TABLESIZE) {
		return table + index;
  	}
	return NULL;
}

This particular rule is evaluated as follows:

Severity

Likelihood

Remediation Cost

Priority

Level

High Likely High P9 L2

Rule 07. Characters and Strings (STR): "Guarantee that storage for strings has sufficient space for character data and the null terminator". The following example is a classic off-by-one problem:

#include 
 
void copy(size_t n, char src[n], char dest[n]) {
   size_t i;
  
   for (i = 0; src[i] && (i < n); ++i) {
     dest[i] = src[i];
   }
   dest[i] = '\0';
}

It might happen that the terminating '\0' is stored out of the array boundaries. Code should be fixed as follows:

#include 
 
void copy(size_t n, char src[n], char dest[n]) {
   size_t i;
  
   for (i = 0; src[i] && (i < n - 1); ++i) {
     dest[i] = src[i];
   }
   dest[i] = '\0';
}

This particular rule is evaluated as follows:

Severity

Likelihood

Remediation Cost

Priority

Level

High Likely Medium P18 L1

Rule 07. Characters and Strings (STR): "Do not pass a non-null-terminated character sequence to a library function that expects a string". The following example contradicts this rule and another one:

#include 
 
void func(void) {
  char c_str[3] = "abc";
  printf("%s\n", c_str);
}

String initialization does not have space for the terminating '\0'. It does not overflow but the string is ill-terminated. Code should be fixed as follows:

#include 
 
void func(void) {
  char c_str[] = "abc";
  printf("%s\n", c_str);
}

This particular rule is evaluated as follows:

Severity

Likelihood

Remediation Cost

Priority

Level

High Probable Medium P12 L1

Rec. 07. Characters and Strings (STR): "Use the bounds-checking interfaces for string manipulation". There exist bound-checking functions for string manipulations such as BSD strlcpy and strlcat. Notice that strncpy and strncat might leave the string unterminated and are considered error prone.

The following code does not follow the recommendation:

#include 
#include 
#include 

int const BUFSIZE=21;

void die(char *s) {
	printf("ERROR: %s\n",s);
	exit(1);
}

void complain(const char *msg) {
  static const char prefix[] = "Error: ";
  static const char suffix[] = "\n";
  char buf[BUFSIZE];

  strcpy(buf, prefix);
  strcat(buf, msg);
  strcat(buf, suffix);

  fputs(buf, stderr);
}

Clearly there could be a buffer overflow if msg plus prefix and suffix are bigger than buf. The code can be fixed by adopting the strlcpy and strlcat functions. They return the length of the string that was supposed to be built but truncate the string so that it fits the buffer, including the terminating '\0'.

#include 
#include 
#include 

int const BUFSIZE=21;

void die(char *s) {
	printf("ERROR: %s\n",s);
	exit(1);
}

void complain(const char *msg) {
  static const char prefix[] = "Error: ";
  static const char suffix[] = "\n";
  char buf[BUFSIZE];

  if ( strlcpy(buf, prefix, sizeof(buf)) >= sizeof(buf) ) 
  	die("strlcpy failed");
  if ( strlcat(buf, msg, sizeof(buf)) >= sizeof(buf) ) 
  	die("strlcat1 failed");
  if ( strlcat(buf, suffix, sizeof(buf))  >= sizeof(buf) ) 
  	die("strlcat2 failed");

  fputs(buf, stderr);
}

This particular recommendation is evaluated as follows:

Severity

Likelihood

Remediation Cost

Priority

Level

High Probable Medium P12 L1

Rule 09. Input Output (FIO): "Exclude user input from format strings". User input in a format string can trigger a format string vulnerability.

The following code does not follow the rule:

#include 
#include 
#include 
 
void incorrect_password(const char *user) {
  int ret;
  /* User names are restricted to 256 or fewer characters */
  static const char msg_format[] = "%s cannot be authenticated.\n";
  size_t len = strlen(user) + sizeof(msg_format);
  char *msg = (char *)malloc(len);
  if (msg == NULL) {
    /* Handle error */
  }
  ret = snprintf(msg, len, msg_format, user);
  if (ret < 0) { 
    /* Handle error */ 
  } else if (ret >= len) { 
    /* Handle truncated output */ 
  }
  fprintf(stderr, msg);
  free(msg);
}

The code constructs string msg that is finally printed with fprintf as a format string. Since msg depends on user input this might trigger a format string vulnerability. A compliant version follows:

#include 
 
void incorrect_password(const char *user) {
  static const char msg_format[] = "%s cannot be authenticated.\n";
  fprintf(stderr, msg_format, user);
}

This particular rule is evaluated as follows:

Severity

Likelihood

Remediation Cost

Priority

Level

High Likely Medium P18 L1

Rule 10. Environment (ENV): "Do not call system()". Use of the system() function can result in exploitable vulnerabilities, for example:

  • When passing an unsanitized or improperly sanitized command string originating from a tainted source
  • If a command is specified without a path name and the command processor path name resolution mechanism is accessible to an attacker
  • If a relative path to an executable is specified and control over the current working directory is accessible to an attacker
  • If the specified executable program can be spoofed by an attacker

Example:

#include 
 
void func(void) {
  system("rm ~/.config");
}

An attacker could manipulate the value of the HOME environment variable such that this program can remove any file named .config anywhere on the system. A compliant version follows:

#include 
#include 
#include 
#include 
#include 
void func(void) {
  const char *file_format = "%s/.config";
  size_t len;
  char *pathname;
  struct passwd *pwd;
 
  /* Get /etc/passwd entry for current user */
  pwd = getpwuid(getuid());
  if (pwd == NULL) {
    /* Handle error */
  }
 
  /* Build full path name home dir from pw entry */
 
  len = strlen(pwd->pw_dir) + strlen(file_format) + 1;
  pathname = (char *)malloc(len);
  if (NULL == pathname) {
    /* Handle error */
  }
  int r = snprintf(pathname, len, file_format, pwd->pw_dir);
  if (r < 0 || r >= len) {
    /* Handle error */
  }
  if (unlink(pathname) != 0) {
    /* Handle error */
  }
 
  free(pathname);
}

This particular rule is evaluated as follows:

Severity

Likelihood

Remediation Cost

Priority

Level

High Probably Medium P12 L1

Exercise

Analyse the compliance to rules and recommendations of the following program and rewrite it to make it compliant:

/*
 * Secure coding exercise for the Security course at Ca' Foscari.
 *
 * This is a particularly non-compliant piece of code with a number of
 * severe vulnerabilities. Find problems and fix them.
 */
#include 
#include 
#include 
#include 
#include 

#define BUFSIZE 32

char buffer[BUFSIZE];
char buffer2[BUFSIZE/2];
char buffer3[BUFSIZE];

int main() {
	int i;

	strcpy(buffer2,"cat ");
	strcpy(buffer3,"date");

	openlog("mylog", LOG_PERROR | LOG_PID, LOG_USER);

	printf("Welcome to pigsty, a program to display file content!\nFile name: ");

	for(i=0; (buffer[i]=getc(stdin)) != '\n'; i++);

	syslog(LOG_DEBUG,buffer); // debug

	strcat(buffer2,buffer);
	system(buffer2);

	printf("\npigsty correctly terminated on: ");
	fflush(stdout);
	system(buffer3);
}