Прогулка с Викой

Жизнь — вообще штука интересная, а уж жизнь на *nix-платформах — тем более. Позвольте мне подтвердить это утверждение триллионным примером!

Есть такая очень старая и вросшая в *nix с корнями штука под названием «сигналы». Идея этих примитивов очень проста: реализовать программный аналог прерываний. Различные процессы могут посылать сигналы друг другу и самим себе, зная process id (pid) получателя. Процесс-получатель волен либо назначить функцию-обработчик сигнала, которая будет автоматически вызываться при его получении, либо игнорировать его с помощью специальной маски, либо же довериться поведению по умолчанию. So far so good.

Я написал простую программу с таким обработчиком сигнала SIGUSR1: напечатать на экран строку «1», войти в бесконечный цикл. Зачем? Я надеялся, что если много раз посылать этой программе сигнал SIGUSR1, то обработчик будет вызываться многократно, что вызовет переполнение стека. К моему сожалению, обработчик вызывался лишь один раз. Окей, напишем аналогичный обработчик сигнала SIGUSR2 и будем посылать два разных сигнала в надежде, что это свалит жертву… Увы, но и это не помогло: каждый из обработчиков был вызван лишь однажды. Переполняли-переполняли, да не выпереполняли!

Поведение по умолчанию при получении сигнала… А что означают эти успокаивающие слова? Уверен, не то, что вы ожидали. Вики говорит, что обработчики 28 стандартных сигналов (существуют и другие!) по умолчанию таковы: 2 игнорируются, 4 вызывают остановку процесса, 1 — его продолжение, 11 — его завершение, 10 — его завершение с созданием дампа памяти. Вот это уже интересно! Итак, дело обстоит следующим образом: даже если ваша программа никак не упоминает сигналы в исходном коде, на самом деле она их использует, причём таким вот драматичным образом.

С этого момента нам придётся копнуть поглубже. Кто и кому может посылать сигналы? Вики говорит: «Процесс (или пользователь из оболочки) с эффективным UID, не равным 0 (UID суперпользователя), может посылать сигналы только процессам с тем же UID.» Итак, если вы запускаете 100 программ, то любая из них может запросто убить все эти 100 программ с помощью системного API, даже если все программы (кроме убийцы) никак не упоминали сигналы в своём исходном коде. Если же вы работали под учёткой root-а, то вообще не важно, кто запустил те или иные процессы, всё равно их можно запросто завершить. Узнать pid конкретного процесса и выполнить его «заказное убийство», разумеется, можно, но ещё проще убить всех кого можно путём простого перебора pid-ов.

«Погоди-погоди, Иван, не гони лошадей. Ты упоминал, что сигналы можно обрабатывать и игнорировать!» — слышу я голос своего читателя. Что скажет начитанная Вики? «Для альтернативной обработки всех сигналов, за исключением SIGKILL и SIGSTOP, процесс может назначить свой обработчик или игнорировать их возникновение модификацией своей сигнальной маски.» Смотрим на действия по умолчанию при получении этих сигналов и видим: «Завершение процесса», «Остановка процесса». Получается, что эти два действия мы можем сделать всегда, когда посылка сигналов SIGKILL и SIGSTOP жертве в принципе возможна. «Единственное исключение — процесс с pid 1 (init), который имеет право игнорировать или обрабатывать любые сигналы, включая KILL и STOP.» Возможно, мы даже из-под root-а не сможем убить один из главнейших системных процессов, но по-хорошему это требует дополнительного исследования.

Что ж, картина стала чуть позитивнее, но она по-прежнему мрачна. Если вы запускаете процесс, то он гарантированно может завершать и останавливать кучу других процессов. При выполнении очень простого условия «разработчики процесса-получателя забыли проигнорировать или как-то обработать один из многих других сигналов» приложение-маньяк сможет вызывать завершение процесса с созданием дампа памяти или продолжение процесса после остановки. Если же разработчики приложения-получателя повесили свои обработчики на какие-то сигналы, можно попытаться помешать функционированию этого приложения путём посылки ему сигналов. Последнее является темой для отдельного разговора, потому что в силу асинхронности выполнения обработчиков возможны гонки и неопределённое поведение…

«Абстрактные рассуждения — это очень круто, Иван, но давай-ка ближе к конкретике,» — скажет мне требовательный читатель. Окей, нет проблем, ща всё будет!

Любому пользователю *nix хорошо знакома такая программа, как bash. Куда же без этой командной оболочки! Во многих дистрибутивах Linux именно она установлена по умолчанию, а уж при желании её можно запихнуть вообще куда угодно. Эта программа развивается уже почти 30 лет и обладает целой горой возможностей, уж поверьте мне. Завалим-ка её для наглядности и получим из её памяти какую-нибудь вкуснятину!

Я достаю из широких штанин свою домашнюю Ubuntu 16.04.2 и запускаю на ней две копии bash 4.3.46. В одной из них я выполню гипотетическую команду с секретными данными: export password=SECRET. Давайте на время забудем про файл с историей команд, в которую тоже записался бы пароль, пожааалуйста! Наберём в этом же окне команду ps, чтобы узнать pid этого процесса — скажем, 3580.

