Ошибка форматной строки - printf - PullRequest
12 голосов
/ 15 апреля 2011

Почему это печатает значение адреса памяти в 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

Ответы [ 4 ]

21 голосов
/ 15 апреля 2011

Я думаю, что в статье приводятся примеры 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, он будет выглядеть как «слова» в стеке (при условии, что они имеют порядковый номер):

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() получает аргумент строки формата и начинает его обработку. Когда он достигнет 1-го «% 08x», который будет соответствовать «сохраненному EDI» в моем примере, то следующий «% 08x» напечатает сохранил ECX 'и так далее. Таким образом, спецификаторы формата "% 08x" просто поглощают данные в стеке до тех пор, пока они не вернутся к строке, которую злоумышленник смог ввести. Определение того, сколько из них необходимо, - это то, что злоумышленник сделал бы методом проб и ошибок (вероятно, при выполнении теста, который имеет целую серию форматов «% 08x», пока он не может «увидеть», где начинается строка формата).

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

8 голосов
/ 15 апреля 2011

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

Однако при нормальном ходе событий значения, переданные в printf(), будут храниться в стеке,поэтому код в printf() считывает значения из стека, как если бы были переданы дополнительные значения.Адрес возврата функции также находится в стеке.Нет никаких гарантий, что я вижу, что значение 0x08480110 действительно будет получено.Этот тип атаки во многом зависит от конкретной программы и вызова неисправной функции, и вы вполне можете получить совершенно другое значение.Пример кода, скорее всего, написан с использованием 32-разрядного процессора Intel (с прямым порядком байтов), а не 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-битном режиме, я получил дамп ядра.Оба результата совершенно верны;программа вызывает неопределенное поведение, поэтому все, что делает программа, является действительным.Если вам интересно, поищите «носовые демоны» для получения дополнительной информации о неопределенном поведении.

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


Еще один вариант

#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
1 голос
/ 15 апреля 2011

Вы неправильно поняли бумагу.

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

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

0 голосов
/ 15 апреля 2011

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...