Предупреждение: смена PID — нестандартный процесс, и при определенных обстоятельствах может привести к панике ядра.
Наш тестовый модуль будет реализовывать символьное устройство /dev/test, при чтении с которого процессу будет изменен PID. За пример реализации символьного устройства спасибо этой статье. Полный код модуля приведен в конце статьи. Конечно, самым правильным решением было добавить системный вызов в само ядро, однако это потребует перекомпиляцию ядра.
Окружение
Все действия по тестированию модуля выполнялись в виртуальной машине VirtualBox с 64 битным дистрибутивомLInux и версией ядра 4.14.4-1. Связь с машиной осуществлялась с помощью SSH.
Попытка #1 простое решение
Пару слов о current: переменная current указывает на структуру task_struct с описанием процесса в ядре(PID, UID, GID, cmdline, namespaces и т.д)
Первой идеей было просто поменять параметр current->pid из модуля ядра на нужный.
static ssize_t device_read( struct file *filp,
char *buffer,
size_t length,
loff_t * offset )
{
printk( "PID: %d.\n",current->pid);
current->pid = 1;
printk( "new PID: %d.\n",current->pid);
,,,
}
Для проверки работоспособности модуля я написал программу на C++:
#include <iostream>
#include <fstream>
#include <unistd.h>
int main()
{
std::cout << "My parent PID " << getppid() << std::endl;
std::cout << "My PID " << getpid() << std::endl;
std::fstream f("/dev/test",std::ios_base::in);
if(!f)
{
std::cout << "f error";
return -1;
}
std::string str;
f >> str;
std::cout << "My new PID " << getpid() << std::endl;
execl("/bin/bash","/bin/bash",NULL);
}
Загрузим модуль коммандой insmod, создадим /dev/test и попробуем.
[root@archlinux ~]# ./a.out
My parent PID 293
My PID 782
My new PID 782
PID не изменился. Возможно, это не единственное место, где указывается PID.
Попытка #2 дополнительные поля PID
Если не current->pid является идентификатором процесса, то что является? Быстрый просмотр кода getpid() навел на структуру task_struct, описывающую процесс Linux и файл pid.c в исходном коде ядра. Нужная функция — __task_pid_nr_ns. В коде функции встречается обращение task->pids[type].pid, этот параметр мы и изменим
Компилируем, пробуем
Так как тестировал я по SSH, мне удалось получить вывод программы до падения ядра:
My parent PID 293
My PID 1689
My new PID 1689
Первый результат, уже что-то. Но PID все равно не изменился.
Попытка #3 не экспортируемые символы ядра
Более внимательное изучение pid.c дало функцию, которая делает то, что нам нужно
static void __change_pid(struct task_struct *task, enum pid_type type,
struct pid *new)
Функция принимает задачу, для которой надо изменить PID, тип PID и, собственно, новый PID. Созданием нового PID занимается функция
struct pid *alloc_pid(struct pid_namespace *ns)
Эта функция принимает только пространство имен, в котором будет находиться новый PID, это пространство можно получить с помощью
task_active_pid_ns
.Но есть одна проблема: эти символы ядра не экспортируются ядром и не могут использоваться в модулях. В решении этой проблемы мне помогла замечательная статья. Код функции find_sym взят оттуда.
static asmlinkage void (*change_pidR)(struct task_struct *task, enum pid_type type,
struct pid *pid);
static asmlinkage struct pid* (*alloc_pidR)(struct pid_namespace *ns);
static int __init test_init( void )
{
printk( KERN_ALERT "TEST driver loaded!\n" );
change_pidR = find_sym("change_pid");
alloc_pidR = find_sym("alloc_pid");
...
}
static ssize_t device_read( struct file *filp,
char *buffer,
size_t length,
loff_t * offset )
{
printk( "PID: %d.\n",current->pid);
struct pid* newpid;
newpid = alloc_pidR(task_active_pid_ns(current));
change_pidR(current,PIDTYPE_PID,newpid);
printk( "new PID: %d.\n",current->pid);
...
}
Комплируем, запускаем
My parent PID 299
My PID 750
My new PID 751
PID изменен! Ядро автоматически выделило нашей программе свободный PID. Но можно ли использовать PID, который занял другой процесс, например PID 1? Добавим после аллокации код
newpid->numbers[0].nr = 1;
Комплируем, запускаем
My parent PID 314
My PID 1172
My new PID 1
Получаем настоящий PID 1!
Bash выдал ошибку, из-за которой не будет работать переключение задач по комманде %n, но все остальные функции работают отлично.
Интересные особенности процессов с измененным PID
PID 0: войти нельзя выйти
Вернемся к коду и изменим PID на 0.
newpid->numbers[0].nr = 0;
Комплируем, запускаем
My parent PID284
My PID 1517
My new PID 0
Выходит PID 0 не такой и особенный? Радуемся, пишм exit и…
Ядро падает! Ядро определило нашу задачу как IDLE TASK и, увидев завершение, просто упало. Видимо, перед завершением наша программа должна вернуть себе «нормальный» PID.
Процесс-невидимка
Вернемся к коду и выставим PID, гарантированно не занятый
newpid->numbers[0].nr = 12345;
Комплируем, запускаем
My parent PID296
My PID 735
My new PID 12345
Посмотрим, что находится в /proc
1 148 19 224 288 37 79 86 93 consoles fb kcore locks partitions swaps version
10 149 2 226 29 4 8 87 acpi cpuinfo filesystems key-users meminfo sched_debug sys vmallocinfo
102 15 20 23 290 5 80 88 asound crypto fs keys misc schedstat sysrq-trigger vmstat
11 16 208 24 291 6 81 89 buddyinfo devices interrupts kmsg modules scsi sysvipc zoneinfo
12 17 21 25 296 7 82 9 bus diskstats iomem kpagecgroup mounts self thread-self
13 176 210 26 3 737 83 90 cgroups dma ioports kpagecount mtrr slabinfo timer_list
139 18 22 27 30 76 84 91 cmdline driver irq kpageflags net softirqs tty
14 182 222 28 31 78 85 92 config.gz execdomains kallsyms loadavg pagetypeinfo stat uptime
Как видим /proc не определяет наш процесс, даже если мы заняли свободный PID. Предыдущего PID тоже нет в /proc, и это весьма странно. Возможно, мы находимся в другом пространстве имен и поэтому не видны основному /proc. Смонтируем новый /proc, и посмотрим что там
1 14 18 210 25 291 738 81 9 bus devices fs key-users locks pagetypeinfo softirqs timer_list
10 148 182 22 26 296 741 82 90 cgroups diskstats interrupts keys meminfo partitions stat tty
102 149 19 222 27 30 76 83 92 cmdline dma iomem kmsg misc sched_debug swaps uptime
11 15 2 224 28 37 78 84 93 config.gz driver ioports kpagecgroup modules schedstat sys version
12 16 20 226 288 4 79 85 acpi consoles execdomains irq kpagecount mounts scsi sysrq-trigger vmallocinfo
13 17 208 23 29 6 8 86 asound cpuinfo fb kallsyms kpageflags mtrr self sysvipc vmstat
139 176 21 24 290 7 80 87 buddyinfo crypto filesystems kcore loadavg net slabinfo thread-self zoneinfo
По прежнему нашего процесса нет, а значит мы в обычном пространстве имен. Проверим
ps -e | grep bash
296 pts/0 00:00:00 bash
Только один bash, с которого мы и запускали программу. Ни предыдущего PID, ни текущего в списке нет.
Ссылка на github
Комментарии (18)
arheops
20.12.2017 05:00Тоесть вы только что нашли критичную ошибку ядра линукс(стандартные ps/psmisc не видят процесс, запущенный стандартным образом) и вот так свободно об этом на всеобщее обозрение выложили статью?
Замечательный метод для руткитов.
А побочные эффекты есть? Выделение памяти/ресурсов? renicing/schedulering? top что показывает при 100% загрузки ядра этим процессом например?EvilMan
20.12.2017 11:48Чтобы это провернуть, надо сначала этот кастомный модуль ядра загрузить. А для этого уже нужны особые привилегии. Критической ошибкой не является.
arheops
20.12.2017 11:49Не особые, а рутовые. Но вопрос не в доступе к руту, есть куча других експлоитов. Вопрос в том, что процесс невидим после этого хака.
Gravit Автор
20.12.2017 11:58+1Если вы можете загрузить любой модуль ядра — считайте, что все в ваших руках. У вас есть тысяча и одна способов обмануть стандартные приложения, системы защиты, пользователей и получить полный контроль над системой.
arheops
20.12.2017 12:12Выглядит, конечно, странно. Наверно так, поскольку нет записи в /proc/.
В подсчете загрузки участвует, но записи нету.
А вот если в нем запустить чтото еще, то это уже видно.
AntonAlekseevich
20.12.2017 05:38Интересно, а это будет работать от имени простого пользователя? (Это же как нелегальная эскалация на кольцо 0 получается. Если PID/PPID можно изменить на 0. (Хотя в обоих случаях могу ошибаться.))
kmeaw
20.12.2017 10:43Никак, обычный пользователь не сможет загрузить такой модуль ядра.
AntonAlekseevich
20.12.2017 14:18Имеется ввиду после загрузки модуля рутом.
Gravit Автор
20.12.2017 15:51Зависит от прав, выставленных на устройство /dev/test. Если читать можно(модуль работает на чтение) то pid изменится. В планах реализовать смену pid через запись, найти бы нужную функцию для преобразования в число. Процесс в пользовательском пространстве, так и останется.
lorc
20.12.2017 16:00хм. atoi()? Вообще, я бы сделал через procfs или sysfs. Там есть нужные примитивы для работы со строками, которые приходят из юзерспейса.
lorc
20.12.2017 15:59Ну если рут грузит в ядро код, который позволяет делать с ядром что угодно и кому угодно…
Никакой дополнительной защиты тут нет. Разработчики ядра считают что вы знаете что делаете.
alex-pat
20.12.2017 12:01-1Знаете, я несколько сомневаюсь, что подобные эксперименты стоит выставлять на показ общественности, даже под тэгом 'ненормальное программирование'. Я уже молчу о большом количестве неточностей в матчасти, и сомнительном выборе в сторону модуля в качестве реализации.
MooNDeaR
20.12.2017 12:34А kill убивает невидимый процесс или нет?
Gravit Автор
20.12.2017 12:40Он его просто не находит
-bash: kill: (12345) - No such process
MooNDeaR
20.12.2017 15:48Вот это дыра :) Я понимаю, что если получен рут доступ — это считай компроментация всей системы. Но теперь можно его получить, затереть все следы и скрыть своё присутствие практически навсегда + сделать невозможным своё уничтожение.
lorc
20.12.2017 16:02Все руткиты делают именно это. В том или ином виде. Правильно установленный руткит изнутри системы задетектить невозможно. Только исследуя систему снаружи.
brrr
То ли статья обрывается, то ли я не понял: какой вывод и зачем это надо?
Gravit Автор
На этот вопрос каждый ответит по своему. Кому-то(как мне) было очень интересно написать такой модуль и получить неожиданные результаты. Кому-то — прочитать об этом и попробывать самим. Что касается вывода — спасибо за замечание, исправлюсь.