Revisiting CVE-2017-7308 with a Data-Only Attack (CFI 3/4)
Reminder: this blog series explores Control-Flow Integrity (CFI) in the Linux kernel. This is the third post, where we revisit the exploit of an existing CVE. If you lack some context, you can access the other posts here:
- LLVM-CFI and the Linux Kernel
- From Crash Report to Root Access: Building an End-to-End Data-Only Exploit
- Revisiting CVE-2017-7308 (this post)
- Revisiting CVE-2017-11176
In the previous two posts, I introduced CFI and its implementation in the Linux kernel, and then we went through creating a data-only exploit from a Syzkaller bug, demonstrating heap grooming and privilege escalation techniques. In this post, we’ll revisit an existing vulnerability, CVE-2017-7308, which allowed control-flow hijacking via function pointer corruption. We’ll see how original exploit works and then see how to adapt it into a data-only attack by targeting another kernel data structure.
This bug was present in the kernel since 2011. It was not found publicly until 2017, when Andrey Konovalov, a member of the Google Project Zero security team, triggered it while fuzzing the kernel using Syzkaller. Konovalov wrote an extensive explanation of the bug, as well as the exploit he wrote to gain root privileges on a Linux 4.8.0 kernel with all common mitigations enabled, and unprivileged namespaces authorized. I highly recommend reading his writeup, as it provides a deep explanation on the different kernel features involved in the bug.
As he describes, the bug results from a signedness issue, which leads to a heap buffer out-of-bounds write. More precisely, the bug resides deep in the code configuring AF_PACKET
sockets. These sockets allow userspace to interact directly with network packets at the device driver level, enabling the implementation of custom protocols on top of the physical layer. These sockets may use ring buffers (same as in the previous post) to speed up data exchange between userspace and kernelspace.
However, the kernel fails to correctly check whether packet data fits inside the ring buffer, resulting in the kernel writing past the buffer and overwriting adjacent objects. The attacker gains significant control over the corruption primitive: the heap out-of-bounds write has an attacker-controlled length and offset of up to 64KB, and the content being written is mostly attacker-controlled packet data. The only uncontrolled part is a 14-byte header at the start.
Original Exploit and Vulnerable Structure
In the original exploit, the author manipulates the heap layout similarly to the method described in the previous data-only attack example, but instead of targeting a slab filled with struct task_struct
objects, the target is the struct packet_sock
, which describes a packet socket. The exploit focuses on three fields within this structure:
// Only the important fields shown
struct packet_sock {
struct packet_ring_buffer {
struct tpacket_kbdq_core {
struct timer_list {
void (*function)(unsigned long);
unsigned long data;
} retire_blk_timer;
} prb_bdqc;
} rx_ring;
int (*xmit)(struct sk_buff *);
};
The first two fields, function
and data
, correspond to a timer callback that the user can configure on the packet socket. When the timer expires, the kernel calls function(data)
, effectively providing a function call primitive with an argument controlled by the attacker.
In the exploit, this primitive is used to disable SMAP and SMEP, the mitigations that prevent the kernel from accessing or executing user-space data. To disable these, the attacker must modify the CR4
control register on the CPU, which holds the switches for various CPU features. Bits 20 and 21 of CR4
control SMAP and SMEP and must be cleared to disable the protections.
The exploit overwrites the function
pointer with the address of the native_write_cr4
kernel function, which can update CR4
, and sets the data
field with the new desired value for CR4
. When the timer expires, the kernel invokes native_write_cr4
with the tampered value, disabling the protections.
Next, the exploit uses the third field, xmit
, normally called when packets are transmitted on the network, replacing it with a pointer to attacker-controlled user-space memory containing a payload. When the attacker sends a packet, the kernel jumps to attacker code that elevates the current process’s privileges by modifying its credentials.
Why LLVM-CFI blocks the original exploit?
This attack would be blocked if CFI were enabled in the kernel, since replacing a kernel function pointer with a userspace buffer does not conform to the Control-Flow Graph (CFG) computed by LLVM-CFI. Such invalid function pointer targets cause CFI to trigger a kernel panic, preventing control-flow hijacking.
Adapting the Exploit for a Data-Only Attack
To bypass this, in our data-only exploit, we change the target. Rather than corrupting a struct packet_sock
, we corrupt again a struct task_struct
. This time, we’ll change its cred
field, which stores a pointer to the process’s credential data:
struct task_struct {
// ...
struct cred *cred;
// ...
};
As you can see, task_struct
does not embed the credentials directly but stores a reference. When a new process is created, a cred
structure is allocated and populated with appropriate permissions. Initially, this credential belongs to an unprivileged user. Our goal is to replace the stored pointer to point to credentials of a privileged process.
These structures are dynamically allocated, so leaking pointers to them is usually difficult. However, the special init
task (the first process to run in the kernel) is allocated as a static variable. Its task_struct
and corresponding cred
structures are kernel symbols, so the kernel symbol addresses can be obtained by inspecting the System.map
file:
$ cat /boot/System.map | grep init_cred
ffffffff82a632c0 D init_cred
Since the init
task is privileged, overwriting the cred
pointer in an adjacent task_struct
slab with 0xFFFFFFFF82A632C0
grants powerful privileges to our process.
For now, we assumed KASLR is disabled or already cirumvented (the original exploit relied on a separate information leak to bypass KASLR).
You can see the full exploit code here. The code is based on the orignal exploit of Andrey Konovalov, adapted to overwrite the structure we wanted.
But, KASLR can also be bypassed by using this vuln to create an arbitrary read primitive. Let’s see how we can do that.
Arbitrary Kernel Read via Overwriting struct packet_sock
The original exploit corrupts another interesting field in struct packet_sock
:
struct packet_sock {
// ...
struct packet_rollover *rollover;
// ...
};
The rollover
field points to a structure defined as:
struct packet_rollover {
// ...
long num;
long num_huge;
long num_failed;
// ...
};
There exists a system call, getsockopt()
, that can query various stats about packet sockets, including “rollover” stats, which reads the fields num
, num_huge
, and num_failed
via the rollover
pointer.
Our out-of-bounds write allows overwriting the rollover
pointer with a malicious kernel address. Upon calling getsockopt()
, the kernel reads three long
values from this attacker-controlled pointer and returns them to user-space, creating an arbitrary read primitive capable of leaking kernel memory.
This read primitive enables bypass of KASLR by using the same “poking” technique as described in the previous data-only attack post.
You can read the code of this arbitrary read primitive here. The code simply pokes the addresses and output the content, so it doesn’t directly output the offset values, but the main idea is here.
That’s it! We now have an exploit that also works when CFI is enabled, with minimal changes to the original exploit. In the next post, we’ll do the same for another exploit, corrupting yet another sensitive data structure in the kernel.