Подтвердить что ты не робот

Уязвимость в формате строки - printf

Почему это печатает значение адреса памяти в 0x08480110? Я не уверен, почему есть 5% аргументов 08x - где это приведет вас к стеку?

address = 0x08480110
address (encoded as 32 bit le string): "\x10\x01\x48\x08"
printf ("\x10\x01\x48\x08_%08x.%08x.%08x.%08x.%08x|%s|");

Этот пример взят со страницы 11 этой статьи http://crypto.stanford.edu/cs155/papers/formatstring-1.2.pdf

4b9b3361

Ответ 1

Я думаю, что документ предоставляет примеры printf() несколько запутанным образом, потому что в примерах используются строковые литералы для строк формата, и они обычно не позволяют описывать тип уязвимости. Уязвимость в формате строки, описанная здесь, зависит от строки формата, предоставляемой пользовательским вводом.

Итак, пример:

printf ("\x10\x01\x48\x08_%08x.%08x.%08x.%08x.%08x|%s|");

Может показаться лучше:

/* 
 * in a real program, some user input source would be copied 
 * into the `outstring` buffer 
 */
char outstring[80] = "\x10\x01\x48\x08_%08x.%08x.%08x.%08x.%08x|%s|";

printf(outstring);

Поскольку массив outstring является автоматическим, компилятор, скорее всего, поместит его в стек. После копирования пользовательского ввода в массив outstring он будет выглядеть следующим образом: "слова" в стеке (если предположить, что он немного endian):

outstring[0c]               // etc...
outstring[08] 0x30252e78    // from "x.%0"
outstring[04] 0x3830255f    // from "_%08"
outstring[00] 0x08480110    // from the ""\x10\x01\x48\x08"

Компилятор поместит другие элементы в стек, как он считает нужным (другие локальные переменные, сохраненные регистры, что угодно).

Когда вызов printf() будет готов, стек может выглядеть так:

outstring[0c]               // etc...
outstring[08] 0x30252e78    // from "x.%0"
outstring[04] 0x3830255f    // from "_%08"
outstring[00] 0x08480110    // from the ""\x10\x01\x48\x08"
var1
var2
saved ECX
saved EDI