Не закрывая первое окно, перейдём во второе. Команда ps в нём даст другой pid этого экземпляра bash — скажем, 5378. Чисто для наглядности именно из этого второго bash-а отправим сигнал первому командой kill -SIGFPE 3580. Да, уважаемый читатель, это полный абсурд: процесс 2 говорит никак не связанному с ним процессу 1, что в этом самом процессе 1 произошла ошибочная арифметическая операция. На экране появляется такое вот окошко:

Произошло желанное аварийное завершение с созданием дампа памяти, то есть bash похоже не обрабатывает и не игнорирует этот сигнал. Загуглив, где мне искать дамп, я нашёл развёрнутый ответ (раз, два). В моей Убунте дело обстоит так: если приложение из стандартного пакета падает из-за сигнала, отличного от SIGABRT, то дамп передаются программе Apport. Это как раз наш случай! Данная программа компонует файл с диагностической информацией и выдаёт окошко, показанное выше. Официальный сайт гордо заявляет: «Apport collects potentially sensitive data, such as core dumps, stack traces, and log files. They can contain passwords, credit card numbers, serial numbers, and other private material.» Так-так, интересно, а где там у нас лежит этот файл? Агааа, /var/crash/_bin_bash.1000.crash. Вытащим его содержимое в папку somedir: apport-unpack _bin_bash.1000.crash somedir. Помимо разных неинтересных мелочей там будет вожделенный дамп памяти под названием CoreDump.

Вот он, момент истины! Давайте поищем в этом файле строку password и посмотрим, что интересного мы получим в ответ. Команда strings CoreDump | grep password напомнит забывчивому хакеру, что password есть SECRET. Чудесно!

То же самое я проделал и со своим любимым текстовым редактором gedit, начав набирать текст в буфере, а затем считав его уже из дампа. Никаких проблем! В этот момент Вики предостерегающе шепнула на ухо: «Иногда (например, для программ, выполняемых от имени суперпользователя) дамп памяти не создаётся из соображений безопасности.» Тааак, проверим… При получении сигнала от рутового bash-а второй рутовый bash упал с созданием дампа памяти, но из-за прав доступа (-rw-r— — с владельцем root) прочитать его уже не так просто, как прежние, владельцем которых был мой пользовательский аккаунт. Что ж, коли гипотетической программе-киллеру удалось послать сигнал с UID суперпользователя, то и такой дамп она сможет потрогать.

Дотошный читатель может заметить: «Иван, тебе было очень легко найти нужные данные в море мусора». Чистая правда, но я уверен: если вы знаете, какую рыбу вы хотите поймать и где она плавает, то найти её в сетях дампа должно быть реально почти всегда. Скажем, никто не мешает скачать пакет с отладочной информацией для упавшей программы и узнать содержимое интересующих вас переменных в GDB.

Всё это может выглядеть вполне безобидно, но на самом деле таковым не является. Стал бы я писать пост о чём-то безобидном, м? :) Все описанные мною действия могли быть запросто проделаны программой или скриптом, работающей в пользовательском режиме, не говоря уже о более привилегированном уровне доступа. В сухом остатке получаем, что зловредная исполнимая штука может легко рубить программы направо и налево, а часто ещё и свободно читать всю их память. Вот тебе и сигналы да отчёты об ошибках! Уверен, что на других *nix-платформах и с другими программами-получателями ситуация аналогична, но проверять не буду, ибо мне лень.

Когда я попытался открыть страничку авторизации vk.com и свалить Firefox тем же роковым сигналом, он упал, но вызвал свой обработчик дампов. Дампы в хитром формате minidump сохраняются по адресу ~/.mozilla/firefox/Crash Reports/{pending или submitted} и требуют дополнительного исследования. Вот что вы узнаете, если в окошке настроек кликните на «Learn more» напротив галочки:

«При желании вы можете отправить сообщение об ошибке в корпорацию Mozilla после падения браузера Firefox. Такое сообщение содержит технические данные, которые мы используем для улучшения работы Firefox, в том числе информацию о причине падения, об активном URL-адресе на момент падения, а также о состоянии памяти компьютера на момент падения. Сообщения об ошибках, которые мы получаем, могут содержать персональную информацию. Некоторые части сообщений об ошибках мы публикуем в открытом доступе по адресу https://crash-stats.mozilla.com/. Перед публикацией сообщений об ошибках мы принимаем меры для автоматического удаления персональной информации. Мы не удаляем ничего из написанного вами в полях для комментариев.» В URL-ках редко бывает что-то по-настоящему вкусное, а вот есть ли в дампах пароли или cookie, вопрос хороший!

На этой таинственной и интригующей ноте я вынужден закончить: ко мне в квартиру внезапно пришёл сигнал SIGFPE, который я забыл обработать или проигнорировать.

P. S. В мире Windows есть некое подобие сигналов — сообщения, которые можно отправлять окнам. Весьма вероятно, что их тоже можно использовать for fun and profit!