The Kernel Self-Protection project aims to make Linux more secure

Kernel Keeper

© Lead Image © stylephotographs, 123RF.com

© Lead Image © stylephotographs, 123RF.com

Author(s):

Security vulnerabilities in the kernel often remain undetected. The kernel hacker initiative, Kernel Self-Protection, promotes safe programming techniques to keep attackers off the network, and, if they do slip through the net, mitigate the consequences.

Any Black Hat who finds a previously unknown vulnerability in the Linux kernel has hit the jackpot. Potentially millions of servers and embedded devices are suddenly open to attack, and the attacker can usually gain root privileges. Users clearly don't want this to happen, and kernel makers try to prevent such events.

Based on pure theory, strict coding standards and a sophisticated software quality management system ensure that loopholes are found immediately and eliminated before the release. A shining and rare example is OpenBSD, which in 20 years' time has only had two significant security breaches [1]. Although I am certainly a supporter of the vigilant approach followed by OpenBSD, we have to be realistic: The Linux kernel contains mountains of code that no one can review with the required depth; dependencies vary, and thus so do the possible attack vectors. (See the box entitled "Harmless Start" for a complex example.)

Harmless Start

Some security breaches are well hidden. CVE-2015-7547 started with an innocuous-sounding bug report on Glibc 2.20 [2]: A programming error that had probably existed since Glibc 2.9 caused a program crash. Six months later, it turned out that a skillful combination of access turned the error into an attack.

The Glibc programmers had provided a buffer of 2048 bytes on the stack for a DNS response, which they wanted to put back on heap if the response was larger. But this sometimes went wrong, because the function queried an IPv4 and an IPv6 address and tried to use the rest of the buffer in the second answer. Only if that didn't work was the bigger buffer requested on heap. However, the variable that held the pointer for this buffer was not updated, so that access to the stack continued. A stack overflow was made possible by the fact that the size check no longer fit the situation.

Taking advantage of this (now remedied) error situation involves some complications for the attacker: For example, they would have to send DNS packets larger than 2KB. According to the vulnerability investigators, the attacker therefore either needs control over a DNS server or at least the ability to spoof DNS packets as the man-in-the-middle [3]. Alternatively, an attack could also be triggered by clever timing. To do this, the second response has to arrive after the time out and the first response needs to be larger than 2KB. For the second response, Glibc then incorrectly sets the buffer size to "large," while it only allocates a small buffer to the stack. This allows a stack overflow.

Because the attack requires an unusual combination of parameters and complex dependencies, it is difficult to detect. One measure that could help detect such an attack is a canary (described later in this article). If a check reveals the canary value has changed, a buffer overflow has occurred. Other measures include address space layout randomization. A non-executable stack would have made the attack more difficult and thus reduced the risk.

The complexity of the Linux kernel means that it is likely to carry legacy ballast and bugs for an indefinite period of time. At the end of 2010 [4], Jonathan Corbet checked how long the safety-relevant bugs eliminated in that year had existed until discovered: 22 of the 80 loopholes examined had been in the code for more than five years!

Practical experience leads to an approach that simultaneously makes attacks more difficult and reduces the consequences of exploitable code weaknesses. This two-pronged approach is the goal of the Kernel Self-Protection [5] project.

Break-In Technology for Everyone

Viewed through the looking glass with sufficient hindsight, most attacks on programs work in a similar way. An attacker tries to add new program code to a running process, which the hijacked process then executes with its privileges. The added code can be SQL or shell commands, or typically, binary code in kernel attacks. In order to inject this code, attackers exploit programming errors that allow them to determine memory contents and manipulate the program counter.

The program counter is a CPU register that points to the next instruction to be executed. For Intel processors, the 16-bit programs had the Instruction Pointer (IP), the 32-bit world had the Extended IP (EIP), and the 64-bit world has a somewhat-morbid Relative Instruction Pointer (RIP). Almost all attacks aim to change the contents of this register in such a way that it points to one of the attacker's commands instead of the next command intended by the programmer.

Direct write access to this register is virtually impossible, which makes a small detour necessary. When a program calls a function, the program copies the return address (i.e., the point at which the program will continue running after calling the function) to the stack. Since the stack also contains local function variables that the attacker may be able to manipulate through input, this return address is a popular destination.

A Leap Back to Ruin

The attacker has several options for changing the address. A buffer overflow is a bit rough: The attacker simply overfills a variable with excessive amounts of data and floods the return address (Figure 1). Slightly more subtle attacks rely on format string vulnerabilities, which make it possible to both read and manipulate the stack.

Figure 1: If an attack succeeds in storing more data in a buffer on the stack than the programmer had intended, the attacker can overwrite the valid return address in the buffer overflow.

Integer overflows are another possibility; for example, an attacker might exploit the fact that signed and unsigned integers have different value ranges. The unsigned number 128 could become a -128. If the program only checks whether a certain upper limit is complied with, for example, because there is a maximum of 100 values on the stack, -128 is technically OK. Instead of occupying memory locations 0 to 100 as the programmer intended, the range now extends from -128 to 100, which can trigger an overflow situation.

