Уязвимость программного кода

18 сентября 2011

Чтобы меньше совершать ошибок, нужно понимать, как можно использовать код для нарушения работы программы. Далее приведены наиболее интересные и важные проблемы безопасности, с которыми вы можете столкнуться.

Контроль данных

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

Если вы работали с Web-приложениями, то должны были слышать о знаменитой атаке SQL Injection. Теоретически данная атака возможна и в клиентских приложениях, просто там чаше используют параметризированные запросы или хранимые процедуры, где инъекция проблематична.

Переполнения

Для C++ наиболее распространенными, и в то же время наиболее опасными, являются ошибки переполнения, буфера стека и других не менее опасных мест. Дело в том, что в этом языке при выделении памяти нет никаких гарантий, что мы не выйдем за ее пределы.

Самое страшное, что все строки в С++ - это буферы памяти, которые также ничем не защищены. Работа со строками усложняется тем, что существует множество кодировок.

Рассмотрим процесс переполнения. Пусть есть функция в которую передается n – переменных. Программа при вызове функции помещает в стек все параметры, передаваемые в функцию, а также адрес возврата. Задача хакера поместить в буфер собственные данные и изменить адрес выполнения так, чтобы выполнить свой код.

Куда поместить код программы? У хакера не так уж и много вариантов и самый простой - поместить его в стек. Проблема ОС том, что код может выполняться не только из сегментах, но и из стека. Допустим, что одна из переменных передаваемых в функцию является строковой и имеет размер в 10 Кбайт. Вполне реальна я ситуация. Допустим, что значение строки вводит пользователь, передается по сети или читается из файла. И последнее допущение: функция копирования из источника в буфер не проверяет размер данных, а копирует все что получает на вход.

Теперь у нас есть все необходимое. Мы должны в строке передать Shell-код или другой код, который мы хотим выполнить. Одно условие: этот код должен быть помещен в отрезок памяти от начала переменной до адреса возврата, т.е. в нашем случае не более 10Кбайт. Профессионал может засунуть в 10Кбайт целого «слона», а если не сможет, то вызовет другую программу, которая сделает уж, все что надо.

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

Код хакера NOP NOP ….и так 20 Кбайт … адрес возврата

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

Пример опасного кода:

char buf[1024];

char input[2048];

strcpy(buf, input);

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

Как решить проблему? Нужно проверить, достаточно ли в буфере приемника памяти, чтобы принять все входящие данные. Если нет, то скопировать только выделенное количество байт под buf или сгенерировать сообщение об ошибке.

Безопасность не стоит на месте, и безопасная на сегодняшний день функция завтра может оказаться опасной. Именно из-за того, что безопасность не стоит на месте, не будем здесь описывать все опасные функции.

Ошибки логики

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

char but [1024];

buf[sizeof(buf)] = ‘\0’;

Все строки в С должны заканчиваться нулем. В примере в качестве последнего символа устанавливается нуль, чтобы гарантировать, что конец буфера будет не за пределами выделенной области. А ведь действительно, есть функции, которые не гарантируют, что строка будет завершаться нулем. Тогда конец строки может оказаться где угодно в памяти или стеке. Это очень опасно, поэтому насильно устанавливать конец строки в полнее логично. Но есть только один момент: нумерация в С начинается с нуля, поэтому последним символом является ячейка памяти с номером sizeof(buf)-1. Это типичная ошибка логики.

Рейтинг@Mail.ru