Автор: (c)Крис Касперски ака мыщъх
Вторжение в адресное пространство чужого процесса - вполне типичная задача, без которой не обходятся ни черви, ни вирусы, ни шпионы, ни распаковщики, ни... даже легальные программы! Возможных решений - много, а способов противостояния - еще больше. Чтобы не завязнуть в этом болоте, мыщхъ решил обобщить весь свой хвостовой опыт в единой статье, относящейся главным образом к Linux'у и различным клонам BSD.
Еще в стародавние время в UNIX'ах существовала игра "Дарвин" (чем-то напоминающая морской бой), где в раздельных адресных пространствах ползали черви, периодически наносящие удары друг по другу. К концу восьмидесятых игры кончились, а потребность в легальных средствах межпроцессорного взаимодействия (Inter Process Communication или, сокращенно, IPC) осталась. В UNIX (в отличии от MS-DOS, Windows 3.x и отчасти Windows 9x) все процессы выполняются в независимых и невидимых друг для друга адресных пространствах, похожих по своему устройству на параллельные миры, знакомые нам по фантастическим фильмам, вот только в реальной жизни, в отличии от сказок, параллельным пространствам приходится как-то взаимодействовать, обмениваясь данными друг с другом.
Просто так взять и залезть в адресное пространство чужого процесса нельзя - политика безопасности не позволяет! UNIX не для того строили, чтобы по нему всякие хакеры шастали! Тем не менее, без хакеров никакая операционная система не обходится и интерес к атакам на UNIX все растет и растет. К сожалению (или к счастью - это смотря с какой стороны баррикады стоять), с правами непривилегированного пользователя под UNIX'ом практически ничего хорошего нельзя сделать. И хотя периодически появляются сообщения о новых дырах, дающих любому пользователю абсолютный контроль над системой, они (дыры, а не пользовали, хотя и пользователи тоже) довольно быстро затыкаются.
Мыщъх главным образом будет говорить о механизмах, требующих наличия прав администратора. А вот как их получить - это уже тема отдельной статьи, никак не связанной ни с адресными пространствами, ни с атаками на них. Также, следуя своим традициям, мыщъх рассматривает Linux наряду с FreeBSD, в котором многое реализовано сильно по-другому. Тем не менее, в умелых руках обе системы уязвимы и подвержены атакам, о которых осведомлен далеко не каждый администратор.
Рисунок 1. В раздельных адресных пространствах между светом и тьмой хакеры и разработчики осей ведут невидимую войну, скрытую от простых пользователей кучей уровней абстракций.
ptrace - древнейший механизм межпроц... стоп! межадресного взаимодействия! Чтобы продолжить дальше, придется сделать небольшое лирическое отступление, пробившись через бурелом терминологической путаницы. Средства межпроцессорного взаимодействия (IPC), охватывают широкий круг механизмов, включающий в себя, в том числе, сокеты, пайпы и другой потребительский ширпотреб. Для внедрения в чужое адресное пространство они непригодны, если конечно, процесс-жертва "добровольно" не установит обработчик на пайп/сокет, позволяющий читать/писать содержимое принадлежащей ему памяти, плюс отсутствуют ошибки переполнения. Ну, обработчик - это, конечно, безумие (по имени back-door), а вот ошибки переполнения достаточно часто встречаются, однако увы, довольно быстро затыкаются и хотя на их место приходят другие, все это неуниверсально и неинтересно.
Рисунок 2. ptrace - очень сложный механизм, значительная часть которого реализована на ядерном уровне.
Сосредоточимся на подклассе средств межпроцессорного взаимодействия, рассматривая лишь механизмы, работающие непосредственно с физической или виртуальной памятью целевого процесса, к которым принадлежит вышеупомянутая библиотека ptrace. "Библиотека" - потому, что изначально она была реализована как обособленный модуль, много позже интегрированный в ядро, поэтому теперь более правильно говорить о наборе функций семейства ptrace, реализованных как на прикладном, так и на ядерном уровне. Собственно говоря, на прикладном уровне доступна всего одна функция: ptrace(int _request, pid_t _pid, caddr_t _addr, int _data), принимающая кучу аргументов и позволяющая решать кучу задач: трассировать процесс, приостанавливая или возобновляя его выполнение, читать/писать содержимое виртуальной памяти, общаться с контекстом регистров и т.д.
Рисунок 3. Принцип работы ptrace.
Формально, ptrace реализована на всех UNIX-подобных системах, но особенности реализации добавляют программистам много хлопот и прежде, чем составить переносимый код придется изрядно потрудится. Львиную долю работы мыщъх уже сделал за вас в виде необходимых пояснений в нужных местах.
Алгоритм внедрения, работающий на всех платформах, в общем случае выглядит так:
Рисунок 4. Раскуривание man'а по ptrace.
Защититься от такого метода внедрения процессу-жертве проще простого. Поскольку функция ptrace нереентерабельна, то есть не допускает вложенного выполнения, процессу-жертве достаточно сделать ptrace()... самому себе! Это никак не повлияет на производительность, но вот вторжение предотвратит. Впрочем, вместе с вторжением отвалится и отладка. За исключением небольшого количества отладчиков (таких, например, как Linice), весь остальной конгломерат (включающий и могущественный gdb) работает именно через ptrace и попытка отладки защищенного процесса накрывается медным Тазом.
Процесс-жертва может легко очиститься от хакерского кода и вовсе не через сжигание на костре, а... повторным вызовом exec() самому себе. Системный загрузчик перечитает исходный образ elf-файла с диска и все изменения в кодовом сегменте будут потеряны. Правда, вместе с ними будут потеряны и оперативные данные, которые в этом случае процессу придется хранить в разделяемой области памяти. Это существенно затруднит программирование, однако затраченные усилия стоят того, поскольку атаки через ptrace (в силу их известности и простоты реализации) - самые популярные из всех на сегодняшний день и в обозримом будущем их активность снижаться не собирается.
Практически на всех UNIX'ах имеется файл /dev/mem, представляющий собой образ физической оперативной памяти компьютера (не путать с виртуальной!). Там же, где этого файла нет (например, удален по соображениям безопасности), его нетрудно создать и вручную своими лапами и хвостом:
mknod -m 660 /dev/mem c 1 1 chown root:kmem /dev/mem
Листинг 1. Создание файла-устройства /dev/mem, предоставляющего доступ к физической памяти компьютера с прикладного уровня.
Здесь: 660 - права доступа, /dev/mem - имя файла (может быть любым, например /home/kpnc/nezumi), "c" - тип устройства (символьное устройство), "1 1" - устройство (физическая память). Файл /dev/mem (или как вы там его назовете) свободно доступен с прикладного уровня, но только для root, что не есть хорошо, но... лучше это, чем вообще ничего.
Поскольку в операционных системах со страничной организаций оперативная память используется как кэш, одни и те же физические страницы в различное время могут соответствовать различным виртуальным адресам, поэтому код процесса-жертвы (вместе с подходящим местом для внедрения) приходится искать по заранее выделенной сигнатуре.
Рисунок 5. Файл /dev/mem в шестнадцатеричном редакторе.
При этом нас подстерегают следующие проблемы. Первая (и самая главная): при недостатке физической оперативной памяти наименее нужные страницы виртуального адресного пространства выгружаются на диск и в их число могут попасть и страницы, принадлежащие нашему процессу-жертве, причем не все сразу, а так... частями. Как решето или как дуршлаг. Следовательно, а) внедряться нужно в часто используемые участки кода, вероятность вытеснения которых минимальна; б) если с сигнатурным поиском в /dev/mem произошел облом, не паникуйте, а просто подождите некоторое время и повторите операцию сканирования вновь, рано или поздно виртуальные страницы считаются операционной системой в память (если, конечно, процесс не будет скоропостижно прибит злым пользователем).
Вторая проблема настолько несерьезна, что и упоминать ее не стоит, но... все-таки! Соседние виртуальные страницы адресного пространства зачастую оказываются в различных частях файла /dev/mem, поэтому: а) размер внедряемого shell-кода не может превышать размеров одной страницы - а это 1000h байт на x86; б) базовые адреса виртуальных страниц при вытеснении на диск всегда кратных их размеру, т.е. мы можем внедрить 200h байт shell-кода, начиная с адреса XXXX1000h, но не можем сделать это же самое с XXXX1EEEh.
Остается только определиться с местом внедрения. А внедряться предпочтительнее всего в начало часто вызываемых функций. Если это будут "внутренние" функции процесса-жертвы, то наш хакерский код окажется привязан к конкретному версии исполняемого файла. После выхода новой версии или даже компиляции старой версии другим компилятором (или с иными ключами), все смещения неизбежно изменятся и...
Гораздо перспективнее внедряться в библиотечные функции. Такие, например, как printf(), расположенные в разделяемой области памяти и позволяющие определить свой адрес штатными средствами операционной системы без всякого дизассемблера. Естественно, внедрение в разделяемую функцию затронет все процессы, ее использующие, и потому писать shell-код следует очень аккуратно. Но... задумаемся, что произойдет, если в момент внедрения, разделяемая функция уже выполняется каким-то процессом?! Правильно! С процессом произойдет крах! Ну, туда ему и дорога! Зато при внедрении в виртуальные функции проблема загрузки виртуальных страниц с диска решается их простым вызовом. Короче говоря, нет худа без добра!
Следующий листинг демонстрируют технику чтения/записи ядерной памяти с прикладного уровня:
Листинг 2. Работа с /dev/mem.
Замечание: под 4.5 BSD (более свежие версии мыщъх не проверял) функция read всегда возвращает позитивный результат, даже если файл /dev/mem уже закончился.
Универсальный вариант кода, работающий на всех платформах, выглядит так:
Листинг 3. Универсальный вариант чтения /dev/mem, работающий и под Linux'ом и под BSD.
На прикладном уровне у процесса-жертвы никаких защитных средств в оборонительном арсенале, в общем-то, и нет ("в общем-то", потому что процесс может использовать динамическую шифровку кода, контроль целостности библиотечных функций перед их вызовом и т.д., но это уже явный перебор). На уровне ядра создание файла /dev/mem блокируется элементарно, но... вместе с этим блокируются и многие полезные программы (в частности X'ы), так что остается только разграничение доступа к /dev/mem с ведением списка "доверительных" лиц, которые к нему могут обращаться, что отчасти реализовано в OpenBSD.
Тем не менее, надежной защиты от внедрения через /dev/mem нет и не будет! Успокаивает лишь тот факт, что доступ к нему имеет лишь root, а root может делать все, что угодно, как и положено богу, ну а богов не судят.
Практически все приложения (за исключением небольшого круга системных утилит) используют динамически загружаемые библиотеки, которые также могут быть использованы для внедрения в чужое адресное пространство. Самое простое - взять готовую библиотеку и подменить ее своей, но это слишком заметно, да и как-то по-пионерски. Короче, такое решение не катит. Поэтому, запасаемся свежим пивом и курим man (в Linux'e - "man ld.so", во FreeBSD - "man ld").
Рисунок 6. Раскуривание man'а по ld.so.
Оттуда мы узнаем, что порядок поиска динамических библиотек - очень интересная штука и в Linux'е системный загрузчик, сосредоточенный в файлах "ld.so" и "ld-linux.so*", в общем случае поступает так (а в не общем - как ему скажет утилита ldconfig, см "man ldconfig"):
Для ускорения поиска загрузчик использует файл /etc/ld.so.cache, содержащий таблицу хинтов (от англ. hint - подсказка, наводка), или, попросту говоря, перечь путей к ранее найденным библиотекам. Это не текстовый формат, да к тому же доступный для модификации одному лишь root'у, так что не будем на нем подробно останавливаться, а лучше посмотрим на файл /etc/ld.so.config, который задает порядок поиска динамических библиотек и в свежеустановленном KNOPPIX'е выглядит так:
/lib /usr/lib /usr/X11R6/lib /usr/i486-linuxlibc1/lib /usr/local/lib /usr/lib/mozilla
Листинг 4. Файл /etc/ld.so.config из свежеустановленного KNOPPIX'а.
Разумеется, модифицировать файл /etc/ld.so.config может только root, зато читать может любой желающий, а для успешной атаки большего и не надо!!! В частности, чтобы "поиметь" Mozill'y, достаточно поместить библиотеку "спутник" (термин пришел из MS-DOS) в одну из вышележащих директорий и тогда она будет загружена первой!!! Спутнику остается только "похозяйничать" внутри чужого адресного пространства - подвигать стулья, сожрать кашу, поспать на постели и всю ее выспать, после чего благополучно ретироваться, загрузив оригинальную библиотеку и передав ей управление.
Рисунок 7. Нетекстовый формат файла /etc/ld.so.cache.
Вот только на этом пути нас ждут две большие проблемы. Первое - создать новые файлы в каталогах /lib, /user/lib,... может только root, а его еще как-то заполучить надо, однако... анализ показывает, что файл /etc/ld.so.config зачастую содержит пути к несуществующим каталогам (в данном случае это /usr/i486-linuxlibc1/lib), которые может создавать кто угодно и класть в них, что угодно!!! Вот только... прежде чем открывать на радостях свежее пиво, следует решить вторую проблему. А именно - скорректировать или очистить кэш в лице файла /etc/ld.so.cache, к которому, опять-таки, имеет доступ только root, однако кэш на то и кэш, чтобы хранить не все, а лишь последние найденные библиотеки. Что мы делаем: грузим все библиотеки, которые только установлены в системе (за исключением "нашей"), в результате чего, "нашей" библиотеке в /etc/ld.so.cache очень скоро уже не окажется и она будет взята не из /usr/lib/mozilla, а из /usr/i486-linuxlibc1/lib!!!
Но что делать, если в /etc/ld.so.config отсутствуют несуществующие пути?! Тогда - добывать root'а любой ценой и класть "свою" библиотеку в /lib или /usr/lib. Во всяком случае, это намного менее заметно, чем прямая модификация атакуемой библиотеки на диске (то есть ее "заражение").
Все сказанное выше относится главным образом к Linux'у. У BSD-систем порядок поиска динамических библиотек слегка иной, хотя суть остается той же (вот неплохая статья на эту тему, найденная в сети: http://www.codecomments.com/archive286-2004-4-172158.html):
Изменение переменных окружения - еще один возможный способ атаки, но, увы, доступный одному лишь root'у, да к тому же слишком заметный, но в некоторых случаях - просто незаменимый (если все остальные попытки атаки закончились крахом).
Защититься от атак этого типа очень просто. Достаточно убедиться, что: а) во все "библиотечные" каталоги писать может только root, б) файл /etc/ld.so.conf не содержит путей к отсутствующим каталогам. Тем не менее, несмотря на кажущуюся простоту, достаточно многие системы в конфигурации по умолчанию могу быть легко атакованы.
За рамками статьи осталось множество интересных способов внедрения (в частности, директория /proc и ее содержимое), однако одним хвостом всего ведь не охватишь, верно?
Главное - не собрать огромную коллекцию способов внедрения (многие из которых быстро устаревают, превращаясь в антиквариат), главное - дать толчок к новым идеям, показать, что UNIX защищена намного слабее, чем это принято считать и что, несмотря на тщательно продуманную политику безопасности, концептуальные дыры в ней все-таки есть.