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:
- 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 - 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
- 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:
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);
}