Различия

Здесь показаны различия между двумя версиями данной страницы.

Ссылка на это сравнение

articles:buf.c.printf [2017/09/05 02:55] (текущий)
Строка 1: Строка 1:
 +====== buf.C.printf ======
 +<​sub>​{{buf.C.printf.odt|Original file}}</​sub>​
 +
 +====== спецификаторы под арестом\\ или дерни printf за хвост ======
 +
 +крис касперски ака мыщъх
 +
 +**строки форматного вывода и спецификаторы не относятся к козырному оружию хакера. к ним прибегают лишь от бессилия,​ когда все остальные средства исчерпали себя, так и не дав результата. но загнанная в угол крыса загрызает собаку. забытый в казарме нож может стоить жизни. в конечном счете никто из нас не знает, что ему пригодиться. так почему бы вам не овладеть техникой фехтования спецификаторами?​**
 +
 +**alt****:​строки форматного вывода представляют собой оружие многоцелевого назначения и хотя сфера их применения ограничена,​ а базирующиеся на них атаки немногочисленны и редки, владеть этой техникой необходимо хотя бы уже затем, чтобы не попасть впросак.**
 +
 +===== введение =====
 +
 +Язык Си выгодно отличается от Паскаля поддержкой //​**спецификаторов**//,​ представляющих собой мощный инструмент форматного вывода/​вывода. Настолько мощный,​ что фактически образующий язык внутри языка. Идея была позаимствована из Фортрана,​ создатели которого учли главный недостаток его предшественника – Алгола. Языка, сосредоточившегося на алгоритмизации (отсюда и название) и пренебрежительно относящегося к вводу/​выводу,​ считая его побочным продуктом основной деятельности. Наивные! Генерация отчетов всегда доставала программистов,​ отнимания уйму времени и сил, оставаясь наиболее нудной и рутиной частью программы. А давайте ее автоматизируем! – решили патриархи. Сказано – сделано. Так в языке Си появился полноценный интерпретатор форматных символов,​ сразу же завоевавший бешенную популярность. А вместе с ним появились и проблемы:​ небрежное обращение со спецификаторами породило новое тип ошибок переполнения,​ или даже поколение. Если это поколение,​ то оно будет третьим по счету. Первые два – последовательное и индексное переполнение уже были рассмотрены в предыдущей статье.
 +
 +Ошибки форматного вывода достаточно малочисленны и встречаются главным образом в UNIX-приложениях,​ где традиции терминального режима все еще остаются сильны. По некоторым оценкам,​ в 2002 году было обнаружено порядка 100 уязвимых приложений,​ а в 2003 – свыше 150! Атаке подверглись сервера баз данных,​ вращающихся под Oracle и сервисы UNIX, такие, например,​ как syslog или ftp. Ни одной атаки на приложения Windows NT до сих пор не зафиксировано. Это не значит,​ что Windows NT лучше, просто графический интерфейс не располагает к интенсивному использованию форматного вывода,​ да и количество консольных утилит под NT очень невелико,​ тем не менее только глупец может считать,​ что он находится в безопасности. Не верите?​ Вот щас мы вам покажем!
 +
 +===== >>>​ врезка\\ функции,​ поддерживающие форматированный вывод =====
 +
 +Услугами интерпретатора форматного ввода/​вывода пользуется множество функций,​ не только **printf** и не только в консольных программах. Графические приложения и серверное программное обеспечение,​ исполняющееся под Windows NT,​ активно использует функцию **sprintf**,​ выводящую отформатированную строку в оперативный буфер.
 +
 +Функции,​ перечисленные в таблице 1,​ сами по себе не опасны. Опасными их делает наличие пользовательского ввода в форматном аргументе. Именно такие участки кода и нужно искать при исследовании программы на уязвимость.
 +
 +|**функция**|**назначение**||
 +|fprintf|ASCII|форматированный вывод в файл|
 +|fwprintf|UNICODE| ::: |
 +|fscanf|ASCII|форматированный ввод с потока|
 +|fwscanf|UNICODE| ::: |
 +|printf|ASCII|форматированный вывод в stdout|
 +|wprintf|UNICODE| ::: |
 +|scanf|ASCII|форматированный ввод с stdin|
 +|wscanf|UNICODE| ::: |
 +| _snprintf|ASCII|форматированный вывод в буфер с ограничителем длины|
 +|_snwprintf|UNICODE| ::: |
 +|sprintf|ASCII|форматированный вывод в буфер|
 +|swprintf|UNICODE| ::: |
 +|sscanf|ASCII|форматированный ввод из буфера|
 +|swscanf|UNICODE| ::: |
 +|vfprintf|ASCII|форматированный вывод в поток (stream)|
 +|vfwprintf|UNICODE| ::: |
 +|vprintf|ASCII|форматированный вывод в stdout|
 +|vwprintf|UNICODE| ::: |
 +|_vsnprintf|ASCII|форматированный вывод в буфер с ограничителем длины|
 +|_vsnwprintf|UNICODE| ::: |
 +|vsprintf|ASCII|форматированный вывод в буфер|
 +|vswprintf|UNICODE| ::: |
 +
 +Таблица 1 основные функции форматного вывода с кратким описанием
 +
 +===== >>>​ врезка патч cfingerd'​a =====
 +
 +Ниже в качестве наглядно-агитационного пособия приведен патч, накладываемый на cfinger демон для устранения уязвимости в обработке форматных строк:
 +
 +snprintf(syslog_str,​ sizeof(syslog_str),​
 +
 +"%s fingered (internal) from %s", username, ident_user);​
 +
 +- syslog(LOG_NOTICE,​ (char *) syslog_str);//​ пользовательскийввод
 +
 +// в форматном аргументе
 +
 ++ syslog(LOG_NOTICE,​ "​%s",​(char *) syslog_str);//​ явнаяспецификация
 +
 +// формантного аргумента
 +
 +Листинг 1 патч cfingerd'​а
 +
 +===== источники угрозы =====
 +
 +Основных источников угрозы всего три. Это: а) навязывание уязвимой программе собственных спецификаторов;​ б) врожденный дисбаланс спецификаторов;​ и в) естественное переполнение буфера-приемника при отсутствии проверки на предельно допустимую длину строки.
 +
 +===== навязывание собственных спецификаторов =====
 +
 +Если пользовательский ввод попадет в строку форматного вывода (что происходит довольно часто) и находящиеся в нем спецификаторы не будут отфильтрованы (а кто их фильтрует?​),​ злоумышленник сможет манипулировать интерпретатором форматного вывода по своему усмотрению,​ вызывая ошибки доступа,​ читая и перезаписывая ячейки памяти и при благоприятных условиях захватывая управление удаленной системой.
 +
 +Рассмотрим следующий пример,​ к которому мы не раз будет обращаться в дальнейшем. Как вы думаете,​ где здесь лыжи?
 +
 +f()
 +
 +{char buf_in[32], buf_out[32];​
 +
 +
 +
 +printf("​введиимя:"​);​ gets(buf_in);​
 +
 +sprintf(buf_out,​ "​hello,​ %s!\n",​ buf_in);
 +
 +
 +
 +printf(buf_out);​
 +
 +}
 +
 +Листинг 2 демонстрационный пример,​ подверженный ошибкам переполнения различных типов
 +
 +{{buf.c.printf_Image_0.png?​553}}
 +
 +Рисунок 1 состояние стека на момент вызова функции printf
 +
 +==== реализация DoS ====
 +
 +Для аварийного завершения программы достаточно вызвать нарушение доступа,​ обратившись к невыделенной,​ несуществующей или заблокированной ячейке памяти. Это легко. Встретив спецификатор "​%s"​ интерпретатор форматного вывода извлекает из стека парный ему аргумент,​ трактуя его как указатель на строку. Если же тот отсутствует,​ интерпретатор хватает первый попавшийся указатель и начинает читать содержимое памяти по этому адресу до тех пор, пока не встретит нуль или не нарвется на запрещенную ячейку. Политика запретов варьируется от одной операционной системы к другой,​ в частности,​ при обращении по адресам:​ 00000000h – 0000FFFFh и 7FFF000h – FFFFFFFFhWindowsNT всегда возбуждает исключение. Остальные же адреса в зависимости от состояния кучи, стека и статической памяти могут быть как доступными,​ так и нет.
 +
 +Откомпилируем пример,​ приведенный в листинге 2,​ и запустим его на выполнение. Вместо своего имени введем строку "​%s"​ (естественно,​ без кавычек). Программа ответит:​
 +
 +введиимя:​%s
 +
 +hello,​ hello,​ %s!\n!"​
 +
 +Листинг 3 реакция программы на навязанный ей спецификатор %s
 +
 +Чтобы понять,​ что такое "​hello,​ %s!"​ и откуда оно здесь взялось,​ необходимо проанализировать состояние стека на момент вызова printf(buf_out),​ в чем нам поможет **отладчик**,​ например,​ тот, что интегрирован в MicrosoftVisualStudio (см. рис. 1)
 +
 +Первым идет двойное слово 0012FF5Ch (на микропроцессорах архитектуры Intel младший байт располагается по меньшему адресу,​ т. е. все числа записываются в памяти задом наперед). Это указатель,​ соответствующий аргументу функции printf, которому в свою очередь соответствует буфер buf_out, содержащий непарный спецификатор "​%s",​ заставляющий функцию printf извлекать следующее двойное слово из стека, представляющее собой обыкновенный мусор, оставленный предыдущей функцией. По воле обстоятельств он (мусор и указатель в одном лице) указывает на тот же самый buf_out и потому нарушения доступа не происходит,​ зато слово "​hello"​ выводится дважды.
 +
 +Будем рыть дальше,​ снимая со стека следующую последовательность адресов:​ 00408000h (указатель на строку "​hello,​ %s!\n"​),​ 0012FF3Ch (указатель на buf_out), 0012FF3Ch (снова он), 0040800Ch (указатель на строку "​введи имя:"​),​ 73257325h (содержимое буфера buf_in, трактуемое как указатель,​ между прочим указывающий на невыделенную ячейку памяти).
 +
 +Таким образом,​ первые пять спецификаторов "​%s"​ проходят сквозь интерпретатор форматного вывода вполне безболезненно,​ а вот шестой посылает его в "​космос"​. Процессор выбрасывает исключение и выполнение программы аварийно прекращается (см. рис. 2). Разумеется,​ спецификаторов не обязательно должно быть ровно шесть – до остальных все равно не дойдет управление. Обратите внимание,​ что Windows NT приводит именно тот адрес, который мы и планировали.
 +
 +{{buf.c.printf_Image_1.png?​553}}
 +
 +Рисунок 2 реакция программы на 6 спецификаторов %s
 +
 +==== реализация peek ====
 +
 +Для просмотра содержимого памяти уязвимой программы можно воспользоваться спецификаторами "​%X",​ "​%d"​ и "​%c"​. Спецификаторы "​%X"​ и "​%d"​ извлекают парное им двойное слово из стека и выводят его в шестнадцатеричном или десятичном виде соответственно. Спецификатор "​%c"​ извлекает парное двойное слово из стека, преобразует его к однобайтовому типу char и выводит в символьном виде, отсекая три старших байта. Таким образом,​ наиболее значимым из всех является спецификаторы "​%X"​ и "​%c"​.
 +
 +Каждый спецификатор "​%X"​ отображает всего лишь одно двойное слово, лежащее в непосредственной близости от вершины стека (точное расположение зависит от прототипа вызываемой функции). Соответственно,​ N спецификаторов отображают 4*N байт,​ а максимальная глубина просмотра равна – 2*C, где C – предельно допустимый размер пользовательского ввода в байтах. Увы! Читать всю память уязвимого приложения нам никто не даст, отдавая на растерзание лишь крошечный кусочек,​ в котором,​ если повезет,​ могут встретиться секретные данные (например,​ пароли) или указатели на них. Впрочем,​ узнать текущее положение указателя тоже неплохо. Но обо всем по порядку.
 +
 +Запустим нашу демонстрационную программу и введем спецификатор "​%X"​. Она ответит:​
 +
 +введи имя:%X
 +
 +hello,​ 12FF5C!
 +
 +Листинг 4 реакция программы на спецификатор %X
 +
 +Почему 12FF5C? Откуда оно взялось?​ Обращаясь к дампу памяти (см. рис.1), мы видим, что это – двойное слово, следующее за аргументом buf_out и представляющее собой результат жизнедеятельности предыдущей функции или попросту говоря мусор. Ну и какая нам радость от этого знания?​ Буфер содержит наш собственный ввод, в котором заведомо нет ничего интересного. Но это лишь часть айсберга. Как уже говорилось в статье,​ посвященной переполняющимся буферам,​ для передачи управления на shell-код необходимо знать его абсолютный адрес, который в большинстве случаев неизвестен и спецификатор "​%X"​ как раз и выводит его на экран!
 +
 +Теперь введем несколько спецификаторов "​%X",​ для удобства разделив их пробелами,​ хотя последнее и не обязательно. Программа ответит:​
 +
 +введи имя:​%X%X%X%X%X%X%X
 +
 +hello, 12FF5C 408000 12FF3C 12FF3C 40800C **25205825 58252058**!
 +
 +Листинг 5 просмотр дампа памяти с помощью спецификаторов
 +
 +Обратите внимание на два последних двойных слова, для наглядности выделенных в тексте жирным шрифтом. Да это же… содержимое буфера пользовательского ввода! (ACSII-строка "​%X "​ в шестнадцатеричном представлении выглядит как "​25 58 20"​).
 +
 +Идея – сформировать указатель на интересующую нас ячейку памяти,​ положить его в буфер, а затем натравить на него спецификатор "​%s",​ читающий память вплоть до встречи с нулевым байтом или запрещенной ячейкой. Нулевой байт – не помеха,​ достаточно сформировать новый указатель,​ расположенный за его хвостом. Запрещенные ячейки намного ​ коварнее – всякая попытка доступа к ним вызывает аварийное завершение программы,​ и до тех пор, пока администратор не поднимет упавший сервер,​ атакующему придется сидеть,​ пить пиво, смотреть анимэ, материться и скучать,​ а после перезапуска расположение уязвимых буфером данных оказаться совсем иным, что обесценит все ранее полученные результаты. Конечно,​ волков бояться – в лес не ходить,​ но и соваться в воду не зная броду тоже не стоит. В общем, со спецификатором "​%s"​ следует быть предельно осторожным,​ а то недолго и DoS схлопотать.
 +
 +Допустим,​ мы хотим прочитать содержимое памяти по адресу 77F86669h (по ней можно определить версию операционной системы,​ т. к. у всех она разная). Расположение буфера пользовательского ввода нам уже известно – актуальные данные начинаются с шестого двойного слова (см. листинг 3). Остается подготовить боевую начинку. Вводим целевой адрес, записывая его в обратном порядке и набирая непечатные символы с помощью <ALT> и цифровой клавиатуры,​ добавляем к ним шесть спецификаторов "​%X",​ "​%d"​ или "​%c"​ (поскольку,​ содержимое этих ячеек нас никак не волнует,​ подойдут любые),​ добавляем опознавательный знак, например,​ звездочку или двоеточие,​ за которым будет идти спецификатор вывода строки "​%s"​ и скармливаем полученный результат программе (опознавательный знак необходим для того, чтобы быстро определить – где кончается мусор, а где начинается актуальные данные):​
 +
 +введи имя:​if<​ALT-248>​w%с%с%с%с%с:​%s
 +
 +hello, if°w \ <<​♀:​**ЛF¶╟@►♥!**
 +
 +Листинг 6 просмотр дампа памяти по вручную сформированному указателю
 +
 +Если перевести "​**Л****F****¶╟@►♥**"​ в шестнадцатеричную форму, получится 8B 46 B3 40 3E B3 00. Откуда взялся нуль? Так ведь это ASII**Z**-строка,​ и нуль (по-английски Zero) служит ее завершителем. Если бы его здесь не оказалось,​ спецификатор "​%s"​ вывел бы на экран намного больше информации.
 +
 +Фактически мы реализовали аналог БЕЙСИК функции **peek**, судьбоносность которой уже обсуждалась в предыдущей статье,​ однако,​ не спешите открывать пиво на радостях. Данная реализация peek'​а очень ограничена в своих возможностях. Указатель,​ сформированный в начале буфера,​ не может содержать в себе символа нуля и потому первые 17 Мбайт адресного пространства недоступны для просмотра. Указатель,​ сформированный в конце буфера,​ может указывать практически на любой адрес, поскольку старший байт адреса удачно совпадает с символом завершающего нуля, однако,​ чтобы дотянуться до такого указателя,​ потребуется пересечь весь буфер целиком,​ а это не всегда возможно.
 +
 +Дизассемблер утверждает,​ что по адресу 004053B4h в нашей демонстрационной программе расположен копирайт фирмы Microsoft (см. листинг 7). Давайте выведем его на экран! Как мы помним,​ начало буфера соответствует шестому спецификатору. Каждый спецификатор занимает два байта и снимает со стека четыре. Еще два байта уходят на спецификатор "​%s",​ выводящий строку. Так сколько всего надо передать спецификаторов программе?​ Составляем простенькое линейное уравнение и сходу решаем его, получая в ответе двенадцать. Одиннадцать из них – выгребают со стека все лишнее,​ а двенадцатый выводит содержимое расположенного за ним указателя.
 +
 +
 +
 +.rdata:​004053B4 aMicrosoftVisua db '​Microsoft Visual C++ Runtime Library',​0
 +
 +Листинг 7 дизассемблерный фрагмент нашей тестовой программы
 +
 +Указатель формируется тривиально:​ открываем ASCII-таблицу символов (как вариант – запускаем HIEW) и переводим 4053B4h в символьное представление. Получается:​ "​@**S**┤"​. Выворачиваем его наизнанку и вводим в программу,​ при необходимости используя цифровую клавиатуру и клавишу ALT.
 +
 +введи имя:​%c%c%c%c%c%c%c%c%c%c%c%s<​Alt-180>​**S**@
 +
 +hello, \ <<​♀%%%%%%**Microsoft Visual C++ Runtime Library**┤S@!
 +
 +Листинг 8 формирование указателя в конце буфера и вывод его на экран
 +
 +Мы сделали это! У нас получилось! Действуя и дальше таким Макаром,​ мы сможем просмотреть практически всю доступную память программы! Кстати говоря,​ Unicode-функции,​ работающие с широкими (wide) символами,​ используют для завершения строки двойной символ нуля и к одиночным нулям относятся довольно лояльно.
 +
 +==== реализация poke ====
 +
 +Спецификатор "​%n"​ записывает в парный ему указатель количество выведенных на данный момент байт, тем самым позволяя нам модифицировать содержимое указателей по своему усмотрению. Обратите внимание:​ модифицируется не сам указатель,​ а то, на что он указывает! (Естественно,​ модифицируемая ячейка должна принадлежать странице с атрибутом PAGE_READWRITE,​ в противном случае процесс сгенерирует исключение).
 +
 +Перед демонстрацией нам необходимо найти в стековом хламе подходящий указатель,​ предварительно прочитав его содержимое строкой типа "​%X %X %X…"​ (см. листинг. 5). Допустим,​ мы выбрали 12FF3Ch, указывающий на буфер пользовательского ввода buf_in, для достижения которого необходимо снять со стека два двойных слова – этим займутся спецификаторы "​%c%c"​.
 +
 +Теперь определимся с числом,​ которое мы хотим записать. Записывать можно только маленькие числа, на большие просто не хватит размера буфера! Для определенности сойдемся на числе 0Fh (это нечетное число, четные приносят несчастье). Считаем:​ два символа выводят спецификаторы,​ снимающие лишние двойные слова с верхушки стека, семь приходится на строку "​hello,​ " (да! да! она тоже в доле!), тогда у нас остается:​ 0Fh – 02h – 07h == 06h. Шесть символов,​ которые мы должны ввести самостоятельно. Они могут быть любыми,​ например,​ "​qwerty"​ или что-то в этом роде. Остается добавить спецификатор "​%n"​ и сформированную строку можно передать программе:​
 +
 +введи имя:​qwerty%c%c**%n**
 +
 +hello, qwerty\ !
 +
 +Листинг 9 перезапись ячейки спецификатором %n
 +
 +Поскольку модификация буфера осуществляется //​**после**//​ его вывода на экран, доказательства перезаписи памяти приходится добывать в отладчике. Загрузив подопытную программу в MicrosoftVisualStudio (или любой другой отладчик по вашему вкусу),​ установите точку останова по адресу 401000 (адрес функции main) или подогнав к ней курсор (Ctrl+G, Address, "​401000",​ <​Enter>​) нажмите Ctrl+F10 для пропуска инструкций стартового кода, совершенно не интересующего нас в настоящий момент.
 +
 +Пошагово трассируя программу по F10 (Step Over – трассировка без захода внутрь функций),​ введите заданную строку,​ когда вас об этом попросят (экран консоли начнет призывно мигать) и продолжайте трассировку вплоть до достижения строки 0040103Сh, вызывающей функцию printf. Теперь перейдите в окно дампа памяти и введите в адресной строке "​ESP",​ сообщая отладчику,​ что нам угодно просмотреть содержимое стека, а затем вернитесь к дизассемблерному коду и нажиме F10 еще раз.
 +
 +Содержимое буфера пользовательского ввода немедленно изменится,​ подсвечивая ядовито-красным цветом число "​0F 00 00 00",​ записанное в его начале. Перезапись выбранной ячейки памяти успешно состоялась!
 +
 +{{buf.c.printf_Image_2.png?​553}}
 +
 +Рисунок 3 демонстрация перезаписи ячейки памяти
 +
 +Напоминаем,​ если спецификаторы перекрывают буфер пользовательского ввода, мы можем самостоятельно сформировать указатель,​ перезаписывая выбранные ячейки памяти произвольным образом. Ну… //​почти//,​ произвольным. К ограничениям выбора целевых адресов теперь еще присоединяется и ограничения выбора перезаписываемого значения,​ которые между прочим очень жестки.
 +
 +Нижняя граница определяется количеством уже выведенных символов (в данном случае длинны строки "​hello,​ "​),​ верхняя же формально неограниченна – достаточно лишь подобрать пару указателей на строки подходящей длины и натравить на них спецификаторы "​%s",​ однако,​ никакой гарантии того, что они там будут у нас нет и осуществить захват управления удаленной машиной с помощью форматированного вывода практически нереально. А вот DoS можно устроить хороший. Строка вида "​%n%n%n%n%n…"​ роняет систему покруче чем "​%s%s%s%s%…"​!
 +
 +==== дисбаланс спецификаторов ====
 +
 +Каждому спецификатору должен соответствовать парный аргумент. Но "​должен"​ еще не обозначает "​обязан"​. Ведь спецификаторы и аргументы программисту приходится набивать вручную и ему ничего не стоит ошибиться! Транслятор откомпилирует такую программу вполне нормально,​ возможно,​ негромко выругавшись при этом и выдав на экран предупреждающий warning (да только кто те warning'​и читает…). Но что произойдет потом?
 +
 +Если аргументов окажется больше,​ чем спецификаторов,​ "​лишние"​ аргументы будут проигнорированы,​ но вот если наоборот… функция форматированного вывода,​ не зная сколько ей аргументов реально передали,​ снимет со стека первый встретившийся ей мусор и события будут развиваться по сценарию,​ описанному в "​навязывание собственных спецификаторов",​ с той лишь разницей,​ что навязывать спецификаторы злоумышленник сможет только косвенно или не сможет совсем.
 +
 +Ошибки этого типа встречаются лишь в "​студенческих"​ программах,​ а потому совершенно не актуальны. Короче говоря,​ их описание не стоит переведенной бумаги.
 +
 +==== переполнение буфера-приемника ====
 +
 +Функция sprintf относится к числу самых опасных и все руководства по безопасности в один голос твердят,​ что лучше пользоваться ее безопасным аналогом – snprintf. Почему?​ Природа форматированного вывода такова,​ что предельно достижимую длину результирующей строки очень трудно рассчитать заранее. Рассмотрим следующий код:
 +
 +f()
 +
 +{
 +
 +char buf[???];
 +
 +sprintf(buf,"​имя:​%s возраст:​%02d вес:%03d рост:​%03d\n",​
 +
 +name, age, m, h);
 +
 +
 +
 +}
 +
 +Листинг 10 пример,​ демонстрирующий переполнение буфера-приемника
 +
 +Как вы думаете,​ буфер каких размеров нам потребуется?​ Из неизвестных факторов здесь присутствуют:​ длина строки name и "​длина"​ целочисленных переменных age, m, h, преобразуемых функцией sprintf в символьное представление. Кажется логичным,​ коль скоро мы отводим 2 столбца на возраст и по 3 на рост и вес, то за вычетом имени и длины форматной строки нам потребуется всего 8 байт. Правильно?​ А вот и нет! Если строковое представление переменных не умещается в отведенных их позициях,​ оно автоматически расширяется,​ дабы избежать усечения результата. В действительности же, десятичное представление 32-разрядных переменных типа int, требует резервирования 11 байт памяти,​ в противном случае возникает угроза переполнения буфера.
 +
 +Переполнения данного типа подчиняются общим правилам всех переполняющихся буфером и потому здесь не рассматриваются.
 +
 +===== заключение =====
 +
 +Ошибки обработки спецификаторов – частный случай более обшей проблемы интерполяции строк. Некоторые языки, например,​ Perl, позволяют не только форматировать вывод, но и внедрять переменные и даже функции (!) непосредственно в саму выводимую строку,​ что существенно упрощает и ускоряет программирование. К сожалению,​ хорошие идеи становится фундаментом воинствующего вандализма. Удобство не сочетается с безопасностью. Что удобно программировать – удобно и ломать,​ хотя обратное утверждение неверно.
 +
 +В общем, не воспринимайте языковые возможности как догму. Подходите к ним творчески,​ отбирая только лучше функции и операторы,​ а остальные – пошлите на хрен.
 +
 +