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 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 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 some code; 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. |