So, we'll talk about something very different today; very different to my normal image filtering videos -
- and that is "buffer overflow exploits" and what they are and how you do them.
Which is kinda fun
I'm obviously somewhat of a geek, I quite like these sort of things - low level memory exploits.
A buffer overflow exploit is a situation where -
- we're using some, probably low-level C function or something to write a string -
- or some other variable - into a piece of memory that is only a certain length.
But we're trying to write something in that's longer than that -
- and it then overwrites the later memory addresses, and that can cause all kinds of problems.
The first thing we should talk about, probably, is roughly what happens in memory with a program when it's run.
Now, we're talking about C programmes in Linux today, just because I happen to have a Linux VM running here and it's easier.
But this will apply to many different languages and many different operating systems.
So, when a programme is run by the operating system... so we're in some shell and we type in a command to run a programme
The operating system will effectively call, as a function, the main method of your code.
But your actual process, your executable, will be held in memory in a very specific way.
And it's consistent between different processes.
So, we have a big block of RAM. We don't know how big our RAM is because it can be varied, but -
- we use something called Virtual Memory Address Translation to say that -
- everything in here, this is 0. 0x000...
This is the bottom of the memory, as it were
And up here is 0xFFF. So, this is the equivalent of "11111111" memory address -
- all the way up to 32 or 64 bits, and this is 0.
Now, when you use this, there are certain areas of this memory that are always allocated to certain things.
So, up here we have kernel things. So, this will be command line parameters that we can pass to our programme -
- and environment variables and so on.
Down here we have something called the text. That's the actual code of our programme.
The machine instructions that we've compiled get loaded in there.
Now that's read-only, because we don't want to be messing about down there.
In here we have data. So, uninitialised and initialised variables get held here.
And then we have the heap. Now, the heap may have been mentioned from time to time.
It's where you allocate large things in your memory.
Big area of memory that you can allocate huge chunks on to do various things.
What you do with that is, of course, up to your programme.
And then up here, perhaps the most important bit, in some ways anyway, is the stack.
Now the stack holds the local variables for each of your functions -
- and when you call a new function like, let's say you say "printf" and then some parameters -
- that gets put on the end of the stack.
So, the heap grows in this direction as you add memory, and the stack grows in this direction.
Now that I've laid that out, we won't talk about it any more; we'll just focus on the stack, because that's where a lot of these buffer overflows happen.
You can have overflows in other areas, but we're not going to be dealing with them today.
I'm going to turn this sideways, because I think it's a little bit easier to understand. At least that's how I tend to look at it.
This is our memory again, nice and big. This is now our stack area (excuse my programmer's writing).
Up here we have the high memory addresses (0xff...) So, something up here is high...
... and this is 0x000. Now, of course, the stack won't be taking up this whole region but it doesn't matter.
So, there are high memory addresses and low memory addresses.
And the stack grows downwards, so when we add something onto the end of the stack it gets put on this side and moves in this direction.
Of course, I'm talking about a stack without telling you what a stack is.
Professor Brailsford has already talked about this and has probably done a much better job of explaining it than I would!
Brailsford: There's a lot of computer science that depends on stacks. I sometimes think stacks and trees are just about all computer science is about.
So we'll just say that you know how a stack works and then we'll move on!
We have some programme that's calling a function. A function is some area of code that does something and then returns back to where it was before.
So, this is our calling function here..
When the calling function wants to make use of something, it adds its parameters that it's passing onto the stack.
This will be parameter A and this will be parameter B, and they will be added into the stack in reverse order.
And then the Assembler code for this function will make something called a "call" -
- and that will jump to somewhere else in memory and work with these two things.
And it's the nature of this stack that causes us to have problems.
Let's look at some code and then we'll see how it works.
I've got myself here a program that isn't very good. I wrote it.
So, it's a piece of C code, so if we look at it...
It's a very simple C code that allocates some memory on the stack and then copies a string into it from the command line.
So up here we've got the main function for C that takes the number of parameters given and a pointer to those variables that you've got.
And they'll be held in kernel area of our memory.
We've allocated a buffer that's 500 characters long and then we call a function called "string copy" (strcopy) -
- which will copy our command line parameter from argv into our buffer.
Our function puts on a return address which is replacing the code we need to go back to once we've done strcopy.
So that's how main knows where to go after it's finished.
And then we put on a reference to the base pointer in our previous function. We won't worry about that too much because it's not relevant particularly to this video.
This is just going to be our EBP base pointer.
This is our allocated space for our buffer, and it's 500 long.
If we write into it something that's longer than 500, we're going to go straight past the buffer, over this, and crucially over our return variable.
And that's where we point back to something we shouldn't be doing.
What I'm going to do is walk through it in the code and then let's see if it works.
So this is my Kali Linux distribution, which has all kinds of slightly dubious password cracking tools and other penetration testing tools.
It's meant for ethical hacking, let's just make that clear!
I've written here a small function that does our copy from the command line.
Now I've compiled it and I can run it. So I can run my vulnerable code with "Hello".
And that will copy "Hello" into this buffer and then simply return, so nothing happens.
It's the most boring programme ever!
[Sean Riley offscreen] Another program might do something like copy "Hello" in there and now it's in the buffer they can go off and process it?
Yeah, I mean maybe you've got a function that makes things all uppercase.
So you copy "Hello" off and then you change this new copy to be all uppercase, and then you output it to the screen.
And this doesn't have to be "main()", this could be any function.
We're going to run something called GDB, which is the Linux command line debugger.
Erm...
I wouldn't advice using GDB unless you really like seeing lots of Assembly and doing really low-level Linux things.
[Sean Riley offscreen] There's a lot of text on the screen now so we don't have to worry about - [cut off]
No, this text here is just warranty information.
So now I'm going to type in "list" -
- and it shows use the code for our function. So we can see it's just a compiled function.
It knows this because the compiler included this information along with the executable.
We can also show the machine code for this so we can say "disas main" and we can see the code for "main()".
[Sean Riley offscreen] So they're the instructions that would actually go to the CPU?
These are the actual CPU instructions that will be run.
Now we won't dwell on much of this, because Assembly is perhaps a whole series of talks by someone other than me!
Steve Bagley knows a lot about Assembler.
However, a couple of really important things are:
This line here, sub of 0x1f4 from %esp, that's allocating the 500 for the buffer.
That is we're here and we go 500 in this direction and that's where our buffer goes.
So buffer's sitting to the left in this image but is lower in memory than the rest of our variables.
Now...
We can run this programme from GDB- and if it crashes, we can look at the registers and find out what's happened
So we can say "run Hello" and it will start the programme and say "Hello".
And it's exited normally.
Now, we can pass something in a little bit longer than "Hello".
If we pass something that's over 500, then this buffer will go over this base pointer and this return value, and break the code.
[Sean Riley offscreen] So that will just crash your-
It should just crash it. Python, for example, can produce strings based on simple scripts on the command line
So what we do is we say "run" and then we pass it a Python script of print... 41 (that's the "a" character"), let's say 506 times.
Just a little bit more than 500 so it's going to cause somewhat of a problem but not a catastrophe. [Chuckling] Okay?
And then we run that.
And it's received a segmentation fault. Now a segmentation fault is what a CPU will send back to you when you're trying to access something in memory you shouldn't be doing.
Now that's not actually happened because we overwrote somewhere we shouldn't; what's happened is the return address was half overwritten with these 41s.
[Sean Riley offscreen] So it doesn't know what it is?
Yeah, there is nothing in memory at 0xb7004141, and if there is, it doesn't belong to this process. It's not allowed, so it gets a segmentation fault.
So if we change this to 508, we're going two bytes further along, which means we're now overwriting the entirety of our return address.
We're overwriting this "ret" here with 41s. Now if there were some virus code at 414141, that's a big problem. So that's where we're going with this.
So we run this, and you can see the return address is now 0x414141.
I can actually show you the registers and you can see that the construction pointer is now trying to point to 0x414141.
So that means that it's read this return value and tried to return to that place in the code and run it, and of course it can't.
Now we can have a little bit more fun. We've broken our code, what can we do now?
Well, what we need to do is change this return value to somewhere where we've got some payload we're trying to give- we're trying to produce. Okay?
So luckily, if I quit this debugger, I have some pre-prepared payload just for this occassion!
Now in fact this payload is just a simple, very short programme in Assembler, that puts some variables on the stack and then executes a system call to tell it to run a shell -
- to run a new command line.
If I show this code, our shell code, this code will depend on the Linux operating system and whether you're using an Intel CPU or something else.
This is just a string of different commands. Crucially, this xcd / x80 is throwing a system interrupt, which means that it's going to run the system call.
That's all we're going to do about this. What this will actually do is run something called ZSH, which is an old shell that doesn't have a lot of protections involved.
Let's go back to our debugger.
And... we're going to run again but this time we're going to run a slightly more malicious piece of code.
We're going to put in our \x41s timesed by 508 - and then we're going to put in our shell code. There we go, okay?
So now we're doing all 41s and then a bunch of malicious code. Now that's actually too long; we've gone too far. But we'll fix that in a minute, okay?
And finally, the last thing we want to add in is our return address, which we'll customise in a moment.
To craft an exploit from this, what we need to do is remember the fact that strcopy is going to copy into our buffer.
So we're going to start here. We want to overwrite the memory of this return address with somewhere pointing to our malicious code.
Now, we can't necessarily know for sure where our malicious code might be stored elsewhere on the disc, so we don't worry about that or memory.
We want to put it in this buffer. So we're going to put some malicious code in here and then we're going to have a return address that points back into it.
Okay... now...
Memory moves around slightly. When you run these programmes, things change slightly, environment variables are added and removed, things move around.
So, we want to try and hedge our bets and get the rough area that this will go in.
In here, we put in something called a No-Op sled. Or, y'know, there's various other words for it.
So this is simply \x90. That is a machine instruction for "just move to the next one". So that's good.
Anywhere we land in that No-Op is going to tick along to our malicious code.
So we have a load of \x90s here... then we have our shell code, right?
That's our malicious payload that runs our shell.
And then we have the return address, right in the right place, that points back right smack in the middle of these \x90s.
And what that means is, even if these move a bit, it'll still work.
[Sean Riley offscreen] So it's like having a slope almost, is it?
It's exactly like that, yes. Anywhere where we land in here is going to cause a real problem for the computer-
[Sean Riley offscreen] So we've got our bomb, or our... I dunno... pit of lava [laughs]
Yep, it's a Sarlacc pit isn't it and your No-Op sled takes you in and you get digested over 10,000 years or whatever it is.
So we've got three things we need to do. We need to put in some \x90s, we need to put in our shell code, which I've already got, and we need to put in our return address.
Worry about the return address last, okay? So...
If we go back to my code: we change the first \x41s that we were putting in, and we change to 90. We're putting in a load of No-Op operations.
Then we've got our shell code and then we've got what will eventually be our return address.
And we'll put in 10 of those because it's just to have a little bit of padding between our shell code and our stack that's moving about.
This 508 here, people will have noticed, now this is too big, because we're putting in extra information.
So, if we write 508 bytes, it goes exactly where we want: over our return address.
But we've now got 43 bytes of shell code and we've got 40 bytes of return address.
So... -40... -43... is 425. We'll change this 508 to 425, and so now this exploit here that we're looking at is exactly what I hoped it would be here.
Some \x90 no operation sleds, the shell code and then we've got our return address, which is 10 times four bytes.
We run this and we've got a segmentation fault, which is exactly what we hoped we'd get because our return address hasn't been changed yet.
So now let's look at our memory and work out where our return address should go.
So in GDB, it's paused the programme after the segmentation fault, so we can say "list the registers", let's say about 200 of them, at the stack point of -550.
So that's going to be right at the beginning of our buffer. And what we're seeing here is a load of 90s in a row.
So we just need to pick a memory address right in the middle of them, so let's pick this one, let's say 0xbffffaba -
- and write that down so I don't forget it.
Now, there's a nice quirk in this, which is that Intel CPUs are little ednian, which means I have to put it in backwards.
It's yet more things we have to learn, but it's fine!
b-f... f-f... f-a... Oops, put my caps-lock on... Can't type with people watching! Errrrm... and er, b-a.
Theoretically when I run this, what will happen is string copy will do its thing: it'll copy a string in.
And then, when it tries to return, it will load this return value and execute that instruction, which will be somewhere in this buffer.
Then it will read off and run our shell code, so we should get a shell.
Okay?
[tappity tappity]
And we did!
So that's a good start, right? We know our program works. Albeit in a debugger with very little side effect.
The question now is: can I take this and use it on a command line to gain access to this machine?
Now...
Linux has quite restrictive policies on what can and can't be done from certain programmes, but some programmes, such as changing your password, are run using something called SUID.
So what that means is that, for the sake of running that programme, you have complete root access to that machine.
Because otherwise, how could you change the password file? You're not normally allowed to even read it.
The shadow file. So...
If you find a vulnerability in that kind of programme, and there's more than I think there should be, then that's when there's a real problem.
Now obviously these vulnerabilities are getting rarer, but it's catastrophic if you get one.
So, let's leave this debugger and back to our nice clear command line environment.
If I list the files we've got, this vulnerable programme here is shown in red, that shows that it's SUID root.
Which means that when we run it, it will be running as root, which is not great for security.
That and my shoddy programming, which means it's vulnerable to a buffer overflow.
So, if I copy my exploit... here we go, this is a big moment of truth, where this whole video is going to work.
I've put my code in just like it was in the debugger. I've tried to make it exactly the same so that the memory doesn't move around.
Let's just say "whoami" on Linux, so we can see I am myself. I don't have root access.
So can I, for example, look at the password file? So I can say "cat /etc/shadow".
"Permission denied". No dice, okay. Fair enough. I'm not supposed to be looking at that.
Now I'm on my exploit, so "./envexec", then my vulnerability, with the right address... now we've got a shell.
"whoami"?
Root.
So now can I look at my shadow file? [Sean Riley offscreen] So root is like God for this system?
In Linux, there is nothing you can't do with root.
So I've got my root shell and I'm root, so I can "cat /etc/shadow", and I can see what's in the shadow file.
But the point is that there's nothing I can't do now, I can wipe the machine, or do anything like that myself.
And then I can quit this and then my programme just gracefully exits. Because it now returns to normal code.
And, hopefully, no-one is any the wiser that anything's gone on.
Now there are things that the operating system does to try and stop this from happening: randomising your memory layout and no executing of stacks and stuff.
There are ways around this; they're obviously for a different video.
But... at least things are getting definitely better.