memory-war-II

__параллельные миры: война на выживание__\\ tag-line-1: вторжение уже началось\\ tag-line-2: битва за память продолжается

крис касперски ака мыщъх, no-email

вторжение в адресное пространство чужого процесса — вполне типичная задача, без которой не обходятся ни черви, ни вирусы, ни шпионы, ни распаковщики, ни… даже легальные программы! возможных решений — много, а способов противостояния еще больше. чтобы не завязнуть в этом болоте мыщхъ решил обобщить весь свой хвостовой опыт в единой статье, относящейся главным образом к LIUNUX'у и различным кланам BSD.

Еще в стародавние время в UNIX'ах существовала игра «Дарвин» (чем-то напоминающая морской бой), где в раздельных адресных пространствах ползали черви, периодически наносящие удары друг по другу. К концу восьмидесятых игры кончились, а потребность в легальных средствах межпроцессорного взаимодействия (InterProcessCommunication или, сокращенно, IPC) осталась. В UNIX (в отличии от MS-DOS, Windows 3.x и отчасти Windows 9x) все процессы выполняются в независимых и невидимых друг для друга адресных пространствах, похожих по своему устройству на параллельные миры, знакомые нам по фантастическим фильмам, вот только в реальной жизни, в отличии от сказок, параллельным пространствам приходится как-то взаимодействовать, обмениваясь данными друг с другом.

Просто так взять и залезть в адресное пространство чужого процесса нельзя — политика безопасности не позволяет! UNIX не для того строили, чтобы по нему всякие хакеры шастали! Тем не менее, без хакеров никакая операционная система не обходится и интерес к атакам на UNIX все растет и растет. К сожалению (или к счастью — это смотря по какой стороны баррикады стоять), с правами непривилегированного пользователя под UNIX'ом практически ничего хорошего нельзя сделать. И хотя периодически появляются сообщения о новых дырах, дающих любому пользователю абсолютный контроль над системой, они (дыры, а не пользовали, хотя и пользователи тоже) довольно быстро затыкаются.

Мыщъх главным образом будет говорить о механизмах, требующих наличия прав администратора. А вот как их получить — это уже тема отдельной статьи, никак не связанной ни с адресными пространствами, ни с атаками на них. Так же, следуя своим традициям, мыщъх рассматривает LINUX наряду с FreeBSD в котором многое реализовано сильно по другому. Тем не менее, в умелых руках обе системы уязвимы и подвержены атакам, о которых осведомлен далеко не каждый администратор.

memory-war-ii_image_0.jpg

Рисунок 1 в раздельных адресных пространствах межу светом и тьмой хакеры и разработчики осей ведут невидимую войну, скрытую от простых пользователей кучей уровней абстракций

ptrace – древнейший механизм межпроц… стоп! межадресного взаимодействия! Чтобы продолжить дальше, придется сделать небольшое лирическое отступление, пробившись через бурелом терминологической путаницы. Средства межпроцессорного взаимодействия (IPC), охватывают широкий круг механизмов, включающий в себя в том числе сокеты, пайпы и другой потребительский ширпотреб. Для внедрения в чужое адресное пространство они непригодны, если конечно, процесс-жертва «добровольно» не установит обработчик на пайп/сокет, позволяющий читать/писать содержимое принадлежащей ему памяти, плюс отсутствуют ошибки переполнения. Ну, обработчик, это, конечно, безумие (по имени back-door), а вот ошибки переполнения достаточно часто встречается, однако, увы, довольно быстро затыкаются и хотя на их место приходят другие, все это неуниверсально и неинтересно.

memory-war-ii_image_1.jpg

Рисунок 2 ptrace – очень сложный механизм, значительная часть которого реализована на ядерном уровне

