Уязвимость форматной строки на примере архитектуры Intel IA-32
Секция: Технические науки
XL Студенческая международная заочная научно-практическая конференция «Молодежный научный форум: технические и математические науки»
Уязвимость форматной строки на примере архитектуры Intel IA-32
Уязвимость форматной строки – это довольно обширный класс уязвимостей, которые возникают и могут быть проэксплуатированы вследствие ошибок программистов. Если программист передаёт контролируемый атакующим буфер в качестве аргумента функции printf() (или другую связанную с ней функцию такую, как, например, sprintf(), fprintf()), то злоумышленник может выполнять запись в произвольные места в памяти. Следующий код как раз содержит такую ошибку (рис. 1):
Рисунок 1.
Так как printf имеет переменное число аргументов, то она вынуждена использовать форматную строку, чтобы определить их количество. В случае, представленном на рисунке выше, атакующий может передать строку %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p и «обмануть» printf, сымитировав передачу 15 аргументов. Функция printf напечатает 15 адресов, лежащих на стеке, предполагая, что они представляют собой переданные аргументы (рис. 2):
Рисунок 2.
После 10 аргументов мы можем видеть повторение адреса 0x252070 – это ничто иное, как наша строка, состоящая из %p. Чтобы увидеть это получше начнём нашу строку с символов «А» (рис. 3):
Рисунок 3.
0x4141414 – это шестнадцатеричное представление «АААА». Но это не последняя особенность функции printf: с помощью специального параметра мы можем выбрать необходимый нам аргумент. Так, например,
printf("%2$x", 1, 2, 3)напечатает 2. В общем случае мы можем отправлять аргументы в уязвимую функцию следующим образом: printf("%<some number>$x") – чтобы выбрать произвольный аргумент при printf.
Но самая интересная особенность printf - это наличие специального аргумента %n, записывающего число напечатанных символов по адресу, лежащему на вершине стека. То есть, если мы пошлём строку AAAA%10$n, то мы с полной уверенностью запишем значение 4 по адресу 0x4141414. Так же мы можем использовать ещё одну особенность printf("AAAA%100x"): 104 символа будут напечатаны т.к. она тем самым выводит на экран минимальное количество символов (в нашем случае их сто). Это позволяет нам дать возможность напечатать именно то, количество символов, которое мы хотим, не вводя их руками.
Однако, если мы хотим, например, записать значение 0x0804a004, то мы должны будем вывести 134520836 символов! Что крайне неудобно и не практично. Мы обойдём это двумя записями: во-первых, мы запишем 0x0804 в старшие байты нужного адреса, после чего 0xa004 в два нижних байтах. Чтобы сделать это необходимо использовать спецификатор %hn для записи по 2 байтам. Такая посылаемая нами строка будет выглядеть следующим образом:
CAAAAAAA%2044x%10$hn%38912x%11$hn
· CAAAAAAA – старшие два байта конечного адреса (0x41414143) и два младших байта адреса (0x41414141
)
· %2044x%10$hn – таким образом мы перепишем 2052 байта, когда достигнем первого %hn т.к. до этого переписали 8 байт (предыдущей строкой), тем самым необходимо ещё 2044 байта
· %38912x%11$hn – таким образом мы перепишем 40964 байт, когда мы достигнем второго %hn т.к. до этого уже переписали 2052 байта нам необходимо дополнительно переписать 38912 байт.
Поскольку уязвимость форматной строки позволяет нам переписать произвольное значение по произвольному адресу, то это открывает перед нам большие возможности. Обычно этим пользуются, чтобы переписать адрес, указывающий на определённое место в памяти (например, адрес возврата), где хранится приготовленный нами заранее исполнительный код, который выполнится, как только будет совершён переход по указанному нами адресу. В программах, скомпилированных без определённых ключей безопасности, это осуществляется очень просто.
Возможен и другой вариант действий. Мы перепишем указатель на функцию из общей библиотеки «своим», по переходе на который, выполнится, например, также установленный нами заранее код. Поясним это чуть более подробно. Когда программа попытается выполнить функцию из общей библиотеки, то абсолютно не важно знать истинное расположение этой функции в момент исполнения программы. Тем самым будет произведён переход по адресу, который раннее был переписан нами. Изначально указатель (расположенный в глобальной таблице смещений) инициализируется в момент во время исполнения, когда происходит первый вызов. Рассмотрим пример на рис. 4.
Рисунок 4.
Здесь мы можем видеть, как легко узнать правильный адрес функции strcat() в общей библиотеке в момент исполнения, что может провести к его дальнейшему изменению и выполнению хода программы уже по тому пути, который приготовил злоумышленник.