Уязвимость в формате строки - 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
Ответы
Ответ 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
для процесса, вы можете сузить число некоторых чисел.