Я думаю, что в статье приводятся примеры 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.