Computer Organization

Stack Smashing Attacks

Andreas Moshovos

October 2019

 

 

 

Buffer Overrun Stack Attacks

There are many different kinds of computer viruses. In this lecture we will be discussing  the mechanics of “stack smashing” viruses and in particular we will be reviewing those that exploit buffer overruns. These viruses are preventable to a great degree as they rely on “sloppy” code. While in principle, sloppy code should not existing, computing systems are complex and it is safe to assume that sloppy code may exist. We will first describe this in the context of our NIOS II system and then comment on how it applies on other systems. The latter requires knowledge we haven’t yet covered such as how operating systems structure their memory usage, concepts such as virtual memory, supervisor vs. user mode, and system calls.

 

Let’s assume that our computer is connected to the internet via some I/O device. When the device receives a packet it “calls” via interrupts its interrupt handler whose purpose is to copy the packet of data received into some location in memory. Let us assume that after this packet of data has been received and copied to memory, our main program needs to process this packet. For example, our main program might be a web browser and the packet contains the contents of a server response to a user request. For example, this could be the response to an http request made by the used on our NIOS II system. At some point, there will be some function of the following form:

 

void
packet_parse_string(char *packet, ….)

which accepts as an argument a pointer in memory where the packet of data has been copied. The packet of data in this case is to be interpreted as a string of characters, something like “I am a very nice string”. We have not have discussed how strings are stored in memory. In C and several other programming languages there are stored as zero terminated arrays. That is in memory, the characters are stored one after the other, each occupying a byte, and after the last character a zero byte follows. The zero byte is a marker the indicates the end of the string. For example the string “ABBA” will be stored in memory as follows:

 

              .data
              .byte ‘A”,’B’,’B’,’A’,0

 

Note that it takes 5 bytes to store it. The first four are the characters (in NIOS II assembly and in C single quotes specific that what follows is a single character and that the assembler or compiler should convert that into an 8b number according to the ASCII encoding of characters).

 

Back to our function. When the function eventually gets called its first argument will be a point to memory where the packet  which in this case is meant to be a string, is to be parsed. Note that as part of normal operation, we assume that this function will be called when the response is supposed to be a string according to the communication protocol. A valid server is supposed to send us a properly formatter, zero terminated string. Our function is written taking this for granted and this is the first sign of “sloppiness”.

 

Let us take a closer look inside our function. It contains a local variable called buff which is meant to be used as a temporary buffer while parsing the string. The mechanics of how the parsing is done are immaterial. All we need to know at this point is that internally the function copies characters from the packet into its own buff and does so assuming that packet contains a zero terminated string:

 

void
packet_parse_string(char *packet, ….)
{
   char buff[256]

   some code;
   some loop that copies characters from packet into buff.e.g.,
   while (packet[i] != 0 ) // there are still characters in the packet
      buff[j++] = packet[i++] // copy them to buff

  some other code

}

 

The flaw in the above code is that it “blindly” copies characters from packet into buff without checking whether we have exceeded the capacity of buff. Note that buff was allocated to hold up to 256 characters. Believe or not such code was and probably is still common enough for this to be a problem. The programmer here wrote the code that the server that send the packets *Will* absolutely and always follow proper protocol and never send a string of more than 256 characters. Well what I the server is a malicious one? What if it does send a longer string? The code above has no check that we exceeded the limits of buff, and thus will naively continue copying values into memory following the end of buff. What can happen then? Well let’s look at where buff is stored at and what follows it?

 

Buff being a local variable, will be stored on the stack. So the stack will look something like this

 

SP à   other local vars or space to pass arguments for call made by packet_parse_string

             Buff[0]
             …

             Buff[255]

             Other local vars

             Callee saved regs (saved by the prologue of packet_parse_string)

             Saved ra (provided that our function calls other functions)

  

   Note that at some offset after the end of buff is the value of RA that packet_parse_string saved (provided that it makes other calls it has to do that). Also note that in its epilogue our function will restore RA from the stack, adjust the stack popping all values, and the execute RET. Ret will do PC = RA. So, provided that we could somehow alter the value of RA as it currently is saved on the stack we can have the function start executing code at a location of our choosing immediately after executing the RET instruction. For example, if somehow we knew that the saved RA is at location 0xFF20 0200 and that buff was at location 0xFF20 0100, if we could change the value at memory location 0xFF20 0200 to 0XFF20 0100 the processor will start reading the contents of buff and start executing those as instructions. Recall that any memory location can hold data or instructions. What makes a memory value an instruction is when a processor accesses it. If it accesses it as part of its fetch request then it will interpret it as instruction. Otherwise as data. A memory value can be interpreted as an instruction (if fetched) and as data (if accessed by a load or store).

 

So here’s the trick: the malicious server has to send a packet which is formatted in a way so that it is much larger than the local buff of our function. It has to be carefully crafted so that it is copied over the stack, it overwrites the stored RA value and makes it point back to some location within buff. This could be for example the beginning of buff as in the discussion above. Rather than containing a legitimate string, the packet instead contains bytes which if interpreted as instructions are code that the malicious serves wants to executed. For example, this could code that sniffs information from your computer such as code that installs a keylogger. This requires careful crafting but it is possible and as many examples demonstrated actually pretty practical. As a result, a remote user has been able to inject code to our system and to have it execute locally. The code can be anything.

 

Practical Considerations

 

For the above attack to work the attacker has to guess where the top o the stack will be when the function gets called, at what offset buff will be and at what offset the saved RA will be. It may seem improbable for this to be possible However,  it is not as hard as it may seem. First, most systems tend to use copies of the same software. For example, there are only a few dominant operating systems out there such as windows, OS X, and linux variants. When device drivers run, they typically run within the OS and the location and stack addresses because of the use of virtual memory tend to be the same across all systems running the same OS. So, a malicious user can experiment with a local system first to determine how the stack will look like and then try it to a remote system. Even if the exact locations are not know, the attacker can always try to probe a system by guessing the addresses. Computing systems are very fast, and an attracker can try several possibilities in little time.

Defenses

There are ways to defend against such attacks. The first is to not write sloppy code :) But that is not a guarantee. Some code bases have been around for decades and bugs such as the one above lurk within them. Sometimes, the “bugs” are not as obvious at all and they are not easy to spot even with code inspection (e.g., nested into several function calls). Another option is to forbid execution of code that lies within the stack. However, at least in the past there have been legitimate uses for writing and executing code on the stack. For example for generating optimized code for java applications. Several processors include hardware features that can check for such execution attempts and deny them.

 

This is just an example of a computer virus. There are many others The example demonstrates that understanding the mechanics of instruction execution and the conventions used by programming languages are valuable if not essential tools for defending against virus attacks. Recently, there have been viruses that have been able to “steal” information by exploiting performance enhancing techniques such as branch prediction and caching. To appreciate those you may want to also attend ECE552 the 4th year computer architecture course.