Ахиллесова пята информационных систем
Виктор Сердюк,
ведущий инженер-программист Департамента развития технологий информационной безопасности компании РНТ
vas@rnt.ru
Современные информационные системы (ИС) — один из тех краеугольных камней, на которые опираются бизнес-процессы компаний и предприятий различных форм и назначений. Это обстоятельство заставляет уделять особое внимание вопросам защиты ИС от информационных атак, способных привести к нарушению конфиденциальности, целостности или доступности ресурсов системы.
Любые атаки нарушителей реализуются путем активизации той или иной уязвимости. Последние, присутствуя априори в системе, создают благоприятные условия для успешных атак на ИС. К числу уязвимостей относятся и некорректным образом заданная политика безопасности, и отсутствие определенных средств защиты, и ошибки в используемом ПО, и т. д. (рис. 1). В данной статье мы уделим основное внимание двум видам уязвимостей ПО, которые по значимости можно смело приравнять к известной с античных времен ахиллесовой пяте. Первый вид привносится в ПО на этапе проектирования и разработки, а второй — на этапе эксплуатации ИС.
Рис. 1. Уязвимости, информационные атаки и их последствия.
|
Уязвимости на этапе разработки
Чтобы полнее представить себе сущность таких уязвимостей, методы их выявления и устранения, обратимся к некоторым хорошо известным их типам.
Переполнение буфера
В основе уязвимости типа buffer overflow ("переполнение буфера") лежит возможность переполнения стека атакуемой подпрограммы, что дает нарушителю шанс выполнить любые команды на хосте, где она запущена.
Именно так была организована в 1988 г. первая крупномасштабная атака в Интернете, впоследствии получившая название "Интернет-червь Ч. Морриса". Атака, базирующаяся на уязвимости сетевой службы fingerd, буквально за несколько дней парализовала работу более половины компьютеров, подключенных к Интернету. И по сей день уязвимость buffer overflow считается одной из наиболее распространенных и весьма опасных для общесистемного и прикладного ПО. Для более полного представления об ее особенностях рассмотрим основные принципы организации стека процессора семейства Intel x86 (листинг 1).
Листинг 1. Пример вызова функции test() void test(int a, int b, int c){ char p1[6]; char p2[9]; } void main() { test(1,2,3); } |
Стек представляет собой область памяти, специально выделенной для временного хранения данных подпрограмм. В защищенном режиме работы микропроцессора максимальный размер стека ограничивается 4 Гбайт. Его структура организована по принципу LIFO (Last In First Out — "последним пришел, первым ушел"), т. е. при чтении информации первым извлекается блок данных, который был записан в стек последним. Запись информации происходит по инструкции PUSH, а чтение — по POP. При этом одна из особенностей стека заключается в том, что при записи данных он увеличивается в сторону младших адресов памяти.
Для работы со стеком используются три регистра процессора (рис. 2):
- ss — сегментный, содержащий адрес начала сегмента стека;
- sp/esp — регистр указателя, который всегда указывает на вершину стека, т. е. содержит смещение, по которому в стек был занесен последний элемент данных. Если стек пуст, то значение sp/esp равно адресу последнего байта сегмента, выделенного под стек;
- bp/ebp — регистр указателя базы кадра, обычно используемого для хранения адреса локальных переменных стека.
Значения регистров sp/esp и bp/ebp представляют собой смещения относительно сегментного регистра ss.
Рис. 2. Схема организации стека.
|
Рассмотрим порядок записи данных в стек при вызове функций на примере программы, исходный текст которой приведен в листинге 1. Первая запись при вызове функции test — значения трех параметров (a, b и c) в обратном порядке (с, b, a). Затем в стек помещается адрес возврата функции, т. е. адрес инструкции, которую процессор должен выполнить по завершении работы функции test. При выходе из функции этот адрес автоматически копируется в регистр EIP (Extended Instruction Pointer), из которого процессор считывает значение и передает управление команде по адресу этого регистра. Значение регистра EBP, который указывает на вершину, и локальные переменные функции main() также сохраняются в стеке. Далее в стек записываются локальные символьные массивы p1 и p2, определенные в функции test. Итоговая структура стека функции test() показана на рис. 3.
Рис. 3. Структура стека функции test().
|
Очевидно, что в приведенном примере уязвимость типа buffer overflow вызвана отсутствием проверки размерности данных, которые записываются в стек. В подобных случаях потенциальный нарушитель может записать в стек избыточную информацию, чтобы изменить значение адреса возврата и передать управление на фрагмент ранее внедренного вредоносного кода (листинг 2).
Листинг 2. Пример внесения изменения в стек для его переполнения #include <stdio.h> int main(int argc, char **argv){ char a1[4]="abc"; char a2[8]="defghij"; strcpy(a2, "0123456789"); printf("%s ", a1); return 0; } |
В приведенной программе определено два символьных массива типа char — a1 и a2. Первый массив a1 имеет размер 4 байт, второй — 8 байт. После определения переменных в программе выполняется функция strcpy, которая записывает строковое значение "0123456789" в массив a2. Но записываемое значение на 7 байт превышает размерность массива a2, что приводит к его переполнению и изменению значения массива a1, который размещен в стеке выше a2.
Рис. 4 иллюстрирует состояние фрагмента стека с переменными a1 и a2 до и после вызова функции strcpy. В левой его части показано расположение в стеке содержимого двух переменных, a1 и a2. Значения обеих завершаются символом "