Hello everyone.
Welcome back to Software Exploitation, section three turn oriented program.
In the last session we learned about security mitigation like Aslr.
And if you remember, the last piece was an NXP which prevented us as attackers to execute code from
stack memory, heap memory, or generally speaking, from Unallowed locations.
Now we are going to talk about an attacker's technique to bypass these mitigations, and you probably
heard about it as ROP return oriented programming or return to libc.
So a bit of introduction.
What is OP return oriented programming is an exploitation technique that bypasses security Protections
like NX bit.
But what it actually means, in fact, is that instead of injecting malicious code, attackers actually
reuse code that is already existing in the device to chain this code together in what we call gadgets,
to build a complex exploit payload that is actually performing their intended behavior.
So that was a very long explanation of something that is pretty much simple.
Instead of placing new code into the system, we are actually using already executable code regions
only in a different order to make this code work for us and perform unintended behavior in real life.
We also have types of OP.
So let's imagine a politician giving an interview that is being recorded and she is saying something
like, I've never supported policies that harm the environment.
And you can read the sentence out of the slides.
And now the editor is actually taking that recording and edit it to actually make it sound like she
said, I support policies that harm the environment.
What you've seen here is actually pretty simple.
It's about taking selectively pieces of words that she actually said only in a different order, or
eliminating certain pieces of it to construct a new meaning.
This is exactly how it works also in software, where instead of words we have code segments.
So going step by step, what we do in OP is starting with existing code.
We add no new code.
Just like in our example, we didn't add any new words to what the politicians said we find reusable
fragments, meaning identifying executable code snippets or gadgets that already present in the program
and can support what we are trying to achieve.
Then we chain them together to create this new meaning or malicious functionality from permitted code,
only executed in a different way.
Then this code or change gadgets is usually being referred to as the shellcode.
The shellcode is just the initial malicious code running on the system that then can legitimately load
and run the full malware.
So what I'm trying to say here is that usually we are not going to execute a full blown malware just
from op gadgets.
That will be pretty much a nightmare.
But what ROP can allow us to do is to execute a very small pieces of software named shellcode to actually
open a legitimate reverse shell, or doing something that will then allow us to legitimately use existing
system functionalities to install the malware.
What is it technically?
So you need to remember that usually the stack pointer is becoming the new PC.
And by addressing the stack pointer as such, we can understand how the chaining of judges actually
works.
Don't worry, we are going to take this step by step with a real example on an ARM architecture.
So in this example, let's assume that we as an attackers are trying to execute system bin sh.
This is what we really want to do.
But we cannot introduce new code into the memory and execute from memory.
So what are we going to do?
First, let's assume we are getting all of the objects we need and all the relevant addresses using
some gadget finder tools like copper.
You will actually use such tools in your hands on exercise during this course.
Then, assuming we have all we need, we have all these addresses and all these gadgets, we overflow
the buffer with the rope chain that we have built.
Let's assume this is the rope chain you see here.
The buffer is filled with B's.
And then this is the option we created.
This is an address within Lipsey.
Another address within Lipsey another address and then string finish.
Now let's assume that in the first address Lipsey plus 18798 we have this binary code pop out three
PC.
So this is what actually is within the address.
Then in leap CE2 c4 we have this chain of commands mov, r0, stack, pointer and branch to our tree
register.
So as you can see those are addresses within the code segment.
And these are the code that actually present in those addresses.
These addresses are allowed to be executed even if you have an NX bit because they are on the code segment.
So let's assume the attacker built this swap chain and put all of these, uh, in the stack memory.
And now let's see what actually happens.
So on the right side you can have uh, the list of registers.
And we will follow that list as we go, uh, step by step into the execution.
So the first thing is PC starts after we successfully overflowed the return address.
Uh, PC is now placed, is now receiving this address.
Let's see this.
You can imagine this as the return address, and we, uh, overwritten the return address with Lipsy
plus 18798.
So now PC starts executing code from this address.
As I previously mentioned, this address contains this code cop r3, pc.
For those of you who doesn't know, pop is actually a command, an assembly command that pops value
from the stack and put it on the registers in the command.
So pop r3 pc will actually take the value that now stack pointer is pointing to, and put it on R3,
and then take the next value and put it on PC.
So let's go through that pop.
R3 will take this address and put it inside our tree.
So you can see here within our array of registers that our tree received this address which points to
system.
Then we perform the second pop which is extracting the next in line address from the stack into PC.
So now PC received the libc address.
Now it means that if you remember.
And of course I assume you remember PC is the program counter.
So now the CPU start executing code from the address that we put in PC.
And this address is this assembly commands.
So PC starts executing from here.
So we jumped from here by controlling PC via popping an address from the this stack and we move to this
command.
So now we are going to perform move our zero stack pointer.
So R0 is going to receive the address that is now present in stack pointer.
Stack pointer as you can see is now pointing to finish.
So R0 received the string bean s.h.
Now PC is progressing to the next command which is branching to register our tree.
Our tree.
We took care of making sure he has the address of system.
We took care of it in our previous command.
So now we are actually making PC jump into the address within system.
So what happened here?
We are now executing code from the function system.
Functions are getting the parameters for R0.
We also took care of the fact that R0 is pointing into being a Qt string.
So what's really being executed here is system been Qt.
And that's it.
We won.
We wanted to execute this piece of code.
We didn't introduce new code to the system.
We just controlled the PC through the stack pointer and through overflowing the return address with
the first address that indicates this command.
And usually, this is what you will look for when you start chaining your gadgets.
You will look for pop commands that you can start controlling PC through popping elements from the stack.
Pretty amazing right?
Uh, sounds complicated, but we have automated tools today to help us build those things out of the
existing, uh, binary of the system that we want to exploit.
So you push the binary, uh, the code segments of the system that you want to exploit and, uh, tools
help you to find relevant gadgets.
Okay.
So now going back to thinking as, uh.
As a defender, could you think of a way to prevent this?
So defender's response to OP is control flow integrity.
What is CFE?
CFI can actually help prevent open job attacks.
Job is jump oriented programming by restricting indirect jumps and calls.
It's not really restricting.
It's more like inspecting that every jump is going to be into allowed locations.
Allowed locations is for example, not within functions and uh, jumps or branches that actually follow
the execution paths that we recognized on the software.
So again, sounds complicated, but let's break it piece by piece.
The first step in a high level of CFI is to instrument the code and create a control flow graph.
A control flow graph at a high level is a graph that shows all the execution paths within the program.
So for example, if function A is calling function b and c and that's it, then the control flow graph
will show that from function A you can only get to function b and function c.
Then there is path validation during execution.
The instrumented code that was inserted uh during the first stage is validating that every uh control
transfer is being verified against the permitted, uh, legitimate paths.
That way, we can actually prevent attacks when there is an unauthorized branch like we did before we
branched, or start executed code just from a middle of a function.
Actually, just some pop up line of code that we jump to, which is probably part of a function.
Middle of a function.
It's going to be blocked.
By blocking this, we can actually prevent, um, rope chains from actually causing unintended behaviors.
Now let's stop for a second and remember section one.
If you remember, exploitation is defined by causing unintended behavior with CFI.
Even if I have a vulnerability, and even if I place a rope chain, that actually works because, uh,
CFI is limiting us to certain Allowed execution paths.
Then my rope chain.
If it violates the legitimate execution paths, I'm going to be blocked.
But if I build a rope chain to actually not violate violate.
Sorry, the execution path, that is legitimate.
On one hand, my rope chain works and running, but on the other hand, the behavior is pretty much
expected because it's legitimate.
And this is really the key element to understand here.
DeFi makes sure that all the branches and jumps and return addresses within the code returns to legitimate
expected code flows.
So how it prevents rope attacks, exactly as we discussed before.
And let's talk about an example.
Without CFE, buffer overflows can redirect execution to objects with CFE.
The system detects that invalid return addresses and terminates, stopping the attack.
Now, step by step of how we calculate what is legitimate.
So the first stage of a CFI is static analysis before execution.
This is where the compiler constructs a control flow graph representing all valid control transfers.
This sounds complicated, but since software is deterministic, we can actually create that graph pretty
reliably.
Then the CFI instruments the code to insert checks before each indirect control transfer like function
pointers Vtables indirect jumps like the branch we saw before, and so on.
Then CFI assign labels to indirect calls and returns.
It means that valid function entry points are tagged with metadata or labels.
Then at runtime, whenever we jump or try to do a control transfer, Sphere, we validate that we are
going to jump to something that is labeled, that is allowed.
Then there is actually runtime enforcement.
CFI verifies each indirect branch against the predefined valid targets.
If the redirect is to unexpected destination, the program is immediately terminated, thus stopping
the exploitation.
So in the wild, CFI is very much used in OS level security windows, Linux, Android.
It's implemented in modern compilers, so you can choose to enable it for your systems or code.
There are some limitations though.
Performance overhead is one of the biggest ones.
It could slow down the execution up and sometimes it's not acceptable.
There are some bypass techniques like using memory leaks or exploiting trusted execution flows.
still in a way that is unintended and it requires developer adoption.
So we must enable it in compile time.
And it's hard to enable on third parties, especially binary third parties.
Since the CFI the CFI calculates the CFG based on source code.
Now, just as a quick example of how indirect branch looks like in ARM when CFI is actually enabled.
So let's take this branch for example to uh to x zero that contains the address of target function.
There are no checks here in the branch itself.
But when we go to the function and branch into the address you see this BTI here.
This is actually the protection tag.
And this uh, shows the system that it's okay to branch to this location.
But let's say we overwrite x zero X0 which address to a job.
Let's say we put the address of this command move.
Uh x29 SP then since there is no BTI here, CFI will stop and say no, you cannot jump here because
it's not an allowed, uh, location.
So this is how it looks like after all the steps we discussed are being applied.
What's next?
So you're going to write your first ROP exploitation.
And that will be exciting.
It's, uh, looks like magic at first, but then it works and it's pretty cool.
And next in line we're going to learn about the art of heap feng shui or heap shaping.
Stay tuned.
It's going to be interesting and practical.
Thank you so much and see you in section four.