You must have encountered the word ‘buffer’ somewhere in your life. In everyday usage, buffer refers to something that acts as an intermediary between two things that don’t get along. It’s a shield which moderates the impact of one thing over the other. In the programming world, buffer refers to an area where we temporarily store data before moving it to another place. Perhaps the most famous example would be copy-paste. When you copy something, it is stored in the buffer until you paste it somewhere. This concept is used extensively while building software systems and it’s important to make sure that the buffer behaves nicely at all times. Now you might ask, what does buffer behavior refer to? Why would it not behave nicely at all times?
What’s the problem here?
The idea of using up more space than you were given, and therefore spilling over into a different field is called overflow. When it happens to a buffer, it’s called buffer overflow. Simple enough to visualize, right? But it probably isn’t clear how this can lead to chaos if an evil programmer is allowed to run his own code. This is pretty simple to explain if you understand it well enough.
When you are writing code, there is a place called the “stack” where you can store temporary information. It’s just like a real life stack where you put one thing on top of another. Just like a stack of pancakes! The “stack pointer” determines where the end of the stack is. Why do we need to know where the end of the stack is? Well, when you push something on to the stack, the last thing has to come out first. So we need to know where to look at any given time. When a function runs, it moves the stack pointer to give itself memory to work with. When it’s done, it moves the pointer back to where it found it.
An important thing to note is that the stack grows backwards. So if you want to give yourself 80 bytes on the stack, you subtract 80 from the stack pointer rather than adding it. For example, if the previous function’s stack started at 620 and I want 80 bytes, then my stack starts at 540. This means that if you use more space than you gave yourself, you won’t just continue writing out into empty space. Instead, you’ll actually start overwriting previous stack values. When the function starts running, the very top value left on the stack by the previous function is the return address. This is where it should go when the function is done. This means that if the function overruns its stack, the very first thing that it’s going to overwrite is the return address. If the attacker is careful about what he fills the stack with, he can specify whatever return address he wants. When the function exists, whatever code is at that return address is what will get executed next. Do you see the problem here?
Let’s say there’s a function that reads your name and then returns. So your stack looks like something like this:
Stack Pointer Prev. Stack Ptr +----------------------------------+--------------+................ | Your Name Here | Return Addr | Old stack ... +----------------------------------+--------------+................
But the evil programmer makes his name long enough to overflow the space. And not only that, instead of typing a real name, he types in some evil code, some padding, and the address of that evil code.
+----------------------------------+--------------+................ | [ Evil Code ]xxxxxxxxxxxxxxxxxxxxxxEvil Address | Old stack ... +----------------------------------+--------------+................
Instead of returning back to the previous caller, you jump straight to the evil code section. Now you’re running his code instead of your program. From there on out, things are pretty much under his control. This is called stack smashing.
How do we prevent this?
Two of the techniques used to reduce the effectiveness of stack smashing are Data Execution Prevention (DEP) and Address Space Layout Randomization (ASLR). DEP works by marking the stack non-executable. So effectively, what we are doing here is to block any kind of program execution. This means that the [Evil Code] on the stack won’t run, because running code on the stack is no longer allowed. Is there a way for the attacker to get around this? Well, the attacker may find chunks of existing code that will do bits and pieces of what he wants. Now, instead of just overwriting his own return address, he creates a chain of return addresses down through the stack for all the functions he wants to run in turn. This is called Return Oriented Programming (ROP). The chain of returns is called a ROP Chain. This is really hard to do, but it’s possible.
We saw that the strategy used by DEP is good, but it still has a loophole. How do take care of this? This is where ASLR comes into picture. It works by randomizing the locations of all the “interesting” functions. This way, creating an ROP chain will not be that easy. Every time the program runs, all the addresses are in different places. So when the attacker goes to overwrite the return address with is own evil address, he won’t know what numbers to use because the code is always in different places. As you may have noticed, these methods do not offer much protection individually. But both together make it very difficult for the attacker to do any harm. While some circumventions still exist, there isn’t a workaround that works everywhere.