Hi, I'm Zoltan Laszlo Nemeth from the University of Szeged, and in this talk I would like to present a short overview of memory corruption attacks and defenses.
I'm a mathematician working as a lecturer at the University of Szeged who is interested in information security in general and especially about ethical hacking.
I also feel responsible for security in general and especially for security education, and I'm also affiliated to the hackerspace Szeged.
Thus, according to the principle of hackerspaces, while I'm mainly still learning, I have just started to make and share something.
In this talk, I will briefly discuss the problem of low-level programming languages.
Then I will present a general model of memory corruption attacks taken from a recent survey article.
Afterwards, I will talk about some currently deployed protections, some advanced attacks and defense methods with a short demo.
Then I will mention some latest defense proposals, and my talk will end by drawing some conclusions.
Basically, the problem is very simple.
Low-level programming languages are unsafe.
You know C and its variants, C++, C Sharp, and Object C, are used extensively in operating system kernels, system drivers, and embedded systems, just to name a few.
I was told that the C language is the language of the programmer profession.
Moreover, we have millions of lines of legacy C code, but the problem is that C is type-unsafe.
And what's more, both check-in and dynamic memory management are solely the responsibility of the programmer, so C is memory-unsafe.
Hence, it is not surprising that it is error-prone as well.
And indeed, we can enumerate the series of vulnerabilities from the Morse form to Heartbleed, or just think of the Ponton contest, where even the most hardened versions of operating systems and browsers are exploited in every year.
Nevertheless, C is still very popular.
Here you can see the ranking of the most popular programming languages by the IEEE.
Here the icons...
So here the icons means web application, mobile application, languages used for enterprise, desktop, and scientific applications, and for embedded systems, respectively.
It is especially important to fight against memory corruption bugs, but it is difficult.
It is a great challenge both for academia and industry, but it is still considered to be answerable for more than three decades.
We have seen a huge number of proposals.
We mentioned a few, but only a few mitigation methods are deployed in practice today.
And yes, they are still insufficient to stop real-world attacks, as the situation naturally raises the question of why this is so.
I like this figure very much, as it tells us that we should not forget about other aspects, namely usability and functionality in favor of security, but we must make a balance among these sometimes conflicting goals.
If I try to translate or adapt this to the area of runtime attack mitigation techniques, then security becomes robustness, that is the type of attacks that can be prevented by the defense, and how effective the method is.
The aspects of ease of use changes to performance.
It is very important, as studies show, that no mitigation technique with more than 5-10% of runtime overhead can have widespread adaptation in practice.
And perhaps functionality should be replaced by compatibility, which can be either binary compatibility, meaning that the defended modules still comply with unmodified libraries, and if it is not possible, we require source compatibility, meaning that no manual annotation of the source code is necessary.
But besides this, modularity, that is a possibility to introduce the same defense for different modules independently from each other, is often an important issue.
Now, let me present a model of memory corruption attacks.
This is taken from an excellent survey article by Sekeres, Peyarvey, and Song.
On the bottom line here, you can see the goal of the attacker, which may be code corruption or control flow hijack, data-only attack, or a memory leak.
And the steps of the attacks are shown above.
Let's see.
So, in this model, in the first phase of the attacker, in the first phase or first step, the attacker must make a pointer invalid.
Memory corruption bug in the program may cause a pointer either go out of bounds or become dangling.
In the first case, we have a spatial error, and in the second case, we have a temporal error.
In both cases, the next step is to dereference the corrupted pointer, which can be performed by either a write or free or by a read operation.
For instance, a spatial pointer error might be caused by a classic buffer overflow underflow or by allocation of failure generating a null pointer dereference that might be exploitable in kernel space, and by indexing bugs, integer overflows, truncation and sadness bugs,
and by incorrect pointer casting.
Temporal pointer errors are also called use-after-free bugs, because there, a dangling pointer is dereferenced, that is used after the memory area it points to has been deallocated, that is freed by the memory management system during a free instruction.
But know that a pointer to a local variable can also become dangling when it is assigned to a global pointer, and the subroutine returns, freeing the local variable from the stack.
Moreover, double frees are also typical memory errors.
In the second step, the attacker dereferences the corrupted pointer.
He or she can use it either to write, to free, or to read.
Thus he can, in the first case, override the return address on the stack, or override a function pointer in a vtable, or override the length field of a string object, or by freeing, he can override a heap metadata, or override a function pointer of an object on the heap.
But know that even just by reading, the value of a corrupted pointer can be used to write malicious data that cause further corruption, or reading a corrupted pointer may lead to execution of malicious code.
Next, the goal of the memory corruption of the first two steps may be quite different.
The attacker can either modify another data pointer, and repeat the phase one and phase two with this newly corrupted pointer.
Oh, sorry.
Or he can modify,
or he can modify code to this attacker-specified code called shellcode, and achieve a code corruption attack.
Next, he can corrupt a pointer either to an address of his injected shellcode, or to an address of a code snippet of other memory modules called gadgets, and later by an indirect call, indirect jump, or return instruction, the attacker can hijack the control flow.
This is the third type of attack.
And please note that it is also possible to just modify a data variable, and if it is a sensitive data variable, that the attacker can execute a data-only attack.
And finally, by corrupting an output variable, like here, for instance, the length field of a string, information can be leaked.
So there are several possibilities.
Next, let us briefly discuss currently deployed runtime protection.
First, we have stack cookies, also called canaries, which are random values between the local buffers and the return addresses on the stack.
They can be used to detect only continuous stack-based buffer overflows, since when the buffer overflows, the canary value is overwritten as well, which can be detected later when the function returns.
In Windows, protection called safeSheck and SheckHop, structured accession handler overwrite protection, may validate the integrity of the accession handler pointers and the accession chain.
The main problem with these techniques is that these are only partial solutions.
That is, they only provide some very specific type of control flow integrity, and stack cookies can be bypassed by direct overwrites, for instance, using indexing errors, or they can be circumvented by memory leaks.
The next main widely deployed mitigation technique is the write-sort-execute policy.
Please remember that it has two equally important sides.
The first is called non-executable data, or data execution prevention, abbreviated as DAP in the Windows terminology.
And the second might be called non-writable code or code integrity.
Nowadays, these are implemented by memory page protection in modern processors.
Put in simple terms, we must have write or read permission for data, but never execute writes.
And similarly, we must have execute or read permission for code pages, but never write at the same time.
Thus, as in many other areas of security, the clear separation of data and code is crucial here as well.
While this policy works well and it enjoys hardware support, so it has a negligible overhead, but it just protects against code injection attacks and is no use against the more sophisticated code reuse attacks, like return to libc, return-oriented programming,
or jump-oriented programming.
It also has some other issues, because separation of data and code is not as easy as it first seems.
We must handle the problem with just-in-time compilation of script languages, where, for instance, for a browser, the scripts are user-supplied input data, but they must be turned into executable code.
Also, there are self-modifying code, like Packers.
Thus, that forces attackers to apply code reuse instead of code injection.
And today, the main defense against code reuse attacks is address-based randomization, especially address-based layout randomization, abbreviated as ASLR.
It simply randomizes the base address of the stacks, heaps, and the main executables and shared libraries.
It works all right by concealing the address of the possible gadgets from the attacker, but it also has some weakness.
First, it requires a position-independent executable that has a 10% runtime overhead, or on a 32-bit platform, the entropy of the usual ASLR implementation is rather weak, which is subject to brute-force attacks.
Moreover, partial address overrides and memory leaks can defeat ASLR as well.
And similar to DEP, ASLR is also an all-or-nothing type defense, meaning that just one non-ASLR module can break the whole protection totally.
Now, let us look at some more advanced defense techniques.
As I mentioned earlier, in the standard ASLR, only the base addresses of the code modules are randomized.
To increase the entropy of the code locations, methods achieving slow, so-called fine-grade randomization were proposed.
The aim here is that a single leaked pointer should not defeat the whole defense.
The randomization can be applied at the following levels.
In a module, we can permute all functions, or just basic blocks, that is, parts with a single entry and exit point, or we can apply instruction-level randomization.
Of course, this makes ROP attacks harder, but adds little protection to the standard ASLR against return-to-lib attacks or other ROP attacks which require just a single address.
Furthermore, with repeated memory leaks, ROP attacks are still possible, as we shall see.
Here you can see how a new type of attack called just-in-time ROP attack works.
The main assumption here is that, besides a vulnerability that allows ROP attacks, the attacker also has a memory-disclosure vulnerability which allows him to read from arbitrary memory address in a process-virtual memory.
But as you probably know, trying to read from an unallocated address causes segmentation fault, and the application usually crashes.
The attacker also needs a single valid runtime memory address, but of course it is not hard to obtain, and the important thing here is that it is also sufficient for a real-world attack.
Here, let this address be the address of function A
, and
using the above-mentioned memory leak, the attacker can read and later disassemble the whole memory page that contains function A, here denoted by page 0.
And the disassembly will certainly reveal other valid memory addresses as well.
For instance, here the instruction call function B, reveals the address of the function B, of course, and another code page it contains in it.
It is contained in code page 1 here.
And by repeating this process, the attacker can gain a significant amount of disassembled pages.
So just by going in cycles, he can obtain many disassembled code pages.
And finally, with a runtime gadget finder and the GTROP compiler, he can generate a ROP payload that works no matter how fine-grained the ISLR is applied.
Moreover, if we have a user-scripting environment, as in a browser or during a document-based attacks, then the whole process can be automated, has a common memory leak together with a ROP, and scripting can defeat even the strictest DAP plus ESLR implementation.
ESLR being insufficient, the control flow integrity approach has gained a considerable interest recently.
The main idea here is to prevent control flow hijack attacks by restricting the control transfer to a regular benign control flow of the application.
Technically, it can be done first by computing the so-called control flow graph of the application, and then monitoring the executable's runtime behavior according to this graph.
And any deviation from the standard control flow, like a return to an unusual address, indicates a control flow hijack, so the attack can be stopped.
But this method has drawbacks as well.
The main problem is that precise validation of all indirect control flow transfers, namely all indirect calls and all indirect jumps and all returns, moreover only allowing return to the original callers, introduces a rather high overhead, about 21%.
Therefore, in practice, usually some relaxed versions are used instead.
So-called coarse-grained control flow integrity policies are implemented, which, as we'll see soon, can be defeated by advanced drop attacks.
So, a CFI, control flow integrity policy, depends on the type of indirect branches that are checked.
They usually also use some behavior-based heuristics.
For example, a frequent use of return to a small code snippet may indicate a drop attack, and the time of check can be varied as well.
In paper 4, the authors clearly demonstrated the weak points of the coarse-grained control flow integrity approach.
They took these five representatives implementing this defense, namely CFI for coarse binaries, K-Bombshell, Ropecker, RobGuard, and Microsoft EMAT.
And what they did, they derived a combined, most restrictive CFI policy from all these programs, and showed that even this combined policy can be bypassed using two new types of gadgets, which they called a core-red-pair gadget and a long-nop gadget.
Now, as a concrete example, let us look at Microsoft EMAT.
EMAT stands for Enhanced Mitigation Experience Toolkit.
It is a free mitigation tool that can be downloaded from the web.
The main goal of the EMAT project is to enhance modern protection to earlier versions of Windows.
It is highly modular, and it has no less than 14 different mitigation options that can be turned on and off on a per-application basis, solving many incompatibility problems.
But like most practical tools today, it is not valid proof.
Even Microsoft warned us that it can be bypassed.
The first main problem is that it is a user space protection.
Has the attacker of the offensive security been able to turn off the defense of the application completely several times, or the mitigation can be circumvented one by one?
Just to show you a short demo.
So, for this demo, I took a test disk utility, which is a very nice tool from CG security.
It's basically a small utility that can be used to recover lost partitions in partition tables.
And it saved my life at least one time when I installed Windows over a Linux system.
But the main problem is that this utility also accepts
image file as input.
And Dennis Andrzejkovic in last May found a buffer overflow, a simple buffer overflow vulnerability in this application.
And what I did was that I exploited this vulnerability, which was easy, but I used it to turn on the mitigation technique of Emmet one by one, and I was able to bypass it finally.
So, just to show you, an attacker can use a small script like this.
Sorry, I do not have time to go into the details.
I am willing to show you the whole code and how the exploits works after the talk.
But the script is used just to generate, just to generate, yes, a file called GameOverExploitBin.
And if I use it as input, then, okay, just one moment.
I would like to show you that Emmet is in effect.
So this is the graphical user face of the tool that you can see that all mitigations are green, turned on, and here we should see, yes, here.
This is the test this utility here.
And as I mentioned, it is very versatile, so I can turn all mitigation methods on and off by per-application basis.
But if I put enter, then nothing happened, because the calculator, just believe me, appeared on my primary desktop.
But I promise you to show you that it works after the talk.
So it is just to demonstrate that even such runtime mitigation tool can be bypassed.
The problem, I cannot return my presentation,
and I do not know why.
Sorry, I must restart it.
Yes, so here we are.
The latest CFI protection solution of Microsoft is called Control Flow Guard.
It was introduced in the preview version of Windows 8.1, but it was disabled in the final edition.
Later it was re-enabled in update 3, and now it can be found on Windows 10.
But it is also a partial and therefore imperfect implementation of control flow integrity, namely because of performance reasons, and only injects checks before indirect calls, hence returns are left unprotected.
Hence it is effective against VT overrides, but requires compiler and linker support, and third-party modules and even old versions of my MS binaries remain unprotected.
And note that even a universal bypass of CFG was demonstrated recently.
Now let me mention some of the recent defense proposals that have arisen recently in academic research.
As far as I know, they are still in prototype phase.
First, the concept of code pointer integrity is an alternative to control flow integrity and code randomization that can guarantee the integrity of all code pointer.
It was formally proven to be correct.
This can be done by separating sensitive control flow data like return address and jump target in a safe, protected region.
You know separation by isolation is one of the basic principles of security.
It also has a relaxed version called code pointer separation, possessing an even smaller overhead.
The main idea of the tool called Redactor is to implement execute but not read policy in order to do code pointer hiding against the pointer harvesting phase of a gtrop attack.
But it needs hardware support for this, which is called hardware accelerated paging, but Redactor also requires OS and kernel extensions as well.
Another very interesting proposal called Isomeron follows a totally different strategy.
Instead of preventing, it tolerates memory disclosures.
To achieve this goal, it randomizes control flow transfers.
For this, it needs two clones called isomers, and it needs isomers of all functions in the memory.
Of course, this roughly doubles the memory requirements for a code, but you know, space is far less an issue than time overhead.
It works as follows.
On each call and return instruction, isomeron randomly determines whether to switch to the other isomer or keep the execution in the current one.
These non-deterministic returns and calls make gtrop attacks impossible.
The outdoors experiment shows that the tool has acceptable time overhead, and it can be integrated into a compiler.
And the last tool we will see is called Hafix.
It is a hardware assisted control flow integrity solution that is significantly more efficient than existing software solutions.
It uses a shadow stack to enforce the intended control flow more precisely than coarse-grained CFI tool deployed today.
For instance, it enforces returns to target not just any co-preceded instruction, but only those that are in function that is currently being executed.
This can be done by three new processor instructions.
As a conclusion, we may say that the problem of R-CFC code is far from being solved at the moment.
It should be obvious that there is no silver bullet, as securing a large amount of R-CF legacy code is easy and will remain hard.
It also seems that while the academic world looks for perfect solutions and is concerned less about real-life realization of the mitigation proposals, the business sector is rather interested in very fast mitigation against current mainstream exploits and is less concerned about possible bypasses.
Of course, inefficient and incompatible solutions are useless in practice, but even a small trade-off for efficiency can totally destroy the defense, as we have seen in the case of coarse-grained control flow integrity.
It is also clear that there is an information warfare between attacks and defenses, since known attacks can be mitigated as known defenses can be bypassed.
The possible solution can be at different levels, though we mainly discuss software-based techniques here, as hardware-based solutions usually need a considerable time to spread.
But many software approaches have the performance overhead as the main bottleneck, and hardware support can bring a relief to this problem.
Lastly, even though it seems unrealistic, perhaps one day we will use type-safe languages instead of C.
Anyway, as we have seen, runtime mitigations are far from being perfect, thus other layers of defenses like secure coding, security testing, and sandboxing should not be neglected.
Okay, that's all I wanted to say.
Thank you for your attention.