3. Memory Safety Vulnerabilities¶
This note describes some memory safety vulnerabilities, namely buffer overflow vulnerabilities, stack smashing, string/integer vulnerabilities, off-by-one vulnerabiliites.
Buffer Overflow Vulnerabilities¶
Buffer overflows are particularly a risk in C, and since C is a widely used systems programming lanugage, we must know how to protect against them. C++ and Objective-C also suffer from these vulnerabilities.
If a programmer declares an array char buffer[4], C will not automatically throw an error if the programmer tries to access buffer[5]. The programmer's responsibility is to check that every memory access is in bounds.
Let's start with the example
char buf[8];
void vulnerable() {
gets(buf)
}
First, know the syntax:
gets()reads as many bytes of input as the user supplies (through standard input), and stores them intobuf[]. Even if the input contains more than 8 bytes of data,gets()will write past the end ofbuf, overwriting some other part of memory.
A danger that can appear:
char buf[8];
int authenticated = 0;
void vulnerable() {
gets(buf);
}
note where the variables are stored in our memory model. They are located in static. Static grows upwards, so authenticated is right above buf. Let's therefore write 9 bytes of data to buf such that the authenticated flag is set to true. Now an attacker can gain access.
Modify the code to look like this instead
char buf[8];
int (*fnptr)();
void vulnerable() {
gets(buf);
}
Now that the static data is fnptr, note that this is a 4 byte value that stores the address of a function that weill dereference the pointer and start executing those instructions at the address. Therefore, let's gets(buf) with 12 bytes, where the ending 4 bytes point to an address of our choosing, redirecting program execution to some other memory location. Perhaps where malicious machine instructions are stored? This is a malicious code injection attack.
-
Malicious code injection attacks allow an attacker to seize control of the program. At the conclusion of the attack, the program is still running, but now it is executing code chosen by the attacker, rather than the original code.
-
For example, if a web server is running as root, once the attacker seizes control, the attacker can do anything that root can do; for example, leaving a backdoor that allows them to log in as root later. Now the system has been "owned" (also called pwned, 0wned, ownzored).
Stack Smashing¶
We can also exploit stack variables
void vulnerable() {
char buf[8];
gets(buf);
}
Two things to note: above buf in the stack lies the return address and other things, like rip and sfp. We could possible overwrite them with some carefully chosen inputs.
Let's assume malicious code exists at 0xDEADBEEF. Then we just input AAAAAAAAAAAA\xef\xbe\xad\xde. And Boom! we have now just make the rip overwritten with 0xDEADBEEF, and the program will go there once the function returns. The program now will start running code at that address. Note that since x86 is little-endian, we had to input it as above.

Now if the malicious code didn't already exist, what if we write some shellcode that allows us to perform arbitrary actions. If the shellcode was 8 bytes long. We might input something like
[shellcode] + [4 bytes of garbage] + [address of buf].
Now notice that rip is sent to the shellcode. That is bad!
Now suppose the shellcode is 100 bytes long. Obviously the shellcode won't fit, but let's do this
[12 bytes of garbage] + [address of rip + 4] + [shellcode]
Now the shellcode is above the rip in memory, so then we start running code at the shellcode. since the rip is sent just above it.

Stack smashing dates back to the late 1980s, where the Morris Worm exploited a buffer overflow vulnerability to infect thousands of computers.
There are tutorials on the web explaining how to deal with complications such as:
-
Malicious code stored at an unknown location
-
Buffer is stored on the heap instead of the stack
-
Characters that can be written to the buffer are limited (like lowercase letters only)
-
There is no way to introduce any malicious code into the program's address space.
Bottom line: If your program has a buffer overflow bug, you should assume that the bug is exploitable and an attacker can take control of your program.
Format String Vulnerabilities¶
Let's look at this example
void vulnerable() {
char buf[8];
if (fgets(buf, sizeof buf, stdin) == NULL) {
return;
}
printf(buf);
}
The stack diagram might look like