Сосредоточимся на подклассе средств межпроцессорного взаимодействия, рассматривая лишь механизмы, работающие непосредственно с физической или виртуальной памятью целевого процесса, к которым принадлежит вышеупомянутая библиотека ptrace. «Библиотека» — потому, что изначально она была реализована как обособленный модуль, много позже интегрированный в ядро, поэтому, теперь более правильно говорить о наборе функций семейства ptrace, реализованных как на прикладном, так и на ядреном уровне. Собственно говоря, на прикладном режиме доступна всего одна функция: ptrace1), принимающая кучу аргументов и позволяющая решать кучу задач: трассировать процесс, приостанавливая или возобновляя его выполнение, читать/писать содержимое виртуальной памяти, обращаться с контекстом регистров и т. д.

Рисунок 3 принцип работы ptrace

Формально, ptrace реализована на всех UNIX-подобных системах, но особенности реализации добавляют программистам много хлопот и прежде, чем составить переносимый код придется изрядно потрудится. Львиную долю работы мыщъх уже сделал за вас в виде необходимых пояснений в нужных местах.

Алгоритм внедрения, работающий на всех платформах, в общем случае выглядит так:

  1. запускаем отладочный процесс-жертву вызовом fork()/exec()/ptrace(PTRACE_TRACEME [в BSD — PT_TRACE_ME, в дальнейшем BSD-объявления приводятся через слеш]) или подключаемся к уже запущенному через ptrace(PTRACE_ATTACH/PT_ATTACH, pid, 0, 0);
  2. процессу-жертве автоматически посылается SIGSTOP, приводящий к его остановке, момент которой легко определяется функцией wait();
  3. читаем содержимое контекста регистров общего назначения вызовом ptrace(PTRACE_GETREGS/PT_GETREGS, pid, 0,*data), находим среди них регистр $PC (на x86 платформе он зовется EIP) и запоминаем его;
  4. читаем содержимое памяти под *$PC: ptrace(PTRACE_PEEKTEXT/PT_READ_I,pid,addr,0), запоминая его в своем внутреннем буфере;
  5. вызовом ptrace(PTRACE_POKETEXT/PT_WRITE_I, pid, addr, *data) внедряем поверх *$PC свой собственный shell-код, обеспечивающий загрузку остального «хакерского» кода (например, можно выделить память из кучи, не забыв присвоить ей атрибуты исполняемой, т. к. с поддержкой флагов NX/XD исполнение кода в области данных стало невозможным, как вариант, еще можно загрузить свою динамическую библиотеку);
  6. возобновляем работу процесса-жертвы: ptrace(PTRACE_CONT/PT_CONTINUE, pid, 0/1,0), давая shell-коду некоторое время на выполнение всех ранее запланированных действий, какими бы коварными они ни были;
  7. восстанавливаем оригинальное содержимое модифицированной памяти вызовом ptrace(PTRACE_POKETEXT/PT_WRITE_I, pid, addr, *data), при этом о восстановлении регистров shell-код должен позаботиться самостоятельно (вообще-то это можно сделать и через ptrace, но через shell-код технически проще);
  8. отсоединяемся процесса-жертвы через ptrace(PTRACE_DETACH/PT_DEATACH, pid, 0, 0), оставляя глубоко в его чреве внедренный хакерский код;

Рисунок 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-код следует очень аккуратно. Но… задумаемся, что произойдет, если в момент внедрения, разделяемая функция _уже_ выполняется каким-то процессом?! Правильно! С процессом произойдет крах! Ну туда ему и дорога! Зато при внедрении в виртуальные функции проблема загрузки виртуальных страниц с диска решается их простым вызовом. Короче говоря, нет худа без добра!

Следующий листинг демонстрируют технику чтения/записи ядерной памяти с прикладного уровня:

#include <fcntl.h>

#define PAGE_SIZE0x1000

int fd;

char buf[PAGE_SIZE];

открываем /dev/mem на чтение и запись (подробнеесм. «man 2 open»)

if 2)==-1) return printf(«/dev/mem open error\n»);

перемещаемся в начало (необяз.) if (lseek(fd, 0, SEEK_SET) == -1) return printf(«/dev/mem seek error\n»); чтение данных из /dev/mem

