Hello everyone, and welcome back to Mastering Software Exploitation.
In this section we are going to dive deep into stack overflows.
We will craft a simple exploitation and we will explore the most common security mitigations, uh,
existing against memory vulnerabilities.
So let's jump right in.
What is the stack memory?
As always, we start with the definitions.
So the stack is, you may have already know, is a special region in memory used for temporary storage
during function calls.
It operates in less than less first out manner.
And the structure of the stack frame, while varies between different architectures, is pretty much
around this layout that you see on the screen where we have the local variables saved in the stack,
which are the variables you actually define in your function.
If you have a buffer or a variable defined in your function, it goes into the stack frame.
Then we have the return address, which is to indicate to the function when, uh, or actually where
it should return when it's finished.
So I call function A and function A when it ends.
Need to know who called her where.
Where should it go back to?
And this is the return address that helps us continue the execution after we jumped into a function.
So let's dive into a specific example about a specific architecture.
Um, so in ARM, what you can see is that we have first some definitions.
PC is the program counter.
It's actually the process register that indicates, uh, the process.
What is the next address to execute code from?
LR is the link register or like in x86 the return address.
This is the register that actually saves the address where function should return to the function.
Returns via restoring PC from LR.
Right.
If I finish the function now, I take what's within LR, put it into PC and then the execution continues
from uh, the LR saved address.
And this is what we really need to know, uh, for this section.
Now let's talk about the oldest vulnerability out there, Stack Overflow.
We even have a website called that name.
Uh, so you can imagine how common and known that vulnerability is.
The reasons for stack overflow is improper boundary checks.
So we have a buffer.
We copy, uh, data into this buffer.
And we are not verifying that this data is not bigger than the buffer size.
The buffer is allocated on the stack, which is why it's called stack overflow.
If the buffer was dynamically allocated, then it was a heap overflow because the buffer was allocated
on the heap.
But now we are talking about stack overflows and pay attention that those sample functions are functions
that if being used, we have to check the size before calling them, because those functions are not,
uh, verifying the input size on their own.
So naive example.
I'm sure you've seen those in the past.
We have a function that defines a buffer size 64.
And then we use str copy copying user input into the buffer without checking the size.
In our main function we receive user input and we call this vulnerable function.
So basically if the user input is bigger than 64 we are going to call vulnerable function with a buffer
or string bigger than 64.
And we're just going to copy it into our 64 sized buffer which will cause us to overflow.
So, uh, if we take this into the frame, uh, of the stack, what we will have is the buffer 64 size
buffer, and after the buffer we will have LR, the link register or the return address.
So if we overflow the buffer after 64 bytes, what would actually happen is that we will start overwriting
the return address.
Since PC is actually taking its um restoring its execution from the LR register, we actually can control
PC.
Controlling PC means that we control where the system is going to execute code from, and that means
we control the program.
So overwriting the return address causes us to take full control over the system execution.
Now let's actually exploit this same vulnerability.
So we have some definitions here.
Buffer size is 64.
Buffer address.
We can extract it and know the address which is a const.
For the purpose of this example is this address.
The return address size is four bytes.
So we will now create our shellcode.
Shellcode is first some NOP sled.
Basically just to make sure that we can continue and fall on our code.
So we have some safe space which is NOP slide.
And then our malicious code coming afterwards.
Then we start constructing the payload.
First we need to fill out the 64 bytes of the buffer.
So we take buffer size and we fill it with ones.
Now we need to overwrite the return address with address that is actually pointing into our shellcode.
So walk with me to the image on the right side.
And let's take a look.
The beginning of the buffer is the buffer address.
After 64 bytes we have the return address.
And after these four bytes this is where the shellcode is going to be placed on.
So the address after the return address is where the shellcode goes.
So what we really want to do is have the return address overwritten with the address here, which is
the shellcode.
Right.
Because then PC will get this address.
This address will point to the shellcode and PC will start executing our shellcode.
So now going back to the payload you can see that the shellcode address is being calculated as the buffer
address plus the buffer size plus the return address size.
Which brings us exactly after the return address, and then we added some offsets into the NOP slide
to make sure that we fall into our NOP slide and then shell code.
So again, the shell code address is the buffer address plus the buffer size 64 plus the return address
size which is another four bytes.
And then we added some safety.
So we follow here where it's written shell code goes here.
So now the shell code address parameter contains the real shell code address.
And now we just need to pack all together.
So we take the payload which we filled up with 64 ones to fill up the buffer.
And we add the shell code address right afterwards.
That means that the LR that comes right after the buffer is going to be overridden with the shell code
address.
Now we actually take the payload and add the shell code, which is the malicious code.
So now we have the payload crafted from once to fill the buffer's shellcode address, to override return
address with the shellcode address and then the actual shellcode.
So when we actually use this payload, what will happen is that we're going to override the buffer override
uh LR with the address right after LR.
And then place our shellcode right after LR.
When we finish this call, the stack will look like LR pointing to an address that contains our shellcode.
So now when the function restores PC will execute code from LR which is the address where our shellcode
went to.
And this is how we basically finalize the exploitation and executing code directly from the stack code
that we placed there that performs our own desired operations.
So this is a very simple exploitation.
And there is a very good security solution to prevent this type of exploitation.
Please note that by preventing exploitation, we are not preventing the presence of the vulnerability.
Stack overflow vulnerability continue to exist out there.
But what we are attempting to do in security mitigations is block the attempt to exploit the vulnerability.
We can't control in a very good way.
Existence of vulnerabilities.
It's hard for us to eliminate all types of vulnerabilities in our code.
What's easier to do is to prevent the attacker from taking this exploitation steps.
So Stack Canaries is a security mitigation to prevent stack overflows from being exploited.
And the way it works is very simple.
When we enter a function, the first stage is to add a cookie and we are placing a cookie at each function
entry point.
And this code that places the cookie is being inserted at compile time.
What that means is that every function frame will now look like this.
We will have a buffer after the buffer and right before the return address we always have the canary.
It will always be the last thing before the return address.
Now, before the function actually returns and exits, it validates that the canary value remained the
same value that was placed there at the beginning of the function.
This is really important.
So when we enter the function, we put some magic value.
Let's say it's one, two, three, four.
Now, when we come to exit the function before we actually return, we verify that the canary value
is still 123, four.
If it's not, then we are not allowing the function to return.
We are assuming that the return address is corrupted, because if our canary is corrupted, the return
address might be corrupted as well.
So we are not returning and we are preventing the exploitation.
Only if the canary value is intact and is identical to the value that was placed there at the beginning
of the function, is where we allow the continuous of execution and restore PC from the return address.
Pay attention now that if we have a vulnerability and a vulnerable buffer as an attacker, I need to
overflow the buffer and I want to get into the return address.
There is no way for for me as an attacker to actually overwrite the return address without overwrite
the cannery value first, because the cannery is placed between the local variables and the return address.
Excuse me?
Between the vulnerable buffer and the return address, I cannot overflow the buffer.
Keep the cannery intact and then overflow the return address.
So our previous exploitation will not work because we will corrupt the cannery value.
We will also corrupt the return address.
But since we will co-op the cannery value, the validation will fail and the function will not allow
us to return from our corrupted return address, which is how the exploitation will be prevented.
There are some bypass techniques to stack canaries.
The first one is information leak vulnerability.
Remember the Heartbleed vulnerability we talked about earlier, which gave the attacker a way to leak
data from the memory?
So imagine me as an attacker has an information leak that could first tell me what's in the canary.
What's the value if I know it's one, two, three, four?
Then I can corrupt the buffer, overflow the buffer, make sure I put one, two, three, four here
in the canary so it remains the same, and only then continue to overwrite the return address.
That way the validation of the cookie will be successful and the function will return for my coopted
return address.
So by chaining two vulnerabilities information leak plus stack overflow, I'm able to restore the canary
value and bypass or pass the validation to continue, uh, the execution for my malicious address.
Another example is brute force.
Let's say that the system is not high in entropy, and the canary value is really limited to a small
amount of, uh, of uh, uh, values.
Then I can just continue and try to overflow with different values that I place in the canary until
I hit the right value and successfully pass the validation, which is kind of the same as the info leak
example.
Only this time I'm guessing again and again.
What's the canary value until I'm successful?
A few limitations to stack canaries.
First, it can only be applied on source code.
So if you have a third party binary that is not compiled with stack canaries, then this binary is still
very vulnerable to stack overflows.
Efficiency depends on the entropy of the system, which we discussed when we talked about the brute
force.
Uh, bypass and local function variables are not protected because everything that goes here before
the canary is placed can still be corrupted.
So if there is a local variable that is important or influence the system, behavior I can still control
is value.
And that can also cause unintended behavior.
While it's not straightforward like the return address, there are still exploitations that can be successful
only by corrupting the local variables.
Another very cool security mitigation is Aslr address space layout randomization, which basically means
randomizing the memory addresses each time the process loads, or the system boots that actually creates
unpredictable memory layout.
Why does that matter?
As you noted before, attackers rely on known vulnerability addresses to exploit vulnerabilities.
Aslr makes it very difficult.
So in the example that we actually, uh, went through earlier without aslr, the vulnerable buffer
always was always at a certain address.
So it was easy to craft a reliable exploit payload as we did.
We just need to jump to a known address.
But with Aslr, the buffer location changes randomly with each execution.
So in the first run it could be at this address, the second round it could be this address and so on
and so forth.
So when I'm writing my exploit sorry.
When I'm writing my exploit.
This buffer address that I put here and used it is actually unknown to me.
And this works.
What makes Aslr a strong security mitigation against exploit crafting?
So the advantages are significantly raising the bar for attackers by eliminating address predictability.
It forces attackers to find memory disclosure vulnerabilities first, like we talked about information
leaks that will indicate to us what is the buffer size at this moment in real time so that we can use
it in our exploitation.
It breaks hardcoded exploit addresses, as we've seen, and it provides protection without really any
performance impact.
I'm not using any code or any CPU cycles, I'm just loading the addresses differently every time, so
no code additions.
The limitations are as follows.
First entropy.
If I don't have a lot of entropy, then addresses could become pretty much predictable.
Information leaks completely bypass this protection.
So many times we will see two vulnerabilities that are being used.
The first one is info leak and the second one is a buffer overflow.
So if you ever thought to yourself, well info leak, it's not that important.
Well it is because it brings attackers away to, uh, exploit other vulnerabilities that you have in
the system.
Resource constraints limit implementation.
So for example, in IoT devices, uh, you will see less and less use of aslr, although this becomes
now, uh, better because they have more resources.
And most importantly, I think or, uh, worth noting is that even if you have a DSLR and it's working
properly, the system will still crash.
If I start, if I if I exploit it.
So while the attacker is not able to gain remote code execution, he is still able to crash the system,
which is a certain of denial of service and not the desired behavior.
Anyhow, the reason is that if I overflow a buffer and able to overflow other elements in the memory,
even though I cannot, uh, predict the buffer address and correctly jump to my malicious code, I am
still corrupting important pieces in the software.
If I corrupt important pieces in the software, many times I will cause unexpected behavior.
So while Aslr prevented the code execution, it didn't prevent the overflow itself, which means the
system might crash.
Another mitigation is next bit, not execute bit.
This means restricting the execution to only designated code segments, so it will be harder for attackers
to execute arbitrary code.
Even if vulnerabilities exist in the code.
So the most straightforward implementation of it is to make the stack and heap memory non-executable.
If you remember from the example before we placed our shellcode right on the stack, we put it after
the return address and then we jump to it.
But if we have the NX bit, that means that the CPU cannot execute code coming from stack memory, which
means that if you place your shellcode on the stack, it will not be executed.
Even if you did everything correctly and corrupted the return address pointing to your shellcode, it
will be blocked at the CPU level.
So this is a very cool technique that is actually very effective because before the uh, NX bit or DEP
in other systems, it was very easy for attackers to place their shellcode in the heap memory or the
stack, just use the memory, throw the shellcode in there and point the PC there in various ways.
Now it's actually harder.
Where can we execute code from?
If I can't place my code in the memory because the memory is not executable?
We will talk more about it when we, uh, hit return oriented programming.
Outliers.
Important to know that there are some systems that need to enable code running from the heap.
In that case, protections can be configured to allow execution of code only from certain segments to
allow this system to execute code from the heap, but still prevent, uh, easy code execution from
other heap segments.
So we have a lot of memory protections allowing to configure different regions in the memory.
So if you ever wondered about why configuring what's allowed to be executed, what's not.
So this is part of the reason some real world data.
So, uh, both ARM and x86 environments are widely using Aslr more than 95% actually for modern desktop
and mobile operating system stack canaries.
Nearly universal Implementation across systems.
IoT is a bit lagging and embedded systems are a bit lagging in general, but those security mitigations
are very strong and are widely adopted.
So coming to exploit advanced systems, sorry, the simple exploitation we learned about will probably
not work for us.
And this is why in the next chapters, we are going to learn about advanced techniques like return oriented
programming.
That gives attackers a way to respond to those security mitigations.
And I hope you will enjoy your first hands on lab writing, your first exploitation, and writing your
own code on affordable program.
Thank you and see you in the next chapter.