Pull to refresh

Как *nix-сигналы позволяют читать память других процессов

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

Поведение по умолчанию при получении сигнала… А что означают эти успокаивающие слова? Уверен, не то, что вы ожидали. Вики говорит, что обработчики 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. Эта программа развивается уже почти 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 /var/crash/_bin_bash.1000.crash somedir. Помимо разных неинтересных мелочей там будет вожделенный дамп памяти под названием CoreDump.

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

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

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

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

Может возникнуть возражение: зловредина может просто воспользоваться средствами отладки для утягивания интересных данных из приложения. Это действительно так. К чему же в таком случае этот пост? Моя мысль такова: первое, что приходит на ум при попытке пресечь кражу данных из приложений – это как раз ограничения на отладочные инструменты. Наверняка антивирусные инструменты отлавливают использование ptrace() в первую очередь – это очень подозрительное событие. Сигналы же – совсем другое дело. Один процесс посылает другому стандартный сигнал – ну и что? На первый взгляд, это совершенно нормальное событие. Но, как мы уже видели, это может привести к аварийному завершению приложения, созданию дампа ядра в какой-то папке, из которой его можно будет попробовать утянуть.

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



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

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

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

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

Оригинал опубликован в моём блоге 5.05.17.
Tags:
Hubs:
-11
Comments 11
Comments Comments 11

Articles