Обратите внимание, что я полностью делаю эти записи вверх - каждый компилятор будет использовать стек по-разному (так что уязвимость в форматированной строке должна быть создана для конкретного точного сценария. Другими словами, вы не всегда будете использовать 5 спецификаторов формата фиктивного типа, как в этом примере, - в качестве злоумышленника вам нужно выяснить, сколько манекенов потребуется для конкретной уязвимости.

Теперь для вызова printf() аргумент (адрес outstring) вставляется в стек и вызывается printf(), поэтому область аргументов стека выглядит так:

outstring[0c]               // etc...
outstring[08] 0x30252e78    // from "x.%0"
outstring[04] 0x3830255f    // from "_%08"
outstring[00] 0x08480110    // from the ""\x10\x01\x48\x08"
var1
var2
var3
saved ECX
saved EDI
&outstring   // the one real argument to `printf()`

Однако printf ничего не знает о том, сколько аргументов было помещено в стек для него - оно идет по спецификаторам формата, которые он находит в строке формата (один аргумент, который он "уверен" получает). Таким образом, printf() получает аргумент строки формата и начинает его обрабатывать. Когда он доберется до первого "% 08x", который будет соответствовать "сохраненному EDI" в моем примере, тогда следующий "% 08x" распечатает сохраненный ECX 'и т.д. Поэтому спецификаторы формата "% 08x" просто едят данные в стеке, пока не вернутся к строке, которую мог ввести злоумышленник. Определение того, сколько из них требуется, - это то, что злоумышленник будет делать с помощью своего рода проб и ошибок (возможно, с помощью тестового прогона, в котором есть множество "% 08x", пока он не сможет "увидеть", где начинается строка формата).

В любом случае, когда printf() получает обработку спецификатора формата "% s", он потребляет все записи стека до того места, где находится буфер outstring. Спецификатор "% s" рассматривает свою запись в стеке как указатель, а строка, которую пользователь помещает в этот буфер, была тщательно обработана, чтобы иметь двоичное представление 0x08480110, поэтому printf() распечатает все, что есть на этом адрес как строка ASCIIZ.

Ответ 2

У вас есть 6 спецификаторов формата (5 лотов %08x и один из %s), но вы не указываете значения для этих спецификаторов формата. Вы сразу попадаете в сферу поведения undefined - все может случиться, и нет неправильного ответа.

Однако при нормальном ходе событий значения, переданные в printf(), были бы сохранены в стеке, поэтому код в printf() считывает значения из стека, как если бы эти дополнительные значения были переданы. Адрес возврата функции также находится в стеке. Нет никакой гарантии, что я смогу увидеть, что действительно будет произведено значение 0x08480110. Такая атака очень сильно зависит от конкретной программы и неисправности вызова функции, и вы можете получить совсем другое значение. Пример кода, скорее всего, написан в предположении, что 32-разрядный процессор Intel (little-endian), а не 64-разрядный или большой процессор.


Адаптация фрагмента кода, компиляция его в полную программу, игнорирование предупреждений компиляции, с использованием 32-разрядной компиляции на MacOS X 10.6.7 с GCC 4.2.1 (XCode 3), следующий код:

#include <stdio.h>

static void somefunc(void)
{
    printf("AAAAAAAAAAAAAAAA.0x%08X.0x%08X.0x%08X.0x%08X.0x%08X.0x%08X.0x%08X.|%s|\n");
}

int main(void)
{
    char buffer[160] =
        "abcdefghijklmnopqrstuvwxyz012345"
        "abcdefghijklmnopqrstuvwxyz012345"
        "abcdefghijklmnopqrstuvwxyz012345"
        "abcdefghijklmnopqrstuvwxyz012345"
        "abcdefghijklmnopqrstuvwxyz01234";
    somefunc();
    return 0;
}

дает следующий результат:

 AAAAAAAAAAAAAAAA.0x000000A0.0xBFFFF11C.0x00001EC4.0x00000000.0x00001E22.0xBFFFF1C8.0x00001E5A.|abcdefghijklmnopqrstuvwxyz012345abcdefghijklmnopqrstuvwxyz012345abcdefghijklmnopqrstuvwxyz012345abcdefghijklmnopqrstuvwxyz012345abcdefghijklmnopqrstuvwxyz01234|

Как вы можете видеть, я в конце концов "нашел" строку в основной программе из инструкции printf(). Когда я скомпилировал его в 64-битном режиме, вместо этого я получил базовый дамп. Оба результата совершенно правильны; программа вызывает поведение undefined, поэтому все, что делает программа, является допустимым. Если вам интересно, найдите "носовые демоны" для получения дополнительной информации о поведении undefined.

И привыкнуть к экспериментированию с такими проблемами.


Другая вариация

#include <stdio.h>

static void somefunc(void)
{
    char format[] =
        "AAAAAAAAAAAAAAAA.0x%08X.0x%08X.0x%08X.0x%08X.0x%08X\n"
        ".0x%08X.0x%08X.0x%08X.0x%08X.0x%08X.0x%08X.0x%08X\n"
        ".0x%08X.0x%08X.0x%08X.0x%08X.0x%08X.0x%08X.0x%08X\n";
    printf(format, 1);
}

int main(void)
{
    char buffer[160] =
        "abcdefghijklmnopqrstuvwxyz012345"
        "abcdefghijklmnopqrstuvwxyz012345"
        "abcdefghijklmnopqrstuvwxyz012345"
        "abcdefghijklmnopqrstuvwxyz012345"
        "abcdefghijklmnopqrstuvwxyz01234";
    somefunc();
    return 0;
}

Это дает:

AAAAAAAAAAAAAAAA.0x00000001.0x00000099.0x8FE467B4.0x41000024.0x41414141
.0x41414141.0x41414141.0x2E414141.0x30257830.0x302E5838.0x38302578.0x78302E58
.0x58383025.0x2578302E.0x2E583830.0x30257830.0x2E0A5838.0x30257830.0x302E5838

Вы можете распознать строку формата в шестнадцатеричном представлении - например, 0x41 - это капитал A.

64-разрядный вывод этого кода аналогичен и отличается:

AAAAAAAAAAAAAAAA.0x00000001.0x00000000.0x00000000.0xFFE0082C.0x00000000
.0x41414141.0x41414141.0x2578302E.0x30257830.0x38302578.0x58383025.0x0A583830
.0x2E583830.0x302E5838.0x78302E58.0x2578302E.0x30257830.0x38302578.0x38302578

Ответ 3

Вы неправильно поняли статью.

Связанный вами текст предполагает, что текущая позиция в стеке - 0x08480110 (посмотрите на окружающий текст). printf() будет удалять данные из любого места в стеке, в котором вы оказались.

\x10\x01\x48\x08 в начале строки формата является просто печать (предполагаемого) адреса на stdout перед сбрасываемыми данными. Ни в коем случае эти цифры не изменяют адрес, из которого данные сбрасываются.

Ответ 4

Вы правильно относитесь к тому, чтобы "взять вас в стек", но только чуть-чуть; он полагается на предположение, что аргументы передаются в стеке, а не в регистры. (Что для variadic function, вероятно, является безопасным предположением, но все же предположением о деталях реализации.)

Каждый %08x запрашивает "следующий unsigned int аргумент" для печати в шестнадцатеричном формате; то, что на самом деле происходит в этом местоположении "следующего аргумента", зависит как от архитектуры, так и от компилятора. Если вы сравните значения, которые вы получаете с помощью /proc/self/maps для процесса, вы можете сузить число некоторых чисел.