
PoC CVE-2017-5123 - LPE - Обход SMEP/SMAP. Без KASLR
PoC CVE-2017-5123 - LPE - Обход SMEP/SMAP. Без KASLR
В этом небольшом руководстве я проанализирую уязвимость ядра, которая позволяет получить привилегии root.
Данный файл разделён на четыре части:
Хочу отметить, что существуют гораздо лучшие способы эксплуатации этой CVE (действительно, это всего лишь PoC для изучения ядра, его нельзя использовать в реальных условиях), но я считаю, что данная методология может быть полезна как введение в эксплуатацию ядра.
Эта уязвимость была внедрена в коммите 4c48abe91be0, поэтому нам нужно собрать эту версию ядра.
Это может быть немного сложно, так как это старая версия, и код должен быть пропатчен.
Я сделал репозиторий с уже пропатченным кодом ядра и файлом .config, так что вы можете клонировать и собрать.
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:
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.
sudo pacstrap /tmp/mount1 base base-devel vim
Наконец, можем модифицировать систему:
# Добавляем пользователя '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:
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 и посмотрим код:
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):
/*
* 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 — это две функции безопасности, внедрённые в ядро для усложнения написания эксплойтов. Следует отметить, что эти функции реализуются процессором.
SMEP предотвращает исполнение пользовательского кода, когда процессор находится в режиме супервизора; SMAP, напротив, блокирует чтение/запись в пользовательскую память.
Ядру необходимо читать/записывать данные в/из пользовательской памяти, и это можно сделать двумя способами:
copy_from_user), которые позволяют копировать память в пространство ядра;Как видно из определения unsafe_put_user, эта функция просто копирует значение x в память, на которую указывает ptr (и переходит к err_label в случае ошибки). Мы только что сказали, что ядро не может обращаться к пользовательскому пространству из-за SMAP, и поэтому эти функции следует оборачивать между user_access_begin/end().
#define __uaccess_begin() stac()
#define __uaccess_end() clac()
Как видим, user_access_begin/end — это просто ASM-инструкции stac и clac.