Ахиллесова пята информационных систем
Виктор Сердюк,
ведущий инженер-программист Департамента развития технологий информационной безопасности компании РНТ
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. Значения обеих завершаются символом "