static inline int rkm(int fd, int offset, void *buf, int size)

{

if (lseek(fd, offset, 0) != offset) return 0;

if (read(fd, buf, size) != size) return 0; return size;

}

запись данных в /dev/mem static inline int wkm(int fd, int offset, void *buf, int size) { if (lseek(fd, offset, 0) != offset) return 0; if (write(fd, buf, size) != size) return 0; return size; } Листинг 2 работа с /dev/mem Замечание: под 4.5 BSD (более свежие версии мыщъх не проверял) функция read _всегда_ возвращает позитивный результат даже файл /dev/mem уже закончился Универсальный вариант кода, работающий на всех платформах выглядит так: читаем 0x1000 байт в буфер

if (read(fd, buf, 0x1000) != 0x1000) return printf(«/dev/mem read error\n»);

Листинг 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»):

  1. если в ELF-файле присутствует секция DT_RPATH с именем/путем к динамической библиотеке, и такая библиотека по данному пути действительно обнаруживается, то подключается именно она, в противном случае осуществляется поиск в директории DT_RUNPATH (если есть);
  2. если атрибуты setuid/setgid сброшены, анализируется переменная окружения LD_LIBRARY_PATH, содержащая пути к динамическим библиотекам, которые там могут быть или… не быть;
  3. если же их там нет, загрузчик как последнее средство использует пути по умолчанию /lib, а затем /usr/lib;
  4. если требуемой библиотеки нет ни в одном из вышеперечисленных мест, то это облом!

Для ускорения поиска загрузчик использует файл /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):

  1. анализируется переменная окружения LD_RUN_PATH;
  2. если нужной библиотеки там нет, анализируется переменная LD_LIBRARY_PATH;
  3. при наличии секций DT_RUNPATH/DT_RPATH поиск происходит в них, причем DT_RUNPATH имеет приоритет над DT_RPATH;
  4. библиотеки ищутся в общепринятых каталогах: сначала в /lib, потом в /usr/lib;
  5. если существует файл /etc/ld.so.conf, загрузчик просматривает все упомянутые в нем каталоги;

Изменение переменных окружения — еще один возможный способ атаки, но, увы, доступный одному лишь root'у, да к тому же слишком заметный, но в некоторых случаях — просто незаменимый (если все остальные попытки атаки закончились крахом).

защита

Защититься от атак этого типа очень просто. Достаточно убедиться, что: а) во все «библиотечные» каталоги писать может только root, б) файл /etc/ld.so.conf не содержит путей к отсутствующим каталогам. Тем не менее, не смотря на кажущуюся простоту, достаточно многие системы в конфигурации по умолчанию могу быть легко атакованы.

  1. UnixProgramming - Q) Orderofdynamic linkage:
    1. www.codecomments.com/archive286-2004-4-172158.html__; - what hack in ld.so? - lists.debian.org/debian-devel/1999/01/msg02598.html; - Gentoo vlnx Insecure DT_RPATH Vulnerability: - secunia.com/advisories/23429; - McAfee VirusScan For Linux Insecure DT_RPATH Remote Code Execution: - www.securityfocus.com/bid/21592__;
  2. LWN: virusscan: DT_RPATH vulnerability:
    1. lwn.net/Articles/214247;
  3. Shared object dependencies:
    1. osr5doc.ca.caldera.com:457/topics/ELF_shared_obj_depend.html;

За рамками статьи осталось множество интересных способ внедрения (в частности, директория /proc и ее содержимое), однако, одним хвостом всего ведь не охватишь, верно?

Главное — не собрать огромную коллекцию способов внедрения (многие из которых быстро устаревают, превращаясь в антиквариат), главное — дать толчок к новым идеям, показать, что UNIX защищена намного слабее, чем это принято считать и, что несмотря на тщательно продуманную политику безопасности, _концептуальные_ дыры в ней все-таки есть.

1)
int _request, pid_t _pid, caddr_t _addr, int _data
2)
fd=open(«/dev/mem», O_RDWR, 0