[MUSIC]
Now in this video, we're going to talk about buffer overflows.
And I'm going to give you several examples to show you exactly how they work and
exactly what can be done.
So if you go to the next slide, it talks about the process memory structure,
and this is the way most systems work.
They have the instructions in one place in the memory.
They have the data in another place in the memory.
And the stack and the heap share a place in memory.
The stack grows towards the heap and the heap towards the stack.
This allows you to balance the way space is used.
For example, if your stack grows quite a bit and
you're not using much of the heap, then there won't be a problem.
Whereas is if you had fixed allocation for the stack and the heap,
if the stack grew too big, it might cause a problem, might run into the boundary.
And even if the heat were very,
very small, your program would still stop running.
And here's the typical stack structure,
the important point here is that local variables get put on the stack.
And as you call each function or entry,
a new frame is placed on the stack and the stack grows up.
At the beginning of each frame is a return address and a processor status word.
And the processor status word, or processor status long word contains
information about condition flags and things like that.
And the return address is where the program will return to,
the address of the instruction it will next execute,
once it's done with this frame, once it returns from the function.
So the next slide talks about the idea underlined buffer overflows.
Basically, you figure out what buffers are stored on the stack and
then write a small machine language program to do what you want.
Typically, this is to spawn a shell or something like that.
Then what you're going to do is take that and put it into memory.
And for this example, it'll be on the stack.
You then pad it enough so that when you upload this input,
you'll upload the small machine language program.
And then afterwards, you'll overwrite the return address
with the address in where you began this small machine language program.
So on return, when the return address is popped and put into the PC,
instead of going to the place where this function was called from,
you're going to go and execute the machine code.
And since the machine code isn't trusted because the user uploaded it,
there's a possible compromise.
So let's look at this In Pictures.
Now this picture uses a function called gets to read input
from the standard input.
You should never under any circumstances use gets because it does no bounds
checking, and that's why it's here.
fgets is one that does do bounds checking, so that's the one you should use.
But in this particular function,
you have an input buffer in a routine.
And then the routine calls gets to fill that input buffer.
So if you look on the side that says the usual stack, you have the input buffer.
Here it's in within the function name.
The main part of the program.
You have the local variables, and one of those local variables is the input buffer.
And then when you call gets, the parameters for
gets get pushed onto the stack.
And in this case, the parameter to gets is where it's to store its int,
the string it reads, which is, of course, the input buffer.
And that's what the little arrow represents.
Once you've got all the parameters on the stack,
you then put the return address, and then the processor long word.
The other returns state information.
Then you branch to gets, which is going to allocate a byte, or rather, an int for
what it reads.
So that goes where the gets local variables go.
Now, what I do is I upload my machine language code,
the program to invoke the shell.
And I pad it enough so that when I reach, it overwrites the parameter to gets.
And then it overwrites the return address of main with the address of
the input buffer.
So now when the function returns, when gets returns,
it's going to pop that return address off, and instead of going back into main,
it will go and execute the program to invoke the shell.
That's what shown on the side after the attack.
And the message, of course,
is what is read from gets that carries out the overflow.
And this describes it in words.
The hard part here is figuring out how much padding to put into
the input that you upload that will cause the buffer overflow.
The usual way around this is simply to use the return address as the padding.
Then once the machine language program is written into memory,
into that buffer, all of the words beyond that will have the return address,
or rather, will have the address of the buffer.
So one of those is going to overwrite the return address, so now you've changed
the return address to branch back into the stack and execute your little program.
So as I said, the required is you have to change the return address.
There are ways to pad it, which is described here.
I mentioned the buffer address as padding.
The main issue with alignment there is if the machine is word aligned,
you're going to be uploading the suggested characters,
so you have to make sure it's word aligned.
If you know the number of bytes per word, this is very easy.
Just count the number of bytes in the machine language program.
Round up the next multiple of 8 or 4 or 16 or whatever.
And then start your return the address there.
The next slide shows a little trick.
One of the problems is that when you use gets or any function that reads
something from the input, it'll either stop at end of file or newline.
Okay, and if there's NUL byte in there, many functions will stop at there as well.
So you have to make sure that if there is a NUL byte or
a byte to transition to a newline in the program you're uploading,
that somehow that's encoded so the function that's
doing the reading will not ignore it or will stop there.
And there are various ways to do this.
You can, in your machine language program, for example,
have something that decodes the encoding that you use to
protect the NULs and the newlines, and that works fine.
If you want to know whether or not something is vulnerable to a buffer
overflow in your own program, there's real quick and dirty way to test this.
When your program asks for input, give it say, 10 or
15,000 characters, like in this case I use A, because the hex is 0x41.
If the program crashes, fire up a debugger and
go to the address that's stored in the program counter.
If it's a sequence of those characters,
you've gotta buffer overflow in which the return address can be altered.
This is not a guarantee that you'll be able to exploit it, but
it does show that there is a problem that you need to deal with.
So now how do you get the address of where to put the buffer?
Well, what you can do is run GDB or some other process debugger?
And that will get you the address.
The one issue here is a common defense is called ASLR,
Address Space Location Randomization.
And that places the data segment, in fact,
all the segments, at random addresses.
And so the address to this buffer may not be the same
the second time through as it is the first time through.
So you have to take that into account.
That if an server's turned on, this type of attack becomes much harder to exploit.
You can also put it somewhere else like the environment list.
Just put it into the environment list and then figure out where that address is.
And then go ahead and jump to that address.
Now, another way this type of attack is prevented,
is simply by making the stack not executable.
So when you return to execute the machine language program on the stack,
the program crashes because you're trying to execute something you can't.
So the way around that is not to upload your own code.
Instead, what you do is look for
a library function that will carry out the task you want.
The system library function, or
the p open library function, are the ones that people usually favor.
Because their first argument is a command to run a Linux or
Unix command in a subshell.
And so they can run that with whatever privileges the program has.
That's called the return-to-libc attack.
Because when you do the return, instead doing return to the stack,
you return to a location in the libc library that's loaded in memory.
So now jump into that routine.
Turning off execute permission on the stack doesn't help there because
the library routine is not stored on the stack.
So at this point the system will execute, but it'll execute and do what I want.
Because I just set the arguments properly on the stack.
Now, there's an even nastier attack that people have figured out.
The question, here, is, okay, I'm doing a return into a library function,
can I chain these things together so instead of executing one function,
I execute a small piece of code that doesn't return?
But when it returns, the return address that's stored on the stack is the address
of another small segment of code.
And I keep doing this until the function or
the process that I want is executed, until I get what I want.
Now it turns out that some the work that has been done on this has shown that,
at least for the C library, the set of small segments is showing complete.
So essentially anything you can write in another programming language,
you can write using these.
These small blocks of code, by the way, are called gadgets, and
they have certain properties.
One of the properties is they always end in a return.
So when you finish executing one gadget,
you do a return, pops the stack and branches to that next location.
But what's stored on the stack is the location of the next gadget.
So you simply chain these all together.
There are other forms of this.
For example, there's something called jump-oriented programming,
which is just like return-oriented programming except it uses jumps.
And a few others like them.
But the ideas are all basically the same.
Okay, that's control flow control.
That's where we've executed code
under our control based on changing return addresses.
But I mentioned earlier that you can do things in the data segment as well.
So here you're changing the flow of control indirectly.
Instead of changing the return addresses, you're changing the value of variables.
And presumably, those variables control what is executed or what is read.
Now, to do this,
you can't change a return address because it's in a different segment, doesn't work.
But what you can do as I say is, change the value of critical variables.
And then when that happens, the program will execute,
but with your values, not the ones that are intended.
So here's an example of a data overflow problem.
This one doesn't exist anymore.
Now with the Unix system, when you enter you password, it's not stored.
It doesn't compare it against what is stored.
It performs a mathematical hash of that password and
then compares that to the hash that's stored.
And there are all sorts of tricks that aren't relevant here.
The algorithm is you get the name, log in then goes into the password file,
which is the authentication database, finds the hash password
corresponding to that logging name and loads it into an array.
The user is then prompted for the password, and they go ahead and
enter the password.
The password is then hashed.
And the results are compared to the contents of the hash array.
But the problem is the arrays are contiguous.
And if you look at the next slide, you'll see what's going on here.
There's a buffer for a cleartext password 80 characters long.
And then there's a buffer for the hash, which is 13 characters long.
So what I do is I enter my cleartext password, let's say it's abcdefg.
And then I type 72 blanks to hand out the buffer for
the cleartext password.
Meanwhile, I have hashed abcdefg, and I know what the hash is.
And then after those 72 blanks, you simply type the hash.
And then when I'm done, the system will take the first eight characters of
the buffer for cleartext password,
because on this particular system the limit of password length was eight.
It would compute the hash and then compare it to the stored hash,
but I already did the stored hash, and set it so
it matches what was just computed, and I get to log in.
The problem, of course, is the arrays are contiguous, and
there's no boundary between them that will catch a problem.
This requires knowing where the data structures are.
And on many programs,
you can use various functions like nm if there's a simple table present.
If not, what you can do is load things into memory and
then use various dynamic buggers to figure out where in memory those things are.
If you got the source, that's wonderful.
The other thing you can do is disassemble the code and
see where in the assembly language the buffers are.
The third thing you have to know is what's a good value and what's a bad value?
And good and bad here are relative.
To the attacker, well, what what's good that will get me in?
To the defender, well, what's good in that it will block this type of attack or
allow me to know that it's happening?
The next slide gives yet another example of this.
syslogd is a privilege program that takes in messages from various hosts and
programs and writes them into a set of files.
This way, logging is centralized.
Now, on one system, syslogd, when it read from the network,
would simply read from a network socket.
It wouldn't use gets, so there was no overflow at the read, it used fgets,
if I remember correctly, or possibly something else,
but there was no buffer overflow possible there.
The message would then be formatted.
And since a variety of processes are using syslogd, you get the PID and
the date and other information to identify which process is creating this.
And then you write it out.
Now the problem was, before it ordered out,
it constructed the line it was going to write out in memory,
using a function called sprintf, and that would write into
an array called line2, which had 2048 characters.
So what happens if the message I get from the socket is 24,
sorry 2050 instead of 2048?
That would overflow the array that sprintf is writing into.
And as result, I can change variables close to line2.
So what's going on here?
Well, in all of these cases that we've seen,
the string is being put into the array, or
characters are being put into the array without checking for overflow.
The first one I showed you was a bug in a program called Finger.
If the buff where the input from gets was being read is not
overflowed, then the return address remains the same and
it'll bounce back to where it's supposed to.
But if that does overflow, and change the return address,
it will branch to whatever the return address is.
The login bug that I just showed you, again, the problem here is if password is
not overflowed, then the hash function loaded from the authentication database is
unchanged and you can do the comparison legitimately.
But if it is overflowed, then I'm going to correct that hash value.
I can turn it into whatever I like.
Exactly the same with syslogd.