Run for Cover

An attacker can attempt to manipulate the stack or heap. Both are storage areas in which data is normally stored. Therefore, a popular avoidance strategy, which Kernel Self-Protection also advocates, is to mark memory areas as either executable or non-executable (in other words, to declare some areas as code and others as data). Newer CPUs have a bit for this in the page descriptor. AMD refers to this as the No Execute (NX) and Intel as the Execute Disable (XD) bit. Linux kernel supports write protection in this context.

Although these access rights do not provide 100 percent protection, they at least make the attack more difficult. For example, advanced attackers could still gain control of the system with a Return to Libc attack, which writes the entry address of execve() to the stack along with suitable parameters [6] [7].

Successful attackers first only manage to influence the memory of a hijacked process. It's a good thing that modern processors contain functions that separate the kernel memory from application memory. In this case, a kernel function must not execute any commands that have been injected into the user space. This protection also makes attacks more difficult.

Canary vs. Overflow

A stack overflow overwrites parts of the stack. Protection can be provided by a canary, a bit sequence prior to the return address that the attacker cannot predict. If the patterns no longer match, that means somebody overwrote the stack. This is a warning, in the same way that a canary warned miners of mine gas.

A skillful attacker can work around this canary protection. For example, a technique called trampolining bends existing pointers in the program so that they point to the return address. However, this approach requires considerably more effort and the use of vulnerable pointers. Similarly, an attacker might try to read out the canary value and adjust the buffer overflow appropriately, but the programmer can counteract this step by randomizing the canary.

Shadow stacks provide a similar line of defense: They keep a copy of the return address, which the attacker is unlikely to access. The process checks whether the copy matches the original before it is returned [8].

The heap, as well as the stack, is sometimes vulnerable to overflows. Attackers often take advantage of use-after-free errors: The programmer releases memory with free(), but then uses the pointer again later on. Such errors can be corrected by more stringent checks when accessing memory areas in the kernel.

Invitation to Gamble

Attackers rely on well-known facts relating to the operating system's memory layout to inject their code (Figure 2). If the kernel randomizes the design of the addresses, attacking is like gambling. Although there are attacks like heap spraying, the overhead is growing. If both the kernel and the kernel modules are loaded at random addresses, and possibly in a random order, attacks on the kernel modules are more difficult. Similarly, random stack base addresses increase the effort required for a successful attack.

Figure 2: Normally, the distribution of the address space is defined on a 32-bit system of the I-386 architecture. The heap and stack are moving towards each other from two sides as their size grows dynamically.

The more chance comes into play, the more important it is for the attacker to find out something about the memory structure or the value of canaries through information exposure. Attackers take advantage of the fact that released memory is usually not overwritten. Uninitialized variables or format string vulnerabilities also help.

To avoid this kind of information gathering, it is important to overwrite the memory immediately after releasing it. In addition, functions can be accessed via IDs and a table, analogous to the interrupt vector table, instead of directly using their addresses. This precaution means that the addresses outside the kernel are not known and are more difficult for attackers to predict.

Modular Is Cool

Attackers who have managed to inject malicious code in spite of the resistance usually look to install a rootkit, which they can do simply by loading their own kernel module into the system. To prevent this, the admin could compile the kernel statically – without module support – but a static kernel is often inconvenient, unless it is used with an embedded devices. Alternatively, the Self-Protection project suggests that only a few locally logged-in users should be allowed to load modules.

Conclusions

You can bet the Linux kernel has many more security problems that aren't yet listed in the CVE databases. The goal of the Kernel Self-Protection is to establish self-defense functions such as address space layout randomization to make the attacker's task more difficult and limit the damage of a successful attack.

When it comes to safe programming practices, doing one thing doesn't mean giving up on another. Targeted code reviews and intensive quality management should be an essential part of any programming effort.

Infos

  1. OpenBSD security page: https://www.openbsd.org/security.html
  2. "In send_dg, the recvfrom function is NOT always using the buffer size of a newly created buffer," CVE-2015-7547: https://sourceware.org/bugzilla/show_bug.cgi?id=18665
  3. Patch for CVE-2015-7547: https://www.sourceware.org/ml/libc-alpha/2016-02/msg00416.html
  4. Corbet, Jonathan. "Kernel vulnerabilities: old or new?": https://lwn.net/Articles/410606/
  5. Kernel Self-Protection: https://www.kernel.org/doc/html/latest/security/self-protection.html
  6. The GNU C Library Reference Manual, "Executing a File": https://www.gnu.org/software/libc/manual/html_node/Executing-a-File.html
  7. C0ntex. "Bypassing non-executable-stack during exploitation using return-to-libc": http://infosecwriters.com/text_resources/pdf/return-to-libc.pdf
  8. Dang, Maniatis, and Wagner. "The Performance Cost of Shadow Stacks and Stack Canaries": https://people.eecs.berkeley.edu/~daw/papers/shadow-asiaccs15.pdf

The Author

Tobias Eggendorfer is a professor of IT security in Ravensburg-Weingarten and a freelance IT consultant (http://www.eggendorfer.info).