Note the following:
-
When
printf()executes, it looks for a format string modifier denoted by a%in its first argument located 4 bytes above the RIP ofprintf. If it does find a modifier, it looks 8 bytes bove the RIP for the actual argument. -
We might have a print statement like
printf("x has the value %d, y has the value %d, z has the value %d \n", x, y);. Note that the format string requires 3 arguments since we have three %d modifiers, but we only have 2 arguments. However, C will not catch this error -
So, here is what will happen with excess modifiers: it will look 4 bytes up for each modifier. Even if we don't pass in a third modifier value, it will still look 4 bytes upwards of the previous one. It will print out the value that is there.

Here are other modifiers that might be useful:
-
%sTreat argument as an address and print the string at that address up until the first null byte -
%nTreat argument as an address and write the number of characters that have been printed so far to that address -
%cTreat the argument as a value and print it out as a character -
%xLook at the stack and read the first variable after the format string -
%[b]uPrint out[b]bytes starting from the argument
Bottom line: if you program has a format string vulnerability, assume the attacker can learn any value stored in memory and can take control of your program.
Integer Conversion Vulnerabilities¶
Another example
char buf[8];
void vulnerable() {
int len = read_int_from_network();
char *p = read_string_from_network();
if (len > 8) {
error("length too large: bad dog, no cookie for you!");
return;
}
memcpy(buf, p, len);
}
Note two definitions for memcpy() and size_t:
void *memcpy(void *dest, const void *src, size_t n);
typedef unsigned int size_t;
Therefore, if an attacker provides a negative value for len, the if statement won't notice anything wrong, and memcpy() will be executed with a negative third argument. Casting this to unsigned integer makes it be very large positive integer. Therefore, memcpy() will copy a huge amount of memory into buf, overflowing the buffer.
Another example that tries to skirt around this problem:
void vulnerable() {
size_t len;
char *buf;
len = read_int_from_network();
buf = malloc(len+5);
read(fd, buf, len);
...
}
It allocates 5 more bytes than necessary, so it seems to avoid overflow problems. However, len+5 can wrap around if len is too large. Let len = 0xFFFFFFFF, then the value of len + 5 is 4. Therefore, the code allocates a 4-byte buffer and then writes a lot more than 4 bytes into it. Classic buffer overflow.
Off-By-One Vulnerabilities¶
Consider a buffer whose bounds checks are off by one. This means we can write n+1 bytes into a buffer of size n, overflowing the byte immediately after the buffer.
We can overwrite a single byte and start executing instructions at an arbitrary address in memory.
The idea here overwrite the extra byte we are allowed, which overwrites the LSB of the sfp. Make sfp point to somewhere inside buff.
Then the function will return. The following will happen
1. mov %ebp, %esp: esp now points where ebp is pointing, which is the forged sfp
2. pop %ebp: Take the next value on the stack, the forged sfp, and place it in the ebp register. ebp is pointing insider the buffer
3. pop %eip: Take the next value on the stack, the rip, and place it in the eip register. Since we didn't maliciously change the rip, the old iep is correctly restored.
Now ebp points insider the buffer, Let's put the shellcode in the buffer, so the execution will go there.
Note two things: 1. We want to overwrite the place that the program eventually tries to interpret as the rip. 2. It is not enough the place the shellcode 4 bytes above where the forged sfp is pointing. You need to put the address of shellcode there, since the program will interpret that part of memory as the rip.
Other Memory Safety Vulnerabilities¶
Other examples of memory safety violations include
-
Dangling pointer (pointer into a memory region that has been freed and is no longer valid)
-
Double-Free (where a dynamically allocated object is explicitly freed multiple times)
-
Use after free (where an object or structure in memory is deallocated but still used). These are particularly attractive targets for exploitation. Involves the attacker triggering the creation of two separate objects that actually share the same memory. The attacker can now use the second object to manipulate the interpretation of the first object.
-
C++ vtable pointers. An example of heap overflow, where programmer declares objects on the heap. This requires storing a vtable pointer, a pointer to an array of pointers. If bounds are not checked correctly, an attacker can overflow one of the instance variables of object
x. If there is another object abovexin memory, then the attacker can overwrite that object's vtable pointer.