Добавлен: 29.10.2018
Просмотров: 48131
Скачиваний: 190
716
Глава 9. Безопасность
В коде содержится вызов чтения пользовательского ввода, где это чтение не является
частью стандартной библиотеки языка C. Мы просто предполагаем, что эта функция
чтения существует и возвращает целочисленное значение, набранное пользователем
в командной строке. Мы также предполагаем, что в ней нет ошибок. Но даже при этом
из данного кода довольно просто организовать утечку информации. Нужно лишь
предоставить индекс больше, чем 15, или меньше, чем 0. Поскольку программа индекс
не проверяет, она с готовностью вернет значение любого целого числа из памяти.
Для успешной атаки зачастую вполне достаточно адреса одной функции. Причина
в том, что хотя место, куда загружается каждая библиотека, может быть выбрано про-
извольно, относительное смещение для каждой отдельной функции от этой позиции
имеет, как правило, фиксированное значение. Иначе говоря, узнав, где находится одна
функция, вы узнаете, где находятся все остальные. Даже если это не так, то при нали-
чии всего лишь одного адреса кода зачастую довольно просто отыскать многие другие
адреса, что и показано в работе Snow et al. (2013).
Атаки, не связанные с перенаправлением потока управления
До сих пор рассматривались атаки на поток управления программы: изменения указа-
телей функций и адресов возврата. Целью всегда было заставить программу выполнять
новые функции, даже если эти функции брались из того же кода, который уже присут-
ствовал в двоичном виде. Но это не единственная возможность. Как показано в следую-
щем фрагменте псевдокода, интересной целью для взломщика могут быть и сами данные:
01. void A() {
02. int author ized;
03. char name [128];
04. authorized = check credentials (...); /* атакующий не авторизован,
поэтому возвращается 0 */
05. printf ("Как вас зовут?\n");
06. gets (name);
07. if (authorized != 0) {
08. printf ("Добро пожаловать %s, вот все ваши секреты \n", name)
09. /* ... демонстрация секретных данных ... */
10. } else
11. printf ("Извините, %s, но вы не прошли авторизацию.\n");
12. }
13. }
Этот код предназначен для проверки авторизации. Совершенно секретные данные
разрешается просматривать только тем пользователям, которые имеют на это соответ-
ствующие полномочия. Функция, проверяющая полномочия, не входит в состав библи-
отеки языка C, но предполагается, что она существует где-то в программе и не содержит
ошибок. Теперь предположим, что взломщик ввел 129 символов. Как и в предыдущем
случае, буфер переполнился, но адрес возврата при этом переписан не был. Вместо этого
взломщик изменил значение переменной авторизации, дав ей значение, отличное от
нуля. Программа не вошла в аварийное состояние и не выполнила код взломщика, но
она допустила утечку секретной информации в адрес неавторизованного пользователя.
Переполнение буфера: точка еще не поставлена
Переполнение буфера является одной из самых старых и наиболее важных техноло-
гий искажений памяти, используемой взломщиками. Несмотря на более чем четверть
9.7. Взлом программного обеспечения
717
века существования связанных с ней инцидентов и на обилие средств защиты (нами
были рассмотрены лишь наиболее важные из них), похоже, избавиться от этой про-
блемы невозможно (Van der Veen, 2012). За все это время значительная доля проблем
безопасности возникала из-за этого дефекта, который трудно устранить, поскольку
вокруг существует множество программ на языке C, не проверяющих переполнение
буфера.
И гонка вооружений еще далека от завершения. Исследователи по всему миру иссле-
дуют все новые средства обороны. Некоторые из этих средств нацелены на двоичные
коды, некоторые состоят из расширений безопасности для компиляторов C и C++.
Важно отметить, что взломщики также совершенствуют свои вредоносные технологии.
В данном разделе мы постарались представить вам обзор некоторых наиболее важных
технологий, но у этой идеи существует множество вариаций. И мы практически увере-
ны, что в следующем издании данной книги этот раздел не утратит своей актуальности
(и, возможно, будет расширен).
9.7.2. Атаки, использующие форматирующую строку
Следующая атака также нацелена на повреждение памяти, но имеет совершенно иную
природу. Некоторые программисты не любят набирать текст, даже если они являются
мастерами этого дела. Зачем набирать имя переменной reference_count, когда понятно,
что rc означает то же самое и экономит 13 нажатий на клавиши при каждом своем по-
явлении? Такая нелюбовь к набору текста порой может приводить к катастрофическим
отказам системы.
Рассмотрим следующий фрагмент программы на языке C, выводящей при запуске
традиционное для C приветствие:
char * s="Hello World";
printf("%s", s);
В этой программе объявляется переменная строки символов s, которой присваивает-
ся начальное строковое значение «Hello World» и нулевой байт, свидетельствующий
о конце строки. Функция printf вызывается с двумя аргументами: строкой формата
«%s», который предписывает функции вывести строку, и адресом строки. При выпол-
нении программы этот фрагмент кода выводит строку на экран (или на стандартное
устройство вывода). Он вполне корректен и неуязвим.
Но предположим, что программист поленился и вместо верхнего фрагмента набрал
следующее:
char * s="Hello World";
printf(s);
Такой вызов printf вполне допустим, поскольку у этой функции непостоянное число
аргументов, где первым из них может быть форматирующая строка. Но строка, не со-
держащая никакой информации о формате (такой, как в строке «%s»), также допусти-
ма, и хотя вторая версия считается дурным тоном в программировании, она разрешена
и вполне работоспособна. Кроме того, экономится целых пять нажатий на клавиши,
что, несомненно, является большой победой.
Полгода спустя какой-нибудь другой программист получает команду изменить код,
чтобы сначала запрашивалось имя пользователя, а затем оно фигурировало в при-
718
Глава 9. Безопасность
ветствии. После поспешного изучения кода он его слегка подправляет, и в результате
получается следующее:
char s[100], g[100] = "Hello "; /* объявление s и g; инициализация g */
gets(s); /* чтение строки с клавиатуры в s */
strcat(g, s); /* подстановка s в конец g */
printf(g); /* вывод g */
Теперь программа считывает строку в переменную s и объединяет ее с инициализиро-
ванной строкой g, чтобы создать на основе g выходное сообщение. Программа сохраняет
работоспособность. Пока вроде нет ничего плохого (за исключением использования
функции gets, подверженной атаке за счет переполнения буфера, но простота исполь-
зования способствует ее популярности).
Но знающий пользователь, увидевший этот код, быстро поймет, что ввод, воспри-
нимаемый с клавиатуры, — это не просто строка, это строка формата вывода, и, по
существу, будут работать все спецификации формата вывода, разрешенные printf.
Хотя большинство индикаторов форматирования, такие как «%s» (для вывода строк)
и «%d” (для вывода десятичных целых чисел), форматируют вывод, некоторые из них
имеют специальное назначение. В частности, «%n” ничего не выводит. Вместо этого
он подсчитывает, какое количество символов должно было быть выведено в месте его
появления в строке, и сохраняет информацию для обработки в следующем аргументе
printf. Рассмотрим пример программы, использующей «%n”:
int main(int argc, char * argv[])
{
int i=0;
printf("Hello %nworld\n", &i); /* %n сохраняется в i */
printf("i=%d\n", i); /* теперь i равно 6 */
}
После компиляции и запуска эта программа выводит следующее:
Hello world
i=6
Обратите внимание на то, что переменная i была изменена путем вызова функции
printf, что не всем понятно. При всей весьма сомнительной пользе от этого свойства
оказывается, что вывод форматирующей строки может привести к тому, что слово
или множество слов будут сохранены в памяти. Неужели ввод этого свойства в функ-
цию printf был разумной идеей? Конечно же нет, но в свое время оно казалось очень
удобным. Подобным образом в программное обеспечение закладывались многие
уязвимости.
Из предыдущего примера видно, что совершенно случайно программист, модифициро-
вавший программный код, непреднамеренно позволил пользователю программы ввести
строку формата. Поскольку вывод строки формата способен переписать содержимое
памяти, теперь у нас есть средство, позволяющее переписывать в стеке адреса возвра-
та функции printf и передавать управления в какое-нибудь другое место, например
в только что введенную строку формата. Этот подход получил название атаки, ис-
пользующей строку описания формата вывода
(format string attack).
Проведение атаки, использующей строку описания формата вывода, является не та-
кой уж тривиальной задачей. Где будет храниться выводимое функцией количество
9.7. Взлом программного обеспечения
719
символов? В адресе аргумента, следующего за самой строкой формата, как и в по-
казанном ранее примере. Но в коде, имеющем уязвимость, взломщик может предо-
ставить только одну строку (опустив второй аргумент функции printf). Фактически
функция printf предположит, что второй аргумент существует. Она просто возьмет
очередное значение в стеке и использует его в качестве этого аргумента. Взломщик
может также заставить функцию printf воспользоваться следующим значением в сте-
ке, предоставив в качестве ввода, к примеру, такую форматирующую строку:
"%08x %n"
Ее часть «%08x» означает, что printf выведет следующий аргумент в виде 8-разрядного
шестнадцатеричного числа. Следовательно, если это значение равно 1, будет выведено
0000001. Иными словами, при такой форматирующей строке функция printf просто
предположит, что следующим значением в стеке является 32-разрядное число, которое
нужно вывести, а значение, находящееся после него, является адресом того места, где
нужно сохранить количество выводимых символов, в данном случае 9: 8 для шестнад-
цатеричного числа и 1 для пробела. Предположим, что предоставлена форматирующая
строка
"%08x %08x %n"
В этом случае функция printf сохранит значение в адресе, предоставленном третьим
значением, следующим в стеке за форматирующей строкой, и т. д. Это является ос-
новой для того, чтобы превратить показанную ранее форматирующую строку в эле-
ментарный дефект, позволяющий взломщику «записывать что угодно и где угодно».
Подробности не вписываются в тему данной книги, но идея заключается в том, что
взломщик делает так, что в стеке находится нужный ему целевой адрес. Это проще,
чем вы могли подумать. Например, в представленном ранее коде с уязвимостью сама
строка g находится в стеке по более высокому адресу, чем стековый фрейм функции
printf (рис. 9.21). Предположим, что, как показано на рисунке, строка начинается
с AAAA, за ней находится последовательность «%0x» и заканчивается все после-
довательностью «%0n». Что же при этом происходит? Получая точное количество
символов в последовательности «%0x», взломщик добирается до самой форматиру-
ющей строки (хранящейся в буфере B). Иными словами, функция printf будет затем
использовать первые четыре байта форматирующей строки в качестве того адреса,
куда следует вести запись. Поскольку ASCII-значение символа A равно 65 (или 0x41
в шестнадцатеричном формате), результат будет записан по адресу 0x41414141, но
взломщик может указать и другие адреса. Разумеется, он должен убедиться в аб-
солютной точности количества выводимых символов (поскольку это количество
будет записано в целевой адрес). На практике ему приходится предпринимать чуть
больше действий, чем здесь описано, но не намного больше. Если в строке поиска
в Интернете набрать «format string attack», вы увидите большое количество ссылок
на информацию, посвященную этой проблеме.
Поскольку пользователь получил возможность переписывать содержимое памяти
и принудительно передавать управление только что внедренному коду, этот код имеет
все полномочия и доступ, которые были у атакованной программы. Если программа
согласно установке бита SETUID принадлежала пользователю root, атакующий сможет
создать оболочку с привилегиями пользователя root. Кстати, использованные в этом
примере символьные массивы фиксированной длины также могут стать мишенью для
атаки за счет переполнения буфера.
720
Глава 9. Безопасность
Áóôåð B
Ïåðâûé àðãóìåíò ôóíêöèè printf
(óêàçàòåëü íà ôîðìàòèðóþùóþ ñòðîêó)
Ñòåêîâûé ôðåéì
ôóíêöèè printf
Рис. 9.21. Атаки, использующие форматирующую строку.
Путем использования точного количества символов в %08x взломщик может
воспользоваться в качестве адреса первыми четырьмя символами форматирующей строки
9.7.3. Указатели на несуществующие объекты
Третьей технологией искажения памяти является весьма популярный в преступном
мире прием, называемый атакой с использованием указателей на несуществующие
объекты (dangling pointer attack). В простом проявлении этой технологии разобраться
совсем нетрудно, но вот создание вредоносного кода может оказаться непростой за-
дачей. C и C++ позволяют программе распределять память под кучу, используя вызов
функции malloc, при котором возвращается указатель на только что выделенную часть
памяти. Позже, когда программа больше в ней не нуждается, она вызывает функцию
free для освобождения памяти. Ошибка указателя на несуществующий объект проис-
ходит, когда программа случайно использует память после того, как она уже ее освобо-
дила. Рассмотрим следующий код, который дискриминирует весьма пожилых людей:
01. int *A = (int *) malloc (128); /* выделение места под 128 целых
чисел */
02. int year of birth = read user input (); /* считывание целого числа из
стандартного ввода */
03. if (input < 1900) {
04. printf ("Ошибка, год рождения должен быть больше 1900 \n");
05. free (A);
06. } else {
07. ...
08. /* совершение полезных действий над содержимым массива A */
09. ...
10. }
11. ... /* множество других инструкций, содержащих, malloc и free */
12. A[0] = year of birth;