2013-12-18 Итак, как же изнасиловать корневой раздел без перезагрузки
Итак, задача — не перезагружаясь, перейти в такой режим, в котором ваш Linux не будет использовать диски совсем. Чтобы, например, иметь возможность с ними что-то поделать.
Главная загвоздка — это init, первый (PID=1) и неубиваемый процесс в системе, запущенный с корневого раздела. Разделы, кроме корневого, отмонтировать обычно довольно легко — достаточно убить все процессы в системе. Однако init убить нельзя, большую часть сигналов, в том числе SIGKILL, он игнорирует (PID’у 1 можно игнорировать SIGKILL), а если у вас и получится его убить — ядро запаникует и остановит работу системы.
Так что init нужно, не выключая, «вытащить» в другой корневой раздел. Способ это сделать, по сути, один — подменить запущенный образ exec()ом другого, который сделает chroot и снова exec. Однако как заставить init сделать exec() того, чего нам надо? А вот как — все init’ы (даже несчастный systemd) умеют перезапускать себя. И хотя это сделано вовсе для другой цели, а именно — для применения обновлений в самом init или системных библиотеках (libc?) без перезапуска системы — это вполне можно заюзать в наших целях… достаточно поверх /sbin/init смонтировать свой файл --bind’ом. После чего команда «init U» перезапустит вместо init’а наш файл. Только сам инит надо куда-то скопировать, типа cp /sbin/init /sbin/init1, мы ж его только что перемонтировали.
А дальше нужно переключать корень. Мне изначально хотелось его переключать обратно в initramfs (то есть rootfs), чтобы «аварийная система» находилась именно там — типа, так надёжнее — загрузился сразу dropbear и всё, никуда уже не уходит и на машину можно зайти по ssh. Если не заморачиваться именно выходом в initramfs, переключить корень ещё проще — достаточно сделать pivot_root. pivot_root — это такой хитрый системный вызов, который подменяет / другой точкой монтирования, а текущий / помещает в подпапку нового, причём (!) насколько я понял, это единственный системный вызов, нагло подменяющий корень всем процессам в системе, в коде ядра прямо так и написано — «foreach (по всем задачам) { если корень == предыдущему, подменить на новый }».
Никаким другим образом подменить корень другому процессу, видимо, нельзя. pivot_root раньше использовался initrd для переключения в «реальный» корень — рамдиск при этом попадал в подпапку корня и потом отмонтировался init’ом, запускаемым exec()ом на месте «старого». Но сейчас это уже не так, initrd теперь называется initramfs, и работает из специальной НЕотмонтируемой и НЕперемещаемой файловой системы «rootfs», которая, по сути, представляет собой просто tmpfs (живущий в памяти), но по-другому названный, монтируемый в / автоматически при старте системы, и недоступный для монтирования потом. В него распаковывается содержимое initramfs (gzip -d | cpio -i, только внутри ядра), в качестве PID 1 оттуда запускается /init, а из-за того, что rootfs НЕотмонтируемый и НЕперемещаемый (так сделано тупо для удобства программирования) отличается метод переключения в новый корень — с помощью утилиты run-init (в терминах klibc) или switch_root (в терминах busybox) initramfs удаляет всё содержимое rootfs, потом монтирует (на самом деле перемещает, mount --move) новый корень прямо поверх старого, а потом делает в него chroot и выполняет там /sbin/init.
Именно поэтому на свежих ядрах в /proc/mounts корень (/) смонтирован всегда дважды, один раз как rootfs, и второй раз как обычный корень. Просто старый rootfs пустой, процессов, видящих его, в системе уже не остаётся, а смонтировать его куда-то ещё раз невозможно — есть явный запрет повторного монтирования в коде ядра — поэтому он висит и практически не отсвечивает. Пустой tmpfs, вроде как, памяти почти не занимает, и оверхед нестрашный.
Но! Если после завершения работы initramfs оставить работающий процесс, запущенный из старого корня — возможность туда попасть сразу же появляется. Достаточно сделать cd /proc/<PID>/root (или chroot туда же, если содержимое осталось). Если этот процесс вообще dropbear — тогда зайдя в него по ssh, вы тоже окажетесь в старом корне. Это, кстати, да — ещё одно откровение: разные процессы в Linux могут видеть файловую систему по-разному, причём даже в рамках одного namespace’а! То есть в разных namespace’ах-то ладно, это же часть функционала контейнеров, полностью изолирующая иерархии ФС друг от друга, но в рамках одного — вообще забавно.
Конкретно, в запущенном из старого корня процессе получается так:
- cd / → попадаем в старый корень (rootfs).
- ls /.. → листается новый корень.
- ls /<любая_папка>/.. → листается новый корень.
- cd /.. → всё-таки остаёмся в старом корне.
- mount --move /.. /root → самое интересное. Новый корень перемещается в подпапку rootfs /root, а остальная система становится как бы запущенной в chroot’е. Но не потому, что произошёл chroot, а потому, что сам корень переместился в новое место :)
А из chroot’а, как известно, можно вылезти — нужно только получить открытый дескриптор на директорию, находящуюся ВНЕ текущего корня. Делается это путём открытия / и потом ещё одного chroot’а в его подпапку. После этого достаточно сделать fchdir в открытый дескриптор и потом «cd ..» столько раз, сколько нужно.
Отсюда у меня созрело такое вот решение.
Сначала правим initramfs (Debian: /usr/share/initramfs-tools/):
- Чтобы вместо перемещения /dev, /dev/pts, /proc в новый корень (/root) он их туда биндил (mount --bind /dev /root/dev и т.п.).
- Чтобы вместо run-init/switch_root, удаляющего содержимое старого корня, он просто делал
mount --move /root /
chroot /.. /sbin/init < dev/console > dev/console - Также сразу включаем в initramfs dropbear, и делаем, чтобы он НЕ выключался при переходе в новый корень. На Debian’е это делается просто, apt-get install dropbear, потом добавляем DROPBEAR=y в /etc/initramfs-tools/initramfs.conf, и потом в /usr/share/initramfs-tools/scripts/init-bottom/dropbear убираем строчку с kill. Заодно меняем ему порт с 22 на, скажем, 1022 — в scripts/init-premount/dropbear добавляем ему опцию «/sbin/dropbear -p 1022». Ключи при установке dropbear сгенерятся сами и подложатся в /etc/initramfs-tools/root/.ssh — их оттуда надо утянуть, чтобы потом иметь возможность зайти в систему.
После чего можно сделать update-initramfs -u -k `uname -r` и загрузиться в новую систему (старый initramfs на всякий случай лучше скопировать и положить рядом — вдруг чего-то не то натворили).
Ну а чтобы вытащить наружу init, я написал пару утилиток — unchroot-init и init-stub. Первая вышеописанным путём сбегает из chroot’а и делает там exec /sbin/init-stub, вторая — тупо висит и игнорирует все сигналы, кроме SIGCHLD и SIGUSR1 — по первому добивает зомби-процессы, по второму — делает «chroot /..» (переход в новый смонтированный поверх старого корень) и запускает там exec’ом /sbin/init --init — это чтобы вернуться в нормальный режим работы. Наверное, ещё можно было туда прикрутить перезапуск того же dropbear’а, если он вдруг упадёт, но фиг с ним.
Итого, чтобы вытащить «наружу» init:
- ssh’имся в запущенный из rootfs dropbear и делаем там:
mount --move /.. /root
cp /root/<путь>/init-stub /sbin/init-stub - потом в нормальной системе говорим:
cp /sbin/init /sbin/init1
mount --bind /<путь>/unchroot-init /sbin/init
/sbin/init1 U
А дальше уже дело техники — находясь в новой системе или в chroot /root, останавливаем запущенные процессы — что можем, через /etc/rc0.d, что не можем — просто убиваем. Главное — не отключить сеть и не сделать случайно reboot :).
Дальше есть ещё один маленький нюанс — запущенный udev держит «базу данных» текущих девайсов в /run/udev (по крайней мере в дебиане это так) — её нужно утащить (тупо mv) в rootfs — если быть точным, то в /dev/.udev, чтобы она потом не потерялась при отмонтировании tmpfs от /root/run, и чтобы при выходе из «аварийного режима» udev спокойно перезапустился, забрал базу из /dev/.udev и опять-таки пересунул её в /run/udev.
И после всех этих манипуляций — всё, файловые системы, в том числе корень /root, можно отмонтировать. Единственный прикол — у меня почему-то получалось так, что dropbear использовал /root/dev/null, и /root/dev отмонтироваться не хотел. ХЗ, как так получилось — я до конца не понял. Но это не так страшно, /root/dev можно сделать umount -l, всё равно это devtmpfs, а не реальная ФС на диске.
Соответственно, чтобы потом вернуться в нормальный режим работы, нужно:
- Подмонтировать /root.
- Забиндить/смонтировать в него /dev, /dev/pts и /sys.
- Сделать mount --move /root /.
- И сказать kill -USR1 1 — chroot в новый корень заглушка сделает уже сама. С этого момента всё будет выглядеть, как нормальная загрузка системы.
Ну а если нет цели разместить аварийную систему именно в initramfs, то приседания с chroot и /.. вообще не нужны, и можно обойтись pivot_root’ом (хотя init перезапускать всё равно понадобится). В таком виде вообще, наверное, можно написать скриптик, который будет систему «выводить» в tmpfs. Но мне хотелось, чтобы это был именно rootfs, поэтому способ такой, как описано выше…
[ Хронологический вид ]Комментарии
Войдите, чтобы комментировать.