Pull to refresh

Книга «Kali Linux в действии. Аудит безопасности информационных систем. 2-е издание»

Reading time11 min
Views11K
image Привет, Хаброжители!

В этой книге рассматриваются методы обхода систем безопасности сетевых сервисов и проникновения в открытые информационные системы. Информационная безопасность, как и многое в нашем мире, представляет собой медаль с двумя сторонами. С одной стороны, мы проводим аудит, ищем способы проникновения и даже применяем их на практике, а с другой — работаем над защитой. Тесты на проникновение являются частью нормального жизненного цикла любой ИТ-инфраструктуры, позволяя по-настоящему оценить возможные риски и выявить скрытые проблемы.

Может ли взлом быть законным? Конечно, может! Но только в двух случаях — когда вы взламываете принадлежащие вам ИС или когда вы взламываете сеть организации, с которой у вас заключено письменное соглашение о проведении аудита или тестов на проникновение. Мы надеемся, что вы будете использовать информацию из данной книги только в целях законного взлома ИС. Пожалуйста, помните о неотвратимости наказания — любые незаконные действия влекут за собой административную или уголовную ответственность.

Вы последовательно пройдете все шаги, необходимые для проведения аудита безопасности информационных систем и тестов на проникновение: от общих понятий, рассмотрения стандартов и необходимых действий перед проведением аудита до методов проникновения в информационную систему и закрепления в ней. Каждая глава книги подкреплена реальными примерами и содержит практическую информацию по применению тех или иных методов.

Книга адресована читателям, имеющим опыт работы в сфере информационных технологий и знакомым с работой основных сетевых сервисов как на Linux-, так и на Windows-платформах, а больше всего будет полезна системным администраторам, специалистам по ИТ-безопасности, всем тем, кто желает связать свою карьеру с защитой информации или аудиторской деятельностью.

Во втором, дополненном и переработанном, издании информация была полностью обновлена и соответствует современным реалиям.
Что нового во втором издании
Во втором издании книги, посвященной аудиту безопасности информационных систем, вся информация переработана таким образом, чтобы она соответствовала современным реалиям. Некоторые разделы были расширены, новые добавлены, и удалено все то, что, на взгляд автора, потеряло свою актуальность.

Книга построена так, что читатель последовательно проходит все шаги, необходимые для проведения аудита безопасности информационных систем и тестов на проникновение. Мы начинаем с общих понятий, рассмотрения стандартов и необходимых действий перед проведением аудита, а заканчиваем методами проникновения в информационную систему и закрепления в ней. Каждая глава книги подкреплена реальными примерами и содержит практическую информацию по применению тех или иных методов.

Излагаемый материал рассчитан на читателя, имеющего опыт в сфере информационных технологий и знакомого с работой основных сетевых сервисов как на Linux-, так и на Windows-платформах. «Аудит безопасности информационных систем» будет полезен системным администраторам, специалистам по ИТ-безопасности, всем тем, кто желает связать свою карьеру с защитой информации или аудиторской деятельностью.

Разбиение стека (Smashing the stack)


Чтобы понять основы разбиения стека, нам необходимо разобраться в том, что происходит в случае, когда одна функция вызывает другую функцию.

Для примера возьмем часть программы:

my_func(param1, param2, ..., paramn);

которая использует локальные переменные var1, var2, ..., varm и некоторый набор, необходимый для занесения в param1, param2, ..., paramn.

На рис. 15.2 показано, как будет выглядеть стек перед выполнением программы.

Указатель стека SP содержит адрес верхушки стека (Y), указатель инструкций IP — информацию о следующей инструкции, которая должна выполняться АЛУ (Z). В нашем случае данные указатели подготавливают все необходимое для выполнения функции my_func().

Как уже упоминалось, стек используется для хранения локальных переменных, но на самом деле он содержит более развернутую информацию. В стек помещается весь необходимый для работы функции контекст, включая параметры ее вызова. Вся область памяти, выделенная под выполнение функции, называется стековым фреймом.

Процессору необходимо каким-то образом ориентироваться в стековом фрейме. Одним из самых разумных способов является выделение фиксированного адреса для каждого стекового фрейма. Указатель на этот адрес содержится в указателе фрейма — BP (base pointer или frame pointer).

image

Вернемся к вызову my_func(). Первое, что происходит при вызове, — это передача данных для параметров param1, param2, ..., paramn. Данные будут передаваться в обратном порядке от paramn к param1.

После вызова функции и передачи параметров нам надо будет выполнить саму функцию. Для того чтобы это произошло, надо добавить в стек адрес инструкции (V), которая должна быть выполнена процессором (рис. 15.3).

image

Следующая инструкция, находящаяся по адресу U, является точкой вызова my_func(). Фактически в этой ячейке содержится адрес, взятый из регистра IP и указывающий на первую инструкцию, которая должна быть выполнена.

На этом этапе завершается подготовка к исполнению my_func(). Но это произойдет после того, как будут выполнены следующие требования:
  1. Предыдущее значение указателя фрейма (BP) сохранено и занесено в стек. Это необходимо для того, чтобы впоследствии мы могли вернуться на ту точку, на которой находились до выполнения функции.
  2. В указатель фрейма (BP) скопировано значение указателя стека (SP), которое определяло указатель фрейма.
  3. Место для локальных переменных var1, var2,…, varm зарезервировано путем перемещения указателя стека вниз.
Первые два шага выполняются для любых функций одними и теми же инструментами, машинным кодом или инструкциями. На третьем шаге для разных функций необходимо будет выделить разное количество места под хранение переменных.

Эти три шага, которые являются общими и одинаковыми для вызова любых функций, называются прологом (prolog) функции (рис. 15.4).

image

Регистр IP содержит адрес S, указывающий на первую «реальную» инструкцию функции, которая должна быть выполнена. Регистр B содержит адрес ячейки W’, данный адрес может быть представлен в виде «адрес W минус общая сумма байтов в слове».

Можно заметить, что параметры и локальные переменные функции распложены в памяти симметрично — над и под регистром BP. Как следствие, по отношению к BP адреса параметров будут возрастать, а адреса локальных переменных уменьшаться.

Как вы могли догадаться, если у функции есть пролог, то должен быть и эпилог. Эпилог (epilog) необходим для того, чтобы «прибраться за собой» после того, как функция выполнит свою работу.

В эпилоге тоже три шага:
  1. Значение регистра W’, в котором находится копия адреса BP, присваивается SP. Это позволяет избавиться от локальных переменных.
  2. Сохраненное значение указателя фрейма Х копируется обратно в ВР (выполняется командой «pop», так как SP указывает на адрес стека, который содержит нужное значение). Если мы сравним рис. 15.3 и 15.4, пренебрегая возможностью изменения значений параметров и переменных, то увидим, что стек после выполнения пролога функции выглядит так же, как до его выполнения. Единственная разница в том, что указатель инструкций теперь содержит адрес R.
  3. Адрес возврата V копируется назад в указатель функции операцией «pop», на которую указывает R.
Инструкция по адресу V переместит указатель стека вверх на такое количество адресов, на которое оно было увеличено до вызова функции, во время добавления в буфер параметров param1, param2, ..., paramn. Мы получили такое же состояние буфера, как и до вызова функции (рис. 15.5). Пока все выглядит хорошо, так почему же возникает проблема, а вместе с ней и уязвимость?

Представьте, что одна из вполне обычных и безвредных переменных var1, var2, ..., varm может оказаться какого угодно типа. По определению ячейки буфера расположены в памяти таким образом, что первая ячейка будет иметь наименьший адрес, то есть адреса буфера растут по направлению к наибольшему адресу.

Предположим, что программа не проверяет введенную пользователем строку на количество символов, а она оказалась больше зарезервированного под нее места в памяти. Теперь, если строка не является последним параметром, то сначала излишком символов будут перезаписаны все переменные, находящиеся после нее. А затем оставшаяся часть строки перезапишет значения указателя фрейма и адрес возврата, на который должен будет вернуться указатель после выполнения эпилога функции. Для нас это открывает очень интересные возможности. Предположим, что мы подадим на вход программы строку, сформированную таким образом, чтобы она переписала адрес возврата на тот, в котором уже на ходится сформированный вредоносный код. Данный код может находиться до или после адреса возврата. Если сделать все правильно, то после выполнения эпилога функции компьютер перейдет по поддельному адресу и выполнит вредоносный код (рис. 15.6).

image

В Unix-подобных системах самым популярным видом кода у злоумышленников является шелл-код. Шелл в Unix — это программная оболочка, которая представляет собой интерфейс для командной строки. По умолчанию в большинстве систем используется командный интерпретатор Броуна (Bourne Shell), находящийся в директории /bin/sh. Целью шелл-кода, сформированного злоумышленником, является запуск интерпретатора из директории /bin/sh. Это позволяет атакующему получить доступ к интерфейсу, в котором он сможет выполнять любые команды от имени пользователя, запустившего подвергнувшуюся взлому программу.

Примеры такого шелл-кода можно найти в интернете. Главное преимущество шелл-кода — это размер, обычно он содержит не более 60 байт. Это очень важно, так как в процессе переполнения буфера атакующий защищен от того, что сформированный код выйдет за пределы отведенной программе памяти.

image

Для атак на ПО, работающее под управлением ОС Windows, также применяется шелл-код, но используется он иначе. При удаленном переполнении буфера систем win32 используется классический метод, при котором необходимо заставить машину жертвы скачать файл из сети и выполнить его. Видимо, это происходит из-за того, что именно такой метод атаки описывает большинство руководств, посвященных информационной безопасности. Однако практика показывает, что шелл-код для Windows может работать по такому же принципу, как и для Unix.

Даже если ваш шелл-код предоставит вам после удачной атаки доступ к командной строке на стороне жертвы, выполнению направленной на переполнение буфера атаки всегда будут препятствовать следующие проблемы:
  • необходимость угадать значение, которое нужно поместить в поддельный адрес возврата;
  • необходимость угадать местонахождение адреса возврата в стеке (W в нашем случае);
  • необходимость убедиться в том, что шелл-код не содержит нулей, так как это приведет к немедленной остановке его выполнения.
Первая и вторая проблемы обычно встречаются одновременно. Основным способом их преодоления является:
  • использование NOP-инструкций. NOP-инструкция ничего не делает и ставится перед шелл-кодом;
  • неоднократное повторение предполагаемого начального адреса в сформированном коде, используемом для направленной на переполнение буфера атаки.
image

На рис. 15.7 изображен буфер, в который помещен эксплойт для его переполнения. Используя этот подход, мы увеличиваем вероятность того, что начальный адрес перезапишет адрес возврата. Более того, теперь нам не обязательно перенаправить выполнение именно на начало шелл-кода — достаточно того, что АЛУ перейдет на любую из NOP-инструкций. Когда АЛУ попадет на NOP-инструкции, оно будет пропускать их одну за другой до тех пор, пока не дойдет до шелл-кода и не выполнит его.

Третье условие может быть выполнено заменой нуля на символ с кодом 90h, так как машинная инструкция с кодом 90h — это NOP.

Еще один способ избежать нулевого байта — маскирование части шелл-кода. Можно заменить все 0 на 1, а в шелл-код встроить функцию, которая преобразует их обратно.

Существует и возможность написания шелл-кода для х86-архитектуры с использованием буквенно-цифровых значений. Такой код, если открыть его в текстовом редакторе, будет выглядеть как непонятный набор цифр больших и маленьких букв. Этот подход дает атакующему большое преимущество, так как позволяет избежать обнаружения шелл-кода стандартными средствами защиты.

Перезапись указателя фрейма


Мы уже убедились, что возможность осуществления разбиения стека появляется из-за отсутствия проверки размера находящихся в буфере данных или ее неправильной организации. Поэтому максимальный размер шелл-кода заранее неизвестен.

Другой распространенной ошибкой при программировании на языке С является так называемая off-by-one error, или ошибка на единицу. Чаще всего она происходит в цикле, выполняющем перебор элементов: например, перебор элементов массива начинается с 1, а не с 0.

Может создаться ложное впечатление, что ничего плохого из-за одного байта произойти не может, но это не так. На самом деле в этом случае перед злоумышленником открываются новые возможности манипуляций с буфером.

Представим себе ситуацию, в которой первая локальная переменная во фрейме стека является буфером, уязвимым к атакам по типу «off-by-one error» во время обработки пользовательских данных. В случае, когда между данной переменной и указателем фрейма нет других данных, введенный дополнительный байт может переписать один байт сохраненного указателя фрейма (Х на рис. 15.4).

Хорошо то, что из-за сохраненного адреса возврата (V на рис. 15.4) у злоумышленника не будет возможности выполнить шелл-код, который он мог загрузить в буфер заранее.

Плохая же новость в том, что для достижения данной цели злоумышленнику надо совершить всего лишь пару дополнительных действий. В архитектуре х86 память организована таким образом, что наиболее важным является байт с наименьшим адресом «little endian». Это означает, что из четырех битов, которые составляют слово, первым идет бит, имеющий наименьшую важность или самый низкий адрес. Если провести аналогию с десятичной системой, получится, что числа будут записаны в обратном порядке, например 1234 будет сохранено таким образом, что 4 будет иметь низший адрес в памяти, а 1 — высший.

Теперь предположим, что переполнение происходит в функции bad_func(), которая вызывается функцией good_func(). В ходе атаки злоумышленник сможет изменить низший бит — в нашем примере цифру 4 — сохраненного указателя фрейма функции good_func(). Почему это так важно? Представьте, что порядок битов в памяти будет другим, «big endian», — тогда любое изменение значения указателя фрейма приводило бы к указанию на адрес, который находится вне текущего контекста исполнения программы. Например, 1234 поменялось бы на 3234. Но в нашем случае мы меняем низший байт, например 1234 на 1232. И у злоумышленника есть все шансы поменять адрес указателя фрейма на такой, который приведет к выполнению загруженного шелл-кода.

Как уже было сказано, указатель фрейма используется для доступа к параметрам и локальным переменным функции. Первым результатом изменения указателя фрейма функции good_func() будет ее работа с неправильными данными и, как следствие, возвращение неправильного результата. В лучшем случае после того, как функция попытается обратиться к ячейке памяти, находящейся вне допустимого ранее выделенного интервала, программа будет экстренно завершена. При худшем развитии событий будет достигнут эпилог функции good_func(). На третьем шаге эпилога будет выполнена команда pop, при помощи которой в указатель инструкции скопируется значение, находящееся за указателем фрейма.

Итак, единственное, что надо сделать для выполнения шелл-кода, — это изменить сохраненное значение указателя фрейма функции good_func() на такое, которое бы указывало на адресное пространство в области памяти, находящейся на одно слово ниже. Где и будет находиться начальная часть шелл-кода (рис. 15.8).

image

Атака возврата в библиотеку


Для рассмотрения атак возврата в библиотеку (Return-into-libc) вновь обратимся к рис. 15.2, на котором изображен стек перед вызовом my_func(). Если посмотреть на ситуацию с точки зрения вызываемой функции, то вначале стек будет содержать адрес возврата, который она должна использовать, а затем — параметры. Как упоминалось в разделе «Разбиение стека», основной целью атакующего является запуск шелл-кода для получения доступа к системе. Обычно подобные шелл-коды в Unix-подобных системах используют такие функции, как system() или execve(), а в системах под управлением ОС Windows — WinExec(). Данные функции в качестве параметра вызова используют имя программы и (или) путь к ней.

Как альтернативу разбиению стека используют прямой вызов определенной функции. Данный способ имеет одно важное преимущество — его нельзя предотвратить, используя защищенный от исполнения стек.

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

image

На рис. 15.9 показано состояние буфера и регистров после проведения атаки возврата в библиотеку после второго шага эпилога. Нормальное состояние стека на аналогичном шаге показано на рис. 15.5. Начальный адрес целевой функции libfunc() — Q — будет занесен в указатель инструкции на третьем шаге эпилога оператором РОР по адресу R. Если данную ситуацию сравнить с изображенной на рис. 15.5, можно заметить, что параметры для вызова libfunc() сдвинуты вверх на одно слово, — это необходимо для установки «заглушки». Интересно, что поддельное значение указателя фрейма Р будет снова занесено в стек после выполнения пролога функции libfunc(). В зависимости от ОС, архитектуры и целевой функции процесс нахождения адреса нужной функции может быть затруднен, но это, безусловно, вполне выполнимая задача.
Об авторе
Никита Скабцов — родился в 1985 г. в Риге. В 2010 г. защитил магистерскую работу, посвященную вопросам защиты электронной коммуникации. С этого момента и по сей день Никита продолжает работать в сфере информационной безопасности. В сферу его интересов входят вопросы защиты информационных систем, в частности обеспечение безопасной работы веб-приложений, защита производственных систем и обеспечение надежной коммуникации. Особенно важным Никита считает достижение баланса между производительностью, удобством использования и надежностью защиты. В данный момент он работает над вопросами применения инструментов искусственного интеллекта для обеспечения защиты внутренней сети и проведения тестов на проникновение. Никита регулярно обновляет свои знания, проходит дополнительные теоретические и практические курсы по повышению квалификации, обладает международными сертификатами по специальности таких организаций, как LPI, RIPE, CompITA, Cisco, Microsoft и других.

Более подробно с книгой можно ознакомиться на сайте издательства:

» Оглавление
» Отрывок

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — Kali Linux
Tags:
Hubs:
Total votes 10: ↑10 and ↓0+10
Comments2

Articles

Information

Website
piter.com
Registered
Founded
Employees
201–500 employees
Location
Россия