Flushall вытолкнуть буфера всех открытых файлов


Содержание

Описание функций C (Си) / C++ — flushall

Описание функций C (Си) / C++ — flushall

#include
int flushall();

Функция flushall записывает содержимое всех буферов, свя-
занных с открытыми input потоками, в соответствующие файлы. Все
буферы, связанные с открытыми потоками, очищаются; следующая опе-
рация чтения (если она есть) считывает новые данные из входных
файлов в буферы. После вызова функции flushall все потоки оста-
ются открытыми.

Данная функция возвращает количество открытых потоков
(входных и выходных). В случае ошибки возвращаемого значения нет.
См. также fflush.

Замечание! Буферы автоматически обновляются, когда они пол-
ны, когда потоки закрываются или происходит нормальное завершение
работы программы без закрытия потоков.

#include
int numopen;
.
.
.
/* следующий оператор ожидает любой ввод/вывод во все пото-
ки */
numopen=flushall();.

Буферизация stdout, работа fflush

Что делает fflush? Многие пишут,что эта функция дает команду ОС сбросить содержимое буфера на диск. Но разве этим занимается ОС? Насколько я понял при работе с файлами буфер создает сама программа, например, функция fopen. В таблице syscall-ов linux я не нашел чего-то похожего на flush. При этом в пример приводят вот такой код:

При запуске такой программы, hello напечатано не будет, если не добавить после printf вызов fflush(stdout). Однако вот такой код вполне нормально работает:

Тут мы не используем высокоуровневые методы работы с stdout, а работаем на уровне syscall-ов. Я правильно понимаю что буферизация работы с файлами реализована средствами самой программы, а не ОС? И при вызове write, данные сразу будут записаны на диск?

1 ответ 1

Что касается fflush, в сущности всё так и есть. Семейство функций для работы с потока в Си (fopen/fread/fprintf итп) fflush сбрасывает только оные буферы. Они же могут быть сброшены и раньше по исчерпанию (размер стандартного буфера BUFSIZ [8к для glibc/linux/x86]) или в зависимости от настроек буферизации (см. man 3 setbuf), в частности по-умолчанию для обычных файлов используется блочная буферизация (сбрасывается по 4к для glibc/linux), для терминалов (втч для stdout — строчная, сбрасывается по символу ‘\n’), для stderr буферизации нет.

Для Linux собственно во время сброса буфера происходит системный вызов write(), а что происходит с данными после сброса этого стандарт Си не гарантирует.

Из man 3 fflush:

ЗАМЕЧАНИЯ

Заметим, что fflush() сбрасывает буферы только пользовательского пространства, заданные библиотекой Си. Чтобы гарантировать, что данные действительно физически сохранены на диске, буферы ядра также должны быть сохранены, например, с помощью вызова sync(2) или fsync(2).

Что происходит с данными после write() зависит от того с каким файлом связан дескриптор и как он был открыт. Практически в любом случае ОС практически всегда проводит дополнительную буферизацию/кеширование. Очевидное исключение, например, запись в /dev/null; в принципе возможно существование устройств не буферизирующих ничего, а работающих напрямую с буфером из юзерспейса, но на вскидку я таких не назову

Не вдаваясь в детали в Linux’е для обычного файла на обычной дисковой ФС при не-синхронном вводе-выводе происходит следующее: ядро вносит изменения в кеше страниц и помечает их как грязные, затем возвращает управление процессу. Далее специальный демон (раньше был один pdflush (с несколькими потоками), ныне для каждого диска/блочного устройства/некоторых ФС свой) следит за грязными страницами и отдаёт команды драйверам ФС сохранить данные на диск при одном из условий:

  • Когда не хватает свободной памяти
  • Когда грязных страниц слишком много (см. /proc/sys/vm/dirty_),
  • По прошествии определённого времени (указанного в /proc/sys/vm/dirty_)
  • По вызову [f]sync()
  • Когда ещё какая-то умная эвристика сработала.

В этот момент ФС может преобразовать данные (например сжать или зашифровать). Далее создаётся запрос на вывод на блочное устройство. Когда драйвер жёсткого диска обработает этот запрос т.е. отправит данные непосредственно на устройство и получил подтверждение, что всё в порядке, страница снова отмечается, как чистая.

При синхронном вводе-выводе (когда файл открыт с O_SYNC или ФС смонтирована с -o sync) всё точно также, но управление процессу из write() не будет возвращено пока страница не отмечена как чистая.

Переполнение буфера: причины, эффективные методы решения проблемы и необходимая защита

Все программисты знают о потенциальной угрозе переполнения буфера (buffer) в своих программах. Существует много угроз, связанных с ним, как в новом, так и в старом ПО, независимо от количества выполненных исправлений. Злоумышленники могут воспользоваться такой ошибкой, внедрив код, специально предназначенный для того, чтобы вызвать переполнение начальной части набора данных, а затем записать оставшиеся в адрес памяти, смежный с переполненным.

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

Определение переполнения буфера

Прежде чем искать переполнение, нужно знать, что оно собой представляет. Как следует из названия, эти уязвимости связаны с буферами или выделением памяти в языках, обеспечивающих прямой низкоуровневый доступ к чтению и записи.

При применении языков C и Assembler чтение или запись таких распределений не влечет за собой автоматической проверки границ. В связи с чем, если обнаружено переполнение стекового буфера в данном приложении, не существует проверки на возможность помещения числа байтов в рассматриваемый буфер. В таких случаях программа может «переполнить» его емкость. Это приводит к тому, что данные, записываемые после наполнения, переписывают содержимое последующих адресов в стеке и считывают дополнительные. Переполнение может произойти непреднамеренно из-за ошибок пользователя.

Бывает, что оно вызвано тем, что злонамеренный субъект посылает тщательно созданный вредоносный ввод в программу, которая затем пытается сохранить его в недостаточный буфер. Если при этом будет обнаружено переполнение стекового буфера в данном приложении, избыточные данные записываются в соседний, где перезаписывают любые имеющиеся данные.

Обычно они содержат указатель возврата эксплуатируемой функции — адрес, по которому процесс должен перейти дальше. Злоумышленник может установить новые значения, чтобы они указывали на адрес по выбору. Атакующий обычно устанавливает новые значения, чтобы обозначить место, где расположена полезная нагрузка. Это изменяет путь выполнения процесса и мгновенно передает управление вредоносному коду.

Использование переполнения буфера позволяет злоумышленнику контролировать или завершать работу процесса либо изменять его внутренние переменные. Это нарушение занимает место в топ-25 наиболее опасных программных ошибок мира (2009 CWE/SANS Top 25 Most Dangerous Programming Errors) и определяется как CWE-120 в словаре перечислений слабых системных мест. Несмотря на то что они хорошо изучены, они продолжают наносить вред популярным программам.

Простой вектор использования буфера

При работе с исходным кодом нужно обратить особое внимание, где буферы используются и модифицируются. Особо следует отметить функции, относящиеся к вводу, предоставленному пользователем или другим внешним источником, поскольку они обеспечивают простой вектор для использования, когда обнаружено переполнение стекового буфера. Например, когда юзер задает вопрос «да» или «нет», целесообразно сохранить строковые данные пользователя в небольшом buffer для строки «да», как показано в следующем примере.

Глядя на код, видно, что проверка границ не выполняется. Если пользователь вводит «возможно», то программа будет аварийно завершать работу, а не запрашивать у него ответ, который записывается в buffer независимо от его длины. В этом примере, поскольку user answer является единственной объявленной переменной, следующие значения в стеке будут значением обратного адреса или местом в памяти, куда программа вернется после выполнения функции ask Question.

Это означает, что если юзер вводит четыре байта данных, что достаточно для переполнения буфера команд клиента, последует действительный адрес возврата, который будет изменен. Это заставит программу выйти из функции в другой точке кода, чем первоначально предполагалось, и может привести к тому, что ПО будет вести себя опасным и непреднамеренным образом.

Если первым шагом для обнаружения переполнения буфера в исходном коде является понимание того, как они работают, вторым этапом является изучение внешнего ввода и манипуляций с буфером, то третьим шагом будет необходимость узнать, какие функции подвержены этой уязвимости и какие могут действовать как «красные флаги». Функция gets отлично подходит для записи за пределами предоставленного ей buffer. На самом деле это качество распространяется на все семейство связанных возможностей, включая strcpy, strcmp и printf/sprintf, везде, где используется одна из этих функций уязвимости переполнения.

Удаление из кодовой базы

Если обнаружено переполнение стекового буфера в исходном коде, потребуется согласованное удаление их из базы. Для этого надо быть знакомым с безопасными методами работы. Самый простой способ предотвратить эти уязвимости — использовать язык, который их не допускает. Язык C имеет эти уязвимости благодаря прямому доступу к памяти и отсутствию строгой типизации объектов. Языки, не разделяющие эти аспекты, обычно неуязвимы. Это Java, Python и .NET, наряду с другими языками и платформами, не требующими специальных проверок или изменений.

Конечно, не всегда возможно полностью изменить язык разработки. В этом случае используют безопасные методы для работы с переполнением буфера команд. В случае функций обработки строк было много дискуссий о том, какие методы доступны, какие безопасны в использовании, а каких следует избегать. Функции strcpy и strcat копируют строку в буфер и добавляют содержимое одного в другой. Эти два метода демонстрируют небезопасное поведение, поскольку не проверяют границы целевого buffer, и выполняют запись за пределами, если для этого достаточно байтов.

Альтернативная защита

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

В этом упрощенном примере видна опасность строк, не оканчивающихся нулем. Когда foo помещается в normal buffer, он завершается нулем, поскольку имеет дополнительное место. Это лучший вариант развития событий. Если байты в переполнения буфера на стеке будут в другом символьном buffer или другой печатаемой строке, функция печати продолжить чтение, пока не будет достигнут завершающий символ этой строки.

Недостаток заключается в том, что язык C не предоставляет стандартную, безопасную альтернативу этим функциям. Тем не менее имеется и позитив — доступность нескольких реализаций для конкретной платформы. OpenBSD предоставляет strlcpy и strlcat, которые работают аналогично функциям strn, за исключением того, что они усекают строку на один символ раньше, чтобы освободить место для нулевого терминатора.

Аналогично Microsoft предоставляет свои собственные безопасные реализации часто используемых функций обработки строк: strcpy_s, strcat_s и sprintf_s.

Использование безопасных альтернатив, перечисленных выше, является предпочтительным. Когда это невозможно, выполняют ручную проверку границ и нулевое завершение при обработке строковых буферов.

Уязвимости компиляции

В случае если небезопасная функция оставляет открытую возможность переполнение буфера C, то не все потеряно. При запуске программы компиляторы часто создают случайные значения, известные как канарейки (canary), и помещают их в стек, поэтому представляют опасность. Проверка значения канарейки по отношению к ее первоначальному значению может определить, произошло ли переполнение буфера Windows. Если значение было изменено, программа будет закрыта или перейдет в состояние ошибки, а не к потенциально измененному адресу возврата.

Некоторые современные операционные системы предоставляют дополнительную защиту от переполнения буфера в виде неисполнимых стеков и рандомизации размещения адресного пространства (ASLR). Неисполняемые стеки — предотвращение выполнения данных (DEP) — помечают стек, а в некоторых случаях другие структуры как области, где код не будет выполнен. Это означает, что злоумышленник не может внедрить код эксплойта в стек и ожидать его успешного выполнения.

Перед тем как исправить переполнение буфера, распаковывают на ПК ASLR. Он был разработан для защиты от ориентированного на возврат программирования как обходной путь к неисполнимым стекам, где существующие фрагменты кода объединены в цепочку на основе смещения их адресов.

Он работает путем рандомизации областей памяти структур, так что их смещения сложнее определить. Если бы эта защита существовала в конце 1980-х годов, червя Морриса можно было бы не допустить. Это связано с тем, что он функционировал частично, заполняя буфер в протоколе UNIX finger кодом эксплойта, а затем переполнял его, чтобы изменить адрес возврата и указывал на заполненный буфер.

ASLR и DEP усложняют точное определение адреса, который нужно указать, выполняя эту область памяти полностью нерабочей. Иногда уязвимость проскальзывает сквозь трещины, открытые для атаки переполнения буфера, несмотря на наличие элементов управления на уровне разработки, компилятора или операционной системы.

Статический анализ покрытия

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

Нельзя предполагать, что такое обновление обеспечит достаточный охват. Организации или частные лица могут использовать программное обеспечение в системах с ограниченным доступом к интернету, требующих ручного обновления. Это означает, что новости об обновлении должны быть распространены среди любых администраторов, которые могут использовать ПО, а патч должен быть легкодоступен для загрузки. Создание и распространение исправлений выполняют как можно ближе к обнаружению уязвимости, что обеспечивает минимизацию времени уязвимости.

Благодаря использованию безопасных функций обработки буфера и соответствующих функций безопасности компилятора и операционной системы можно создать надежную защиту от переполнения buffer. С учетом этих шагов последовательная идентификация недостатков является решающим шагом для предотвращения эксплойта.

Комбинирование строк исходного кода в поисках потенциальных угроз может быть утомительным. Кроме того, всегда есть вероятность, что человеческие глаза могут пропустить что-то важное. Инструменты статического анализа используются для обеспечения качества кода, были разработаны специально для обнаружения уязвимости безопасности во время разработки.

Статический анализ покрытия устанавливает «красные метки» для потенциальных переполнений buffer. Затем их обрабатывают и исправляют отдельно, чтобы вручную не искать в базе. Эти инструменты в сочетании с регулярными проверками и знанием того, как устранить переполнения, позволяют выявлять и устранять подавляющее большинство недостатков до завершения разработки ПО.

Выполнение атаки через root

Ошибки кодирования обычно являются причиной переполнения buffer. Распространенные ошибки при разработке приложений, которые могут привести к нему, включают в себя неспособность выделить достаточно большие буферы и отсутствие механизма проверки этих проблем. Такие ошибки особенно проблематичны в языках C/C++, которые не имеют встроенной защиты от переполнения и часто являются объектами атак переполнения буфера.

В некоторых случаях злоумышленник внедряет вредоносный код в память, которая была повреждена из-за переполнения стекового буфера. В других случаях просто используют преимущества повреждения соседней памяти. Например, программа, которая запрашивает пароль пользователя для предоставления ему доступа к системе. В приведенном ниже коде правильный пароль предоставляет привилегии root. Если пароль неверный, программа не предоставляет юзеру привилегии.

В приведенном примере программа предоставляет пользователю привилегии root, даже если он ввел неверный пароль. В этом случае злоумышленник предоставляет вход, длина которого больше, чем может вместить буфер, создавая переполнение, перезаписывающего память целого числа pass. Поэтому, несмотря на неверный пароль, значение pass становится ненулевым, и злоумышленник получает права root.

Атака временной области хранения

Буфер представляет собой временную область для хранения данных. Когда программа или системный процесс размещает больше данных чем было изначально выделено для хранения, дополнительные переполняются. Это приводит к тому, что некоторые из них просачиваются в другие buffer, повреждают или перезаписывают данные.

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

Злоумышленник использует эксплойт с переполнением, чтобы воспользоваться программой, ожидающей ввода пользователя. Существует два типа переполнения buffer: на основе стека и кучи. Основанные на куче трудны для выполнения и наименее распространенные, при этом атакуют приложение, заполняя пространство, зарезервированное для программы.

Стек — пространство памяти, используемое для хранения пользовательского ввода. Такое переполнение чаще встречается у злоумышленников, использующих приложения.

Современные компиляторы обычно предоставляют возможность проверки переполнения во время компиляции/компоновки, но во время выполнения довольно сложно проверить эту проблему без какого-либо дополнительного механизма защиты обработки исключений.

Варианты работы программы:


  1. Ввод: 12345678 (8 байт), программа работает без сбоев.
  2. Ввод: 123456789 (9 байт), появится сообщение «Ошибка сегментации», программа завершается.

Уязвимость существует из-за переполнения, если пользовательский ввод argv превышает 8 байтов. Для 32-битной системы (4 байта) заполняют память двойным словом (32 бита). Размер символа составляет 1 байт, поэтому если запросить буфер с 5 байтами, система выделит 2 двойных слова (8 байтов). Вот почему при вводе более 8 байтов Buffer будет переполнен.

Подобные стандартные функции, которые технически менее уязвимы, существуют. Например, strncpy (), strncat () и memcpy (). Проблема с этими функциями заключается в том, что ответственность за определение размера буфера лежит на программисте, а не на компиляторе.

Каждый программист C/C++ должен знать проблему прежде чем начинать кодирование. Многие генерируемые проблемы в большинстве случаев могут быть защищены от переполнения.

Опасности в C/C++

Пользователи C должны избегать применения опасных функций, которые не проверяют границы, если они не уверены, что границы не будут превышены. Функции, которых следует избегать в большинстве случаев, чтобы обеспечить защиту, включают функции strcpy. Их следует заменить такими функциями, как strncpy. Следует избегать использования функции strlen, если пользователь уверен, что будет найден завершающий символ NIL. Семейство scanf (): scanf (3), fscanf (3), sscanf (3), vscanf (3), vsscanf (3) и vfscanf (3) — опасно для использования, его не применяют для отправки данных в строку без контроля максимальной длины, «формат% s» является особенно распространенным сбоем.

Официально snprintf () не является стандартной функцией C в классификации ISO 1990. Эти системы не защищают от переполнения буфера, они просто вызывают sprintf напрямую. Известно, что текущая версия Linux snprintf работает правильно, то есть фактически соблюдает установленную границу. Возвращаемое значение snprintf () также меняется.

Версия 2 спецификации Unix (SUS) и стандарт C99 отличаются тем, что возвращает snprintf (). Некоторые версии snprintf don’t гарантируют, что строка закончится в NIL, а если строка слишком длинная, она вообще не будет содержать NIL. Библиотека glib имеет g_snprintf () с последовательной семантикой возврата, всегда заканчивается NIL и, что наиболее важно, всегда учитывает длину буфера.

Переполнение буфера коммуникационного порта

Иногда последовательный порт сообщает о переполнении buffer. Эта проблема может быть вызвана несколькими факторами. К ним относятся скорость компьютера, скорость передачи используемых данных, размер FIFO последовательного порта и размер FIFO устройства, который передает данные на последовательный порт.

Управление потоком будет ждать, пока в буфере не появится определенное количество байтов, прежде чем процессор отправит сообщение или сигнал другому устройству для прекращения передачи. При более высоких скоростях передачи последовательный порт будет получать несколько байтов с момента достижения уровня управления потоком буфера и прекращения передачи прибора.

Эти дополнительные байты будут больше, если процесс с высоким приоритетом контролирует процессор цели в реальном времени. Поскольку процесс переполнения буфера коммуникационного порта имеет более высокий приоритет, чем прерывание VISA, процессор не будет предпринимать никаких действий, пока такой не будет завершен в реальном времени.

Настройки VISA и Windows по умолчанию для 16-байтового FIFO составляют 14 байтов, оставляя 2 байта в FIFO, когда устройство пытается отправить сообщение от источника. При более высоких скоростях передачи на медленных компьютерах возможно получить более 4 байтов в момент, когда последовательный порт запрашивает процессор, посылая сигнал о прекращении передачи.

Чтобы решить проблему, когда обнаружено переполнение стекового буфера в Windows 10, нужно открыть диспетчер устройств. Затем найти COM-порт, для которого изменяют настройки, и открыть свойства. Далее нажимают на вкладку «Дополнительно», появится ползунок, которым изменяют размер переполнения буфера обмена, чтобы UART быстрее включил управление потоком.

Значение по умолчанию в большинстве случаев достаточно. Однако если поступает ошибка переполнения buffer, уменьшают значение. Это приведет к тому, что большее количество прерываний будет отправлено процессору с замедлением байтов в UART.

Методы безопасной разработки

Методы безопасной разработки включают регулярное тестирование для обнаружения и устранения переполнения. Самый надежный способ избежать или предотвратить его — использовать автоматическую защиту на уровне языка. Другое исправление — проверка границ во время выполнения, которая предотвращает переполнение, автоматически проверяя, что данные, записанные в буфер, находятся в допустимых границах.

Облачная служба Veracode выявляет уязвимости кода, такие как переполнение buffer, поэтому разработчики устраняют их до того, как они будут использованы. Уникальная в отрасли запатентованная технология тестирования безопасности бинарных статических приложений (SAST) Veracode анализирует его, включая компоненты с открытым исходным кодом и сторонние, без необходимости доступа к нему.

SAST дополняет моделирование угроз и обзоры кода, выполняемые разработчиками, быстрее и с меньшими затратами обнаруживая ошибки и упущения в коде за счет автоматизации. Как правило, он запускается на ранних этапах жизненного цикла разработки ПО, поскольку проще и дешевле устранять проблемы, прежде чем приступать к производственному развертыванию.

SAST выявляет критические уязвимости, такие как внедрение SQL, межсайтовый скриптинг (XSS), ошибку переполнения буфера, необработанные состояния ошибок и потенциальные закоулки. Кроме того, двоичная технология SAST предоставляет полезную информацию, которая определяет приоритеты в зависимости от серьезности и предоставляет подробную инструкцию по исправлению.

Уязвимость переполнения buffer существует уже почти 3 десятилетия, но она по-прежнему обременительна. Хакеры по всему миру продолжают считать ее своей тактикой по умолчанию из-за огромного количества восприимчивых веб-приложений. Разработчики и программисты затрачивают огромные усилия для борьбы с этим злом IT-технологий, придумывая все новые и новые способы.

Основная идея последнего подхода заключается в реализации инструмента исправления, который делает несколько копий адресов возврата в стеке, а затем рандомизирует расположение всех копий в дополнение к количеству. Все дубликаты обновляются и проверяются параллельно, так что любое несоответствие между ними указывает на возможную попытку атаки и вызывает исключение.

Язык программирования Си для персонального компьютера (46 стр.)

Стандартные потоки: stdin, stdout, stdeir, stdaux, stdprn.

Когда программа начинает выполняться, автоматически открываются пять потоков. Эти потоки — стандартный ввод (stdin), стандартный вывод (stdout), стандартный вывод для сообщений об ошибках (stderr), стандартный последовательный порт (stdaux) и стандартное устройство печати (stdprn).

По умолчанию стандартный ввод/вывод и стандартный вывод сообщений об ошибках связывается с консольным терминалом.

Назначения по умолчанию для стандартного порта и стандартного устройства печати зависят от конфигурации аппаратуры компьютера; эти потоки обычно связываются с последовательным портом и принтером, но могут быть и не установлены в отдельных системах.

Следующие указатели на структуру типа. FILE определяются в файле stdio.h и могут использоваться в любом месте как указатели потоков:

extern FILE * stdin; — стандартный ввод

extern FILE * stdout; — стандартный вывод

extern FILE * stderr; — стандартный вывод сообщений об ошибках

extern FILE * sidaux; — стандартный порт

extern FILE * stdprn; — стандартное устройство печати

При запуске оттранслированной программы на выполнение можно использовать символы перенаправления в/в из командного языка MS-DOS ( или >> ) для переопределения стандартного ввода и вывода программы.

Можно переопределить stdin, stdout, stderr, stdaux или stdprn так, что они будут относиться к файлу на диске или устройству. Такие возможности предоставляет функция freopen.

Управление буферизацией потоков

Открытые файлы, для которых осуществляется высокоуровневый ввод/вывод, буферизуются по умолчанию, за исключением потоков stdin, stdout, stderr, stdaux, stdprn.

Потоки stderr и stdaux — не буферизованы. Если к ним применяется функция printf или scanf, создается временный буфер. Для обоих потоков может задаваться буферизация с помощью функций setbuf или setvbuf.

Буферизация для потоков stdin, stdout, stdprn выполняется следующим образом: буфер сбрасывается при его заполнении или когда вызванная библиотечная функция ввода/вывода завершает работу.

Использованием функции setbuf или setvbuf можно сделать поток небуферизованным или связать буфер с небуферизованным до этого потоком. Буфера, размещенные в системе, недоступны пользователю, кроме буферов, полученных с помощью setbuf или setvbuf.

Буфера должны иметь постоянный размер, равный константе BUFSIZ в stdio.h.

Если используется setvbuf, размер буфера устанавливает пользователь. Буфера автоматически сбрасываются при их наполнении, или когда связанный с буфером файл закрывается, или когда происходит нормальное завершение программы.

Можно сбросить буфера в произвольный момент времени, используя функции fflush и flushall. Функция fflush сбрасывает буфер одного заданного потока, а функция flushall сбрасывает буфера всех потоков, которые открыты и буферизованы в данный момент.

Закрытие потоков

Функции fclose и fcloseall закрывают поток или потоки. Функция fclose закрывает один заданный поток, fcloseall — все потоки, кроме потоков stdin, stdout, stderr, stdaux, stdprn.

Если программа не выполняет закрытия потоков, потоки автоматически закрываются, когда программа завершается неаварийно. Однако следует закрывать потоки по завершении работы с ними, так как число потоков, которые могут быть открыты одновременно, ограничено.

Чтение и запись данных

Функции ввода/вывода верхнего уровня позволяют передавать данные различными способами.

Операции чтения и записи в потоках начинаются с текущей. позиции в потоке, идентифицируемой как «file pointer»

(указатель файла) для потока. Указатель файла изменяется после выполнения операции чтения или записи.

Например, если Вы читаете один символ из потока, указатель файла продвигается на 1 байт, так что следующая операция начнется с первого несчитанного символа. Если поток открыт для добавления, указатель файла автоматически позиционируется на конец файла перед каждой операцией записи.

Поток, связанный с таким устройством, как консольный терминал, не имеет указателя файла. Программы, которые перемещают указатель файла (с помощью функции fseek), будут иметь в этом случае неопределенный результат.

Обнаружение ошибок

Когда происходит ошибка в операции с потоком, устанавливается в ненулевое значение флажок ошибки для потока. Можно использовать макроопределение ferror, чтобы определить, произошла ли ошибка.

После каждой ошибки флажок ошибки остается установленным до тех пор, пока не будет сброшен вызовом функции clearerr или rewind.

Функции вода/вывода нижнего уровня

Функция Краткое описание
close закрыть файл
creat создать файл
dup создать второй дескриптор (handle) для файла
dup2 переназначить дескриптор (handle) для файла
eof проверка на конец файла
lseek позиционирование указателя файла в заданное место
open открыть файл
read читать данные из файла
sopen открыть файл в режиме разделения
tell получить текущую позицию указателя файла
write записать данные в файл

Система программирования ТС предоставляет дополнительно следующие функции:

Функция Краткое описание
_creat создать файл
creatnew создать новый файл
creattemp создать новый файл
_ореn открыть файл
_read чтение данных из файла
_write запись данных в файл

Нижний уровень ввода и вывода не работает с буферизованными или форматированными данными. Для работы с файлами, открытыми посредством функции нижнего уровня, используется дескриптор файла (handle).

Для открытия файлов используются функции open и _ореn; В ОС MS/DOC версии 3.0 или выше может быть использована функция sopen для открытия файлов с атрибутами режима разделения файла.

функции нижнего уровня, в отличие от функций верхнего уровня, не требуют включения файла stdio.h. Тем не менее нескольких общих констант, определенных в файле stdio.h, как, например, признак конца файла EOF, могут оказаться полезными. Если программа использует эти константы, необходимо включить файл stdio.h.

Прототипы функций нижнего уровня содержатся в файле io.h.

Открытие файлов

Файл должен быть открыт функциями open, sopen или creat до выполнения первой операции ввода или вывода с использованием функций нижнего уровня для этого файла.

Файл может быть открыт для чтения, записи, или для чтения и записи, может быть открыт в текстовом или в двоичном режиме.

Файл fcntl.h должен быть включен при открытии файла, так как содержит определения для флагов, используемых в функции open. В некоторых случаях также должны быть включены файлы sys\types.h и sys\stat.h.


Перечисленные функции возвращают дескриптор файла, который используется при последующих операциях с файлом. При вызове одной из функций открытия файла необходимо возвращаемое функцией значение присвоить целочисленной переменной и потом использовать значение этой переменной, чтобы обращаться к открытому файлу.

9.6.2.2. Переопределение дескрипторов (handle)

Когда программа начинает выполняться, пять дескрипторов (handle), соответствующих стандартным вводу, выводу, выводу сообщений об ошибках, порту и устройству печати, уже назначены. Пользователь может использовать значения этих дескрипторов при вызове функций ввода/вывода нижнего уровня.

Каждый из этих дескрипторов соответствует одному из стандартных потоков, значения этих дескрипторов таковы:

поток значение дескриптора
stdin
stdout 1
stderr 2
stdaux 3
stdprn 4

Можно использовать эти дескрипторы файлов в программе без предварительного открытия этих файлов. Они автоматически открываются при запуске программы.

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

Flushall вытолкнуть буфера всех открытых файлов

рассматривая различные механизмы переполнения и обсуждая их возможные последствия, ранее мы не касались таких «организационных» вопросов, как, например, порядок размещения переполняющихся буферов, затираемых переменных и служебных структур данных в оперативной памяти. Теперь пришло время восполнить этот пробел.

в стеке…

Переполнения автоматических буферов наиболее часты и наиболее коварны. Часты – потому что размер таких буферов жестко (hardcoded) определяется еще на этапе компиляции, а процедура проверки корректности обрабатываемых данных зачастую отсутствует или реализована с грубыми ошибками. Коварны – потому что в непосредственной близости от автоматических буферов присутствует адрес возврата из функции, модификация которого позволяет злоумышленнику осуществить передачу управления на произвольный код.

Еще в стеке содержится указатель на фрейм (он же кадр) материнской функции, сохраняемый компилятором перед открытием фрейма дочерней функции. Вообще-то, оптимизирующие компиляторы, поддерживающие технологию «плавающих» фреймов, обходятся и без этого, используя регистр указатель вершины кадра как обычный регистр общего назначения, однако, даже поверхностный анализ обнаруживает большое количество уязвимых приложений с кадром внутри, так что этот прием атаки все еще остается актуальным. Модификация кадра стека срывает адресацию локальных переменных и аргументов материнской функции и дает возможность управлять ими по своему усмотрению. Установив кадр материнской функции на «свой» буфер, злоумышленник может засунуть в материнские переменные (аргументы) любые значения (в том числе и заведомо некорректные, поскольку проверка допустимости аргументов обычно выполняется до вызова дочерних функций, а корректность автоматических переменных после их инициализации проверяют только параноики). Внимание! Поскольку после возврата из дочерней функции все, принадлежащие ей, локальные переменные, автоматически освобождаются, использовать дочерний буфер для хранения материнских переменных нельзя (точнее, не рекомендуется, но если действовать осторожно – то можно). Обратитесь к куче, статической памяти или автоматической памяти параллельного потока, воздействуя на нее косвенным образом.

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

Дальше начинается область, «оккупированная» локальными переменными (и переполняющимся буфером в том числе). В зависимости от прихоти компилятора последний может быть расположен как наверху кадра стека, так и в гуще локальных переменных – это уже как повезет (или не повезет – с точки зрения жертвы). Переменные, находящиеся «ниже» переполняющегося буфера, могут быть затерты при последовательном переполнении – самом распространенном типе переполнения. Переменные, находящиеся «выше» переполняющегося буфера, затираются лишь индексным переполнением, которое чрезвычайно мало распространено.

Наконец, выше кадра стека находится только небо и звезды, пардон – свободное стековое пространство. Затирать тут особенно нечего и эта область памяти используется в основном для служебных нужд shell-кода. При этом следует учитывать, что: а) объем стека не безграничен и упирается в определенный лимит, так что выделять гигабайты памяти все-таки не стоит; б) если один из спящих объектов процесса-жертвы неожиданно проснется, содержимое свободной стековой памяти окажется искажено и чтобы этого не случилось, shell-код должен подтянуть регистр ESP к верхнему уровню, резервируя необходимое количество байт памяти; в) поскольку стековая память, принадлежащая потоку выделается динамически по мере его распухания, всякая попытка выхода за пределы сторожевой страницы (page guard) завершается исключением, поэтому либо не запрашивайте более 4 Кб, либо прочитайте хотя бы по одной ячейке из каждой резервируемой страницы, двигаясь снизу вверх. Подробнее об этом можно прочитать у Рихтера.

В зависимости от ограничений, наложенных на предельно допустимую длину переполняющегося буфера, могут затираться те или иные локальные переменные или служебные структуры данных. Очень может статься, что до адреса возврата просто не удастся «дотянуться», а даже если и удастся – не факт, что функция не грохнется задолго до своего завершения. Допустим, за концом строкового переполняющегося буфера располагается указатель, из которого после переполнения что-то читается (записывается). Поскольку, переполнение буфера неизбежно затирает указатель, всякая попытка чтения оттуда вызываем немедленное исключение и – как следствие – аварийное завершение программы. Причем, затереть адрес возврата, подсунув указателю корректный адрес, скорее всего не удастся, т. к. в операционных системах семейства Windows все, гарантированно доступные адреса, лежат значительно ниже 01010101h – наименьшего адреса, который только можно внедрить в середину строкового буфера (подробнее см. «запрещенные символы«). Так что буфера, распложенные внизу кадра стека, для переполнения все же предпочтительнее.

За концом адреса возврата начинается область памяти, принадлежащая материнским функциям и содержащая: аргументы дочерней функции, автоматические переменные материнской функции, сохраненные регистры/кадр стека проматеринской функции/адрес возврата в праматеринскую функции и т. д. Теоретически переполняющийся буфер может все это затереть (ну бывают же такие буйные буфера), практически же – это либо ненужно, либо неосуществимо. Если мы можем навязать программе корректный адрес возврата (т. е. адрес возврата указывающий на shell-код или любую точку «родного» кода программы), то в материнскую функцию она уже не вернется и все махинации с материнскими переменными останутся незамеченными. Если же навязать корректный адрес возврата по тем или иным причинам невозможно, то материнская функция тем более не сможет получить управления.

Намного большую информацию несет чтение материнской области памяти (см. «указатели и индексы» в предыдущей статье данного цикла) – здесь действительно можно встретить много чего интересного. Конфиденциальные данные (типа паролей и номеров кредитных карт), дескрипторы секретных файлов, которые невозможно открыть обычным образом, сокеты установленных TCP-соединений (почему бы их не использовать для обхода брандмаузеров?) и т. д.

Модификация аргументов дочерней функции менее перспективна, хотя временами и бывает полезной. Среди аргументов Си/Си++ программ традиционно много указателей. Обычно это указатели на данные, но встречаются и указатели на код. Последние наиболее перспективны, поскольку позволяют захватывать управление программой до ее обрушения. Указатели на данные, конечно, тоже хороши (особенно те из них, что позволяют записывать по навязанным адресам навязанные данные, т. е. работают как Бейсик-функция POKE), однако, чтобы дотянуться до своих аргументов при последовательном переполнении уязвимого буфера, необходимо пересечь ячейки памяти, занятые адресом возврата…

автоматические переменные дочерней функции

[кадр стека материнской функции]

адрес возврата в материнскую функци.

аргументы дочерней функции

автоматические переменные материнской функции

[кадр стека праматеринской функции]

адрес возврата в праматеринскую функци.

аргументы материнской функции

Рисунок 1 карта распределения стековой памяти

В затирании адреса возврата есть одна интересная тонкость: адрес возврата – это абсолютный адрес и, если мы хотим передать управление непосредственно на сам переполняющийся буфер, нам либо приходится надеяться на то, что в уязвимой программе переполняющийся буфер окажется по такому-то адресу (а это не факт), либо искать механизм передачи управления на вершину стека.

Червь Love San решает проблему путем подмены адреса возврата на адрес машинной инструкции JMP ESP, расположенной во владениях операционной системы. Недостатки такой методики очевидны: во-первых, она не срабатывает в тех случаях, когда переполняющийся буфер расположен ниже вершины стека, а, во-вторых, местоположение инструкции JMP ESP тесно связано с версией операционной системы и получается как в той поговорке «за что боролись, на то и напоролись». К сожалению, более прогрессивных методик передачи управления пока не придумано…

в куче…

Буфера, расположенные в динамической памяти, также подвержены переполнению. Многие программисты, ленивые от природы, сначала выделяют буфер фиксированного размера, а затем определяют сколько памяти им реально необходимо, причем ситуацию недостатка памяти обработать традиционно забывают. В куче чаше всего встречаются переполняющиеся буфера двух типов: элементы структур и динамически выделяемые блоки памяти.

Допустим, в программе имеется структура demo, содержащая в том числе и буфер фиксированного размера:

Неосторожное обращение с обрабатываемыми данными (например, отсутствие нужных проверок в нужном месте) может привести к возможности переполнения буфера buf и как следствие – затиранию расположенных за ним переменных. В первую очередь это переменные-члены самой структуры (в данном случае – переменная b), стратегия модификация которых вполне типична и подчиняется тем же правилам – общим для всем переполняющихся буферов. Менее очевидна возможность затирания ячеек памяти, лежащих за пределами выделенного блока памяти. Кстати, для буферов, монопольно владеющим всем выделенным блоком памяти, это единственно возможная стратегия переполнения вообще. Взгляните на следующий код. Как вы думаете, что здесь можно переполнить?

Долгое время считалось, что переполнять здесь особенно и нечего. Максимум – можно устроить банальный DoS, но целенаправленно захватить управление жертвой невозможно в силу хаотичности распределения динамических блоков по памяти. Базовый адрес блока pвообще говоря случаен и за его концом может быть расположено все, что угодно, в том числе и невыделенный регион памяти, всякое обращение к которому приводит к немедленному исключению, аварийно завершающим программу.

На самом деле, все это не более чем расхожие заблуждения. Сегодня переполнением динамических буферов никого не удивишь. Эта технология широко и небезуспешно используется в качестве универсального (!) средства захвата управления. Нашумевший червь Slapper – один из немногих червей, поражающий UNIX-машины – распространяется именно так. Как же такое возможно? Попробуем разобраться…

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

Существует множество реализаций динамической памяти и различные производители используют различные алгоритмы. Выделяемые блоки памяти могут быть нанизаны и на дерево, и на одно/двух связанный список, ссылки на который могут быть представлены как указателями, так и индексами, хранимыми либо в начале/конце каждого выделяемого блока, либо в отдельной структуре данных. Причем, последний способ реализации по ряду причин встречается крайне редко.

Рассмотрим следующую организацию динамической памяти, при которой все выделяемые блоки соединены посредством двухсвязных списков, указатели на которых расположены в начале каждого блока (см. рис. «2»), причем смежные блоки памяти не обязательно должны находиться в соседних элементах списка, т. к. в процессе многократных операций выделения/освобождения список неизбежно фрагментируется, а постоянно дефрагментировать его себе дороже.

указатель на следующий блок в цепочке

указатель на предыдущий блок в цепочке

память, выделенная блоку

указатель на следующий блок в цепочке

указатель на предыдущий блок в цепочке

память, выделенная блоку

Рисунок 2 карта приблизительного распределения динамической памяти

Переполнение буфера приводит к затиранию служебных структур следующего блока памяти и как следствие – возможности их модификации. Но что это нам дает? Ведь доступ к ячейкам всякого блока осуществляется по указателю, возращенному программе в момент его выделения, а отнюдь не по «служебному» указателю, который мы собираемся затирать! Служебные указатели используются исключительно функциями malloc/free (и другим подобными им функциям). Искажение указателя на следующий/предыдущий блок позволяет навязать адрес следующего выделяемого блока, например, «наложив» его на доступный нам буфер, но никаких гарантий, что это получится у нас нет – при выделении блока памяти, функция malloc ищет наиболее подходящий с ее точки зрения регион свободной памяти (обычно это первый свободный блок в цепочке, совпадающий по размеру с запрошенным), и не факт, что наш регион ей подойдет. Короче говоря, не воодушевляющая перспектива получается.

Освобождение блоков памяти – другое дело! Для уменьшения фрагментации динамической памяти функция free автоматически объединят текущий освобождаемый блок со следующим, если тот тоже свободен. Поскольку, смежные блоки могут находится на различных концах связывающего их списка, перед присоединением чужого блока памяти, функция free должна «выбросить» его из цепочки. Это осуществляется путем склейки предшествующего и последующий указателей, что в псевдокоде выглядит приблизительно так: *указатель на следующий блок в цепочке = указатель на предыдущий блок в цепочке. Постойте, но ведь в это… Да! Это аналог бейсик-функции POKE, позволяющий нам модифицировать любую ячейку уязвимой программы!

Подробнее об этом можно прочитать в статье «Onceuponafree()…», опубликованной в 39h-номере электронного журнала PHRACK, доступного по адресу www.phrack.org. Статья перегружена техническими подробностями реализации динамической памяти в различных библиотеках и написана довольно тяжелым языком, но ознакомится с ней безусловно стоит.

Как правило, возможность записи в память используется для модификации таблицы импорта с целью подмены некоторой API-функции гарантированно вызываемой уязвимой программой вскоре после переполнения («вскоре» потому что часы ее уже сочтены – целостность ссылочного каркаса динамической памяти нарушена и это неустойчивое сооружение в любо момент может рухнуть, пустив программу в разнос). К сожалению, передать управление на переполняющийся буфер скорее всего не удастся, т. к. его адрес наперед неизвестен и тут приходится импровизировать. Во-первых, злоумышленник может разместить shell-код в любом другом доступном ему буфере с известным адресом (см. «в секции данных…«). Во-вторых, среди функций уязвимой программы может встретиться и такие, что передают управление на указатель, переданный им с тем или иным аргументом (условимся называть такую функцию функцией f). После чего останется найти API-функцию, принимающую указатель на переполняющийся буфер и подменить ее адрес адресом функции f. В Си++ программах с их виртуальными функциями и указателями this такая ситуация случается не так уж и редко, хотя и распространенной ее тоже не назовешь. Но при проектировании shell-кода на универсальные решения закладываться вообще говоря и не приходиться. Проявите инженерную смекалку, удивите мир!

Будьте заранее готовы к тому, что в некоторых реализациях кучи вы встретитесь не с указателями, а с индексами, в общем случае представляющие собой относительные адреса, отсчитываемые либо от первого байта кучи, либо от текущей ячейки памяти. Последний случай встречается наиболее часто (в частности, штатная библиотека компилятора MSVC 6.0 построена именно так), поэтому имеет смысл рассмотреть его поподробнее. Как уже говорилось выше, абсолютные адреса переполняющего буфера наперед неизвестны и непредсказуемым образом изменяются под воздействием ряда обстоятельств. Адреса же ячеек, наиболее соблазнительных для модификации, напротив, абсолютны. Что делать? Можно, конечно, исследовать стратегию выделения/освобождения памяти для данного приложения на предмет выявления наиболее вероятных комбинаций – кое-какие закономерности в назначении адресов переполняющимся буферам безусловно есть. Методично перебирая все возможные варианты один за другим, атакующий рано или поздно захватит управление сервером (правда, перед этим несколько раз его завесит, демаскируя атаку и усиливая бдительность администраторов).

в секции данных…

Переполняющиеся буфера, расположенные в секции данных (статические буфера) –настоящая золотая жила с точки зрения злоумышленника! Это единственный тип буферов, адреса которых явно задаются еще на этапе компиляции (вообще-то, не компиляции, а компоновки, но это уже детали) и постоянны для каждой конкретной версии уязвимого приложения независимо от того, на какой операционной системе она выполняется.

Самое главное – секция данных содержит огромное количество указателей на функции/данные, глобальные флаги, дескрипторы файлов и кучи, имена файлов, текстовые строки, буфера некоторых библиотечных функций… Правда, до всего этого богатства еще предстоит «дотянуться» и, если длина переполняющегося буфера окажется жестко ограничена сверху (как часто и случается), атакующий не получит от последнего никаких преимуществ!

К тому же, если стек и куча гарантированно содержат указатели в определенных местах и поддерживают более или менее универсальные механизмы захвата управления, то в случае со статическими буферами атакующему остается надеяться лишь на удачу. А удача, как известно, баба подлая и переполнения статических буферов носят единичный характер и всегда развиваются по уникальному сценарию, не допускающему обобщающей классификации.

секреты проектирования shell-кода

Попытка реализовать собственный shell-код неминуемо натыкает атакующего на многочисленные ограничения, одни из которых обходятся путем хитроумных хаков и извращений, с другими же приходится мириться, воспринимая их как неотъемлемую часть жестоких сил природы.

запрещенные символы

Строковые переполняющиеся буфера (в особенности те, что относятся к консольному вводу и клавиатуре) налагают жесткие ограничения на ассортимент своего содержимого. Самое неприятное ограничение заключается в том, что символ нуля на всем протяжении строки может встречаться лишь однажды и лишь на конце строки (правда, это ограничение не распространяется на UNICODE-строки). Это затрудняет подготовку shell-кода и препятствует выбору произвольных целевых адресов. Код, не использующих нулевых байт, приятно называть ZeroFree кодом и техника его подготовки – настоящая Кама-Сутра.

Искусство затирания адресов. Рассмотрим ситуацию, когда следом за переполняющимся буфером идет уязвимый указатель на вызываемую функцию (или указатель this), а интересующая злоумышленника функция root располагается по адресу 00401000h. Поскольку, только один символ, затирающий указатель, может быть символом нуля, то непосредственная запись требуемого значения невозможна и приходится хитрить.

Начнем с того, что в 32-разрядных операционных системах (к которым, в частности, принадлежит Windows NT и многие клоны UNIX’а) стек, данные и код большинства приложений лежат в узком диапазоне адресов: 00100000h

00×00000h, т. е. как минимум один ноль у нас уже есть – и это старший байт адреса. В зависимости от архитектуры процессора он может располагаться как по младшим, так и по старшим адресам. Семейство x86-процессоров держит его в старших адресах, что с точки зрения атакующего, очень даже хорошо, поскольку мы можем навязать уязвимому приложению любой XxYyZzh адрес, при условии, что Xx, Yy и Zz не равны нулю.

Теперь давайте рассуждать творчески: позарез необходимый нам адрес 401000h в прямом виде недостижим в принципе. Но, может быть, нас устроит что-нибудь другое? Например, почему бы не начать выполнение функции не с первого байта? Функции с классическим прологом (коих вокруг нас большинство) начинаются с инструкции PUSH EBP, сохраняющей значение регистра EBP в стеке. Если этого не сделать, то при выходе функция непременно грохнется, но… это уже будет неважно (свою миссию функция выполнила и все, что было нужно атакующему она выполнила). Хуже, если паразитный символ нуля встречается в середине адреса или присутствует в нем дважды, например – 50000h.

В некоторых случаях помогает способ коррекции существующих адресов. Допустим, затираемый указатель содержит адрес 5000FAh. Тогда, для достижения желаемого результата атакующий должен затереть один лишь младший символ адреса, заменив FAh символом нуля.

Как вариант можно попробовать поискать в дизассемблерном листинге команду перехода (вызова) интересующей нас функции, – существует вероятность, что она будет располагаться по «правильным» адресам. При условии, что целевая функция вызывается не однажды, и вызовы следуют из различных мест (а обычно именно так и бывает), вероятность, чтобы хотя бы один из адресов нам «подойдет» весьма велика.

Следует также учитывать, что некоторые функции ввода не вырезают символ перевода каретки из вводимой строки, чем практически полностью обезоруживают атакующих. Непосредственный ввод целевых адресов становится практически невозможным (ну что интересного можно найти по адресу 0AXxYyh?), коррекция существующих адресов хотя и остается возможной, но на практике встретить подходящий указатель крайне маловероятно (фактически мы ограничены лишь одним адресом ??000A, где ?? прежнее значение уязвимого указателя). Единственное, что остается – полностью затереть все 4-байта указателя вместе с двумя последующими за ним байтами. Тогда, мы сможем навязать уязвимому приложению любой FfXxYyZz, где Ff > 00h. Этот регион обычно принадлежит коду операционной системы и драйверам. С ненулевой вероятностью здесь можно найти машинную команду, передающую управление по целевому адресу. В простейшем случае это CALL адрес/JMP адрес (что достаточно маловероятно), в более общем случае – CALL регистр/JMP регистр. Обе – двухбайтовые команды (FF Dx и FF Ex соответственно) и в памяти таких последовательностей сотни! Главное, чтобы на момент вызова затертого указателя (а, значит, и на момент передачи управления команде CALL регистр/JMP регистр) выбранный регистр содержал требуемый целевой адрес.

Штатные функции консольного ввода интерпретируют некоторые символы особым образом (например, символ с кодом 008 удаляет символ, стоящий перед курсором) и они [censored] еще до попадания в уязвимый буфер. Следует быть готовым и к тому, что атакуемая программа контролирует корректность поступающих данных, откидывая все нетекстовые символы или (что еще хуже) приводит их к верхнему/нижнему регистру. Вероятность успешной атаки (если только это не DoS атака) становится исчезающе мала.

Подготовка shell-кода. В тех случаях, когда переполняющийся строковой буфер используется для передачи двоичного shell-кода (например, головы червя), проблема нулевых символов стоит чрезвычайно остро – нулевые символы содержатся как в машинных командах, так и на концах строк, передаваемых системных функциям в качестве основного аргумента (обычно это «cmd.exe» или «/bin/sh»).


Для изгнания нулей из операндов машинных инструкций следует прибегнуть к адресной арифметике. Так, например, MOV EAX,01h (B8 00 00 00 01) эквивалентно XOR EAX,EAX/INC EAX (33 C0 40). Последняя записи, кстати, даже короче. Текстовые строки (вместе с завершающим нулем в конце) так же могут быть сформированы непосредственно на вершине стека, например:

Как вариант, можно воспользоваться командой XOR EAX,EAX/MOV [XXX], EAX, вставляющей завершающий нуль в позицию XXX, где XXX адрес конца текстовой строки, вычисленный тем или иным способом (см. «в поисках самого себя«).

Более радикальным средством предотвращения появления нулей является шифровка shell-кода, в подавляющем большинстве случаев сводящаяся к тривиальному XOR. Основную трудность представляет поиск подходящего ключа шифрования – ни один шифруемый байт не должен обращаться в символ нуля. Поскольку, aXORa == 0, для шифрования подойдет любой байтовый ключ, не совпадающий ни с одним байтом shell-кода. Если же в shell-коде присутствует полный набор всех возможных значений от 00h до FFh, следует увеличить длину ключа до слова и двойного слова, выбирая ее так, чтобы ни какой байт накладываемой гаммы не совпадал ни с одним шифруемым байтом. А как построить такую гамму (метод перебора не предлагать)? Да очень просто – подсчитываем частоту каждого из символов shell-кода, отбираем 4 символа, которые встречаются реже всего, выписываем их смещения относительно начала shell-кода в столбик и вычисляем остаток от деления на 4. Вновь записываем полученные значения в столбик, отбирая те, которые в нем не встречаются – это и будут позиции данного байта в ключе. Непонятно? Не волнуйтесь, сейчас все это разберем на конкретном примере.

Допустим, в нашем shell-кода наиболее «низкочастотными» оказались символы 69h, ABh, CCh, DDh встречающиеся в следующих позициях:

После вычисления остатка от деления на 4 над каждым из смещений, мы получаем следующий ряд значений:

Мы получили четыре ряда данных, представляющих собой позиции наложения шифруемого символа на гамму, в которой он обращается в нуль, что недопустимо, поэтому нам необходимо выписать все значения, которые не встречаются в каждом ряду данных:

Теперь из полученных смещений можно собрать гамму, комбинируя их таким образом, чтобы каждый символ встречался в гамме лишь однажды. Смотрите, символ DDh может встречаться только в позиции 00h, символ CCh – только в позиции 03h, а два остальных символа – в любой из оставшихся позиций. То есть это будет либо DDh ABh 69h ССh, либо DD 69h ABh 69h. Если же гамму собрать не удается – необходимо увеличить ее длину. Разумеется, выполнять все расчеты вручную совершенно необязательно и эту работу можно переложить на компьютер.

Естественно, перед передачей управления на зашифрованный код он должен быть в обязательном порядке расшифрован. Эта задача возлагается на расшифровщик, к которому предъявляются следующие требования: он должен быть а) по возможности компактным, б) позиционно независимым (т. е. полностью перемещаемым) и в) не содержать в себе символом нуля. В частности, червь Love San поступает так:

вчера были большие, но по пять…
или размер тоже имеет значение!

По статистике габариты подавляющего большинства переполняющихся буферов составляет 8 байт. Значительно реже переполняются буфера, вмещающие в себя от 16 до 128 (512) байт, а буферов больших размеров в живой природе практически не встречаются.

Закладываясь на худший из возможных вариантов (а в боевой обстановке атакующим приходится действовать именно так!), учитесь выживать даже в жесточайших условиях окружающей среды с минимум пиши, воды и кислорода. В крошечный объем переполняющегося буфера можно вместить очень многое, если подходить ко всякому делу творчески и думать головой.

Первое (и самое простое), что пришло нашим хакерским предкам в голову – это разбить атакующую программу на две неравные части – компактную голову и протяжный хвост. Голова обеспечивает следующие функции: переполнение буфера, захват управления и загрузку хвоста. Голова может нести двоичный код, но может обходиться и без него, осуществляя всю диверсионную деятельность руками уязвимой программы. Действительно, многие программы содержат большое количество служебных функций, дающих полный контроль над системой или, на худой конец, позволяют вывести себя из строя и пойти в управляемый разнос. Искажение одной или нескольких критических ячеек программы, ведет к ее немедленному обрушению и количество искаженных ячеек начинает расти как снежный ком. Через длинную или короткую цепочку причинно-следственных последствий в ключевые ячейки программы попадают значения, необходимые злоумышленнику. Причудливый узор мусорных байт внезапно складывается в законченную комбинацию, замок глухо щелкает и дверцы сейфа медленно раскрываются. Это похоже на шахматную головоломку с постановкой мата в N ходов, причем состояние большинства полей неизвестно, поэтому сложность задачи быстро растет с увеличением глубины N.

Конкретные примеры головоломок привести сложно, т. к. даже простейшие из них занимают несколько страниц убористого текста (в противном же случае листинги выглядят слишком искусственно, а решение лежит буквально на поверхности). Интересующиеся могут обратиться к коду червя Slapper, до сих пор остающимся непревзойденным эквилибристом по глубине атаки и детально проанализированным специалистами копании Symantec, отчет которых можно найти на их же сайте (см. «An Analysis of the Slapper Worm Exploit»).

Впрочем, атаки подобного типа скорее относятся к экзотике интеллектуальных развлечений, чем к практическим приемам вторжения в систему и потому чрезвычайно мало распространены. В плане возвращения к средствам традиционной «мануальной терапии», отметим, что если размер переполняющегося буфера равен 8 байтам, отсюда еще не следует, что и длина shell-кода должна быть равна тем же 8 байтам. Ведь это же переполняющийся буфер! Но не стоит бросаться и в другую крайность, надеяться, что предельно допустимая длина shell-кода окажется практически неограниченной. Подавляющее большинство уязвимых приложений содержат несколько уровней проверок корректности пользовательского ввода, которые будучи даже не совсем правильно реализованными, все-таки налагают определенные, под час весьма жесткие, ограничения на атаку.

Если в куцый объем переполняющегося буфера вместить загрузчик никак не удается, атакующий переходит к плану «B», заключающемуся в поиске альтернативных способов передачи shell-кода. Допустим, одно из полей пользовательского пакета данных допускает переполнение, приводящее к захвату управления, но его размер катастрофически мал. Но ведь остальные поля тоже содержаться в оперативной памяти! Так почему бы не использовать их для передачи shell-кода? Переполняющийся буфер, воздействуя на систему тем или иным образом, должен передать управление не на свое начало, а на первый байт shell-кода, если конечно, атакующий знает, относительный или абсолютный адрес последнего в памяти. Поскольку, простейший способ передачи управления на автоматические буфера сводится к инструкции JMP ESP, то наиболее выгодно внедрять shell-код в те буфера, которые расположены в непосредственной близости от вершины стека, в противном случае ситуация рискует самопроизвольно выйти из под контроля и для создания надежно работающего shell-кода атакующему придется попотеть. Собственно говоря, shell-код может находится в самых неожиданных местах, например, в хвосте последнего TCP-пакета (в подавляющем большинстве случаев он попадает в адресное пространство уязвимого процесса, причем зачастую располагается по более или менее предсказуемым адресам).

В более сложных случаях shell-код может быть передан отдельным сеансом, – злоумышленник создает несколько подключений к серверу, по одному передается shell-код (без переполнения, но в тех полях, размер которых достаточен для его вмещения), а другому –запрос, вызывающий переполнение и передающий управление на shell-код. Дело в том, что в многопоточных приложениях локальные стеки всех потоков располагаются в едином адресном пространстве процесса и их адреса назначаются не хаотичным, а строго упорядоченным образом. При условии, что между двумя последними подключениями, установленными злоумышленником, к серверу не подключился кто-то еще, «трас-поточное» определение адресов представляет собой хоть и сложную, но вполне разрешимую проблему.

в поисках самого себя

Предположим, что shell-код наделен сознанием (хотя это и не так). Что бы мы ощутили оказавшись на его месте? Представьте себе, что вы диверсант-десантник которого выбрасывают куда-то в пустоту. Вас окружает враждебная территория и еще темнота. Где вы? В каком месте приземлились? Рекогносцировка на местности (лат. recognoscere [рассматривать] – разведка с целью получения сведений о расположении противника, его огневых средствах, особенностях местности, где предполагаются боевые действия, и т. п. проводимая командирами или офицерами штаба перед началом боевых действий) и будет вашей первой задачей (а если вас занесет в болото, то и последней тоже).

Соответственно, первой задачей shell-кода является определение своего местоположения в памяти или более строго говоря, текущего значения регистра указателя команд (в, частности, в x86-процессорах это регистр EIP).

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

Использование абсолютной адресации (или, говоря другими словами, жесткой привязки к конкретным адресам, вроде MOV EAX, [406090h]) ставит shell-код в зависимость от окружающей среды и приводит к многочисленным обрушениям уязвимых приложений, в которых буфер оказался не там, где ожидалось. «Из чего только делают современных хакеров, что они даже переполнить буфер, не угробив при этом систему, оказываются не в состоянии?» вздыхает прошлое поколение. Чтобы этого не происходило, shell-код должен быть полностью перемещаемым – т. е. уметь работать в любых, заранее ему неизвестных адресах.

Поставленную задачу можно решить двумя путями – либо использовать только относительную адресацию (что на x86-платформе в общем-то недостижимо), либо самостоятельно определять свой базовый адрес загрузки и вести «летоисчисление» уже от него. И тот, и другой способ рассматриваются ниже, с характерной для хакеров подробностью и обстоятельностью.

Семейство x86-процессоров с относительной адресаций категорически не в ладах и разработка shell-кода для них – это отличная гимнастика для ума и огромное поле для всевозможных извращений. Всего имеется две относительных команды (CALL и JMP/Jx с опкодами E8h и Ebh,E9h/7xh,0F 8xh соответственно) и обе – команды управления. Непосредственное использование регистра EIP в адресных выражениях запрещено.

Использование относительных CALL’ов в 32-разрядном режиме имеет свои трудности. Аргумент команды задается знаковым 4-байтовым целым, отсчитываемым от начала следующей команды и, при вызове нижележащих подпрограмм, в старших разрядах содержащих одни нули. А, поскольку, в строковых буферах символ нуля может встретиться лишь однажды, такой shell-код просто не сможет работать. Если же заменить нули на что-то другое, можно совершить очччень далекий переход, далеко выходящий за пределы выделенного блока памяти.

Уж лучше прыгать назад – в область младших адресов, тогда нули волшебным образом превратятся в символы с кодом FFh (которые, кстати говоря, так же относятся к категории «трудных» символов, которые соглашаются проглотить далеко не все уязвимые программы). Применив военную хитрость и засадив в инструкцию префикс 66h, мы не только сократим длину машинной команды на один байт (что в ряде случаев весьма актуально), но и оторвем два старших байта операнда (те, которые были с нулевыми символами).

В машинном виде все это выглядит приблизительно так:

Сказанное справедливо и по отношению к команде JMP, за тем лишь исключением, что команды условного перехода (равно как и команда JMP SHORT) размешают свой адрес перехода в одном-единственном байте, что не только усиливает компактность кода, но и избавляет нас от проблемы «трудных» символов.

Если же необходимо совершить переход по абсолютному адресу (например, вызвать некоторую системную функцию или функцию уязвимой программы) можно воспользоваться конструкцией CALL регистр/JMP регистр, предварительно загрузив регистр командой MOV регистр, непосредственный операнд (от нулевых символов можно избавиться с помощью команд адресной арифметики) или командой CALL непосредственный операнд с опкодом FF /2, 9A или FF /3 для ближнего, дальнего и перехода по операнду в памяти соответственно.

Относительная адресация данных (в т. ч. и самомодифицирующегося кода) обеспечивается на порядок сложнее. Все, имеющиеся в нашем распоряжении команды, адресуются исключительно относительно регистра-указателя верхушки стека (в x86 процессорах это регистр ESP), что, конечно, довольно привлекательно само по себе, но и таит определенную внутреннею опасность. Положение указателя стека после переполнения в общем случае не определено и наличие необходимого количества стековой памяти не гарантировано. Так что действовать приходится на свой страх и риск.

Стек можно использовать и для подготовки строковых/числовых аргументов системных функций, формируя их командой PUSH и передавая через относительный указатель ESP + X, где X может быть как числом, так и регистром. Аналогичным образом осуществляется и подготовка самомодифицирующегося кода – мы «пушим» код в стек и модифицируем его, отталкиваясь от значения регистра ESP.

Любители же «классической миссионерской» могут пойти другим путем, определяя текущую позицию EIP посредством конструкции CALL $ + 5/RET, правда в лоб такую последовательность машинных команд в строковой буфер не передать, т. к. 32-раязрдярый аргумент команды CALL содержат несколько символов нуля. В простейшем случае они изгоняются «заклинаниям» 66 E8 FF FF C0, которое эквивалентно инструкциям CALL $‑3/INC EAX наложенным друг на друга (естественно, это может быть не только EAX и не только INC). Затем лишь остается вытолкнуть содержимое верхушки стека в любой регистр общего назначения, например, EBP или EBX. К сожалению, без использования стека здесь не обойтись и предлагаемый метод требует, чтобы указатель вершины стека смотрел на выделенный регион памяти, доступной на запись. Для перестраховки (если переполняющийся буфер действительно срывает стек на хрен) регистр ESP рекомендуется инициализировать самостоятельно. Это действительно очень просто сделать, ведь многие из регистровых переменных уязвимой программы содержат предсказуемые значения, точнее – используются предсказуемым образом. Так, в Си++ программах ECX наверняка содержит указатель this, а this это не только ценный мех, но и как минимум 4 байта доступной памяти!

В порядке дальнейшего развития этой идеи отметим, что не стоит, право же, игнорировать значения регистров, доставшихся shell-коду в момент начала его выполнения. Многие из них указывают на полезные структуры данных и выделенные регионы памяти, которые мы гарантированно можем использовать, не рискуя нарваться на исключение и прочие неожиданные неприятности. Некоторые регистровые переменные чувствительны к версии уязвимого приложения, некоторые – к версии компилятора и ключам компиляции, так что «гарантированность» эта очень и очень относительна, впрочем, как и все сущее на земле.

техника вызова системных функций

Возможность вызова системных функций, строго говоря, не является обязательным условием успешности атаки, поскольку все необходимое для атаки жертва (уязвимая программа) уже содержит внутри себя, в том числе и вызовы системных функций вместе с высокоуровневой оберткой прикладных библиотек вокруг них. Дизассемблировав исследуемое приложение и определив целевые адреса интересующих нас функций, мы можем сделать CALL целевой адрес или PUSH адрес возврата/JMP относительный целевой адрес или MOV регистр, абсолютный целевой адрес/PUSH адрес возврата/JMP регистр.

Замечательно, если уязвимая программа импортирует пару функций LoadLibrary/GetProcAddress, – тогда shell-код сможет загрузить любую динамическую библиотеку и обратиться к любой из ее функций. А если функции GetProcAddress в таблице импорта нет? Тогда – атакующий будет вынужден самостоятельно определять адреса интересующих его функций, отталкиваясь от базового адреса загрузки, возращенным LoadLibrary и действуя либо путем «ручного» разбора PE-файла, либо отождествляя функции по их сигнатурам. Первое сложно, второе – ненадежно. Закладывается на фиксированные адреса системных функций категорически недопустимо, поскольку они варьируются от одной версии операционной системы к другой.

Хорошо, а как быть когда функция LoadLibrary в таблице импорта конкретно отсутствует и одной или нескольких системных функций, жизненно необходимых shell-коду для распространения, там тоже нет? В UNIX-системах можно (и нужно!) использовать прямой вызов функций ядра, реализуемый либо посредством прерывания по вектору 80h (LINUX, Free BSD, параметры передаются через регистры), либо через дальний CALL по адресу 0007h:00000000h (System V, параметры передаются через стек), при этом номера системных вызовов содержатся в файле /usr/include/sys/syscall.h, так же смотри врезку. Еще можно вспомнить машинные команды syscall/sysenter, которые, как и следует из их названия, осуществляют прямые системные вызовы вместе с передачей параметров. В Windows NT и производных от нее системах дела обстоят намного сложнее. Взаимодействие с ядром реализуется посредством прерывания INT 2Eh, неофициально называемого nativeAPIinterface («родной» API интерфейс). Кое-какая информация на этот счет содержится в легендарном InterruptList’e Ральфа Брауна и «Недокументированных возможностях Windows NT» Коберниченко, но мало, очень мало. Это чрезвычайно скудно документированный интерфейс и единственным источником данных остаются дизассемблерные листинги KERNEL32.DLL и NTDLL.DLL. Работа cnative API требует высокого профессионализма и глубокого знания архитектуры операционной системы, да и как-то громоздко все получается, – ядро NT оперирует с небольшим числом довольно примитивных (или, если угодно, – низкоуровневых) функций. К непосредственному употреблению они непригодны и, как и всякий полуфабрикат, должны быть соответствующим образом приготовлены. Например, функция LoadLibrary «распадается» по меньшей мере на два системных вызова – NtCreateFile (EAX == 17h) открывает файл, NtCreateSection (EAX == 2Bh) проецирует файл в память (т. е. работает как CreateFileMapping), после чего NtClose (EAX == 0Fh) со спокойной совестью закрывает дескриптор. Что же касается функции GetProcAddress, то она целиком реализована в NTDLL.DLL и в ядре даже не ночевала (впрочем, при наличии спецификации PE-формата – она входит в Platform SDK и MSDN – таблицу экспорта можно проанализировать и в «ручную»).

С другой стороны, обращаться к ядру для выбора «эмулятора» LoadLibrary совершенно необязательно, поскольку библиотеки NTDLL.DLL и KERNEL32.DLL всегда присутствуют в адресном пространстве любого процесса и если мы сможем определить адрес их загрузки, мы сорвем банк. Автору известно два способа решения этой задачи – через системный обработчик структурных исключений и через PEB. Первый – самоочевиден, но громоздок и неэлегантен, а второй элегантен, но ненадежен. «PEB только на моей памяти менялась три раза» (с) Юрий Харон. Однако, последнее обстоятельство ничуть не помешало червю Love San разбросать себя по миллионам машин.

Если во время выполнения приложения возникает исключительная ситуация (деление на ноль или обращение к несуществующей странице памяти, например) и само приложение никак ее не обрабатывает, то управление получает системный обработчик, реализованный внутри KERNEL32.DLL и в W2K SP3 расположенный по адресу 77EA1856h. В других операционных системах этот адрес будет иным, поэтому грамотно спроектированный shell-код должен автоматически определять адрес обработчика на лету. Вызывать исключение и трассировать код (как это приходилось делать во времена старушки MS-DOS) теперь совершенно необязательно. Лучше обратиться к цепочке структурных обработчиков, упакованных в структуру EXCEPTION_REGISTRATION, первое двойное слово которых содержит указатель на следующий обработчик (или FFFFFFFFh, если никаких обработчиков больше нет), а второе – адрес данного обработка

Flushall вытолкнуть буфера всех открытых файлов

_flushall возвращает число открытых потоков (ввода и вывода). Ошибка не возвращается.

По умолчанию функция _flushall записывает в соответствующие файлы содержимое всех буферов, связанных с открытыми потоками вывода. Все буферы, связанные с открытыми входными потоками, очищаются. (Эти буферы обычно обслуживаются операционной системой, которая автоматически определяет оптимальное время записи данных на диск: при заполнении буфера, при закрытии потока или при нормальном завершении программы без закрытия потоков).

Если после вызова функции _flushall выполняется операция чтения, из входных файлов в буферы считываются новые данные. После вызова функции _flushall все потоки остаются открытыми.

Предусмотренная в библиотеке времени выполнения функция фиксации на диск позволяет обеспечить запись критически важных данных непосредственно на диск, а не в буферы операционной системы. Эту функцию можно включить, не переписывая программу, а скомпоновав объектные файлы программы с файлом Commode.obj. В создаваемом исполняемом файле вызовы функции _flushall записывают содержимое всех буферов на диск. Файл Commode.obj влияет только на функции _flushall и fflush .

Сведения об управлении функцией фиксации на диск в разделе поток ввода-вывода, fopen, и _fdopen.

Функция Обязательный заголовок
_flushall

Дополнительные сведения о совместимости см. в статье Compatibility во введении.

Управление буферизацией потоков

Управление буферизацией потоков

Открытые файлы, для которых осуществляется высокоуровневый ввод/вывод, буферизуются по умолчанию, за исключением потоков stdin, stdout, stderr, stdaux, stdprn.

Потоки stderr и stdaux — не буферизованы. Если к ним применяется функция printf или scanf, создается временный буфер. Для обоих потоков может задаваться буферизация с помощью функций setbuf или setvbuf.

Буферизация для потоков stdin, stdout, stdprn выполняется следующим образом: буфер сбрасывается при его заполнении или когда вызванная библиотечная функция ввода/вывода завершает работу.

Использованием функции setbuf или setvbuf можно сделать поток небуферизованным или связать буфер с небуферизованным до этого потоком. Буфера, размещенные в системе, недоступны пользователю, кроме буферов, полученных с помощью setbuf или setvbuf.

Буфера должны иметь постоянный размер, равный константе BUFSIZ в stdio.h.

Если используется setvbuf, размер буфера устанавливает пользователь. Буфера автоматически сбрасываются при их наполнении, или когда связанный с буфером файл закрывается, или когда происходит нормальное завершение программы.

Можно сбросить буфера в произвольный момент времени, используя функции fflush и flushall. Функция fflush сбрасывает буфер одного заданного потока, а функция flushall сбрасывает буфера всех потоков, которые открыты и буферизованы в данный момент.

Статья Переполнение буфера и размещение шеллкода в памяти — разработка эксплойтов, часть 6

fuzzz

Доброго времени суток codeby. В предыдущей статье мы познакомились с методом, как перезаписывать адрес возврата используя регистр EIP и так же вычисляли смещение (offset). Это 6 часть цикла статей посвященная разработке эксплойтов. В этой статье мы впервые познакомимся с шеллкодом и напишем с вами первый эксплойт. Шестая часть, шеллкод, черная магия. Совпадение ? Не думаю. Поехали.

Начиная с этого момента нам будет не очень удобно работать напрямую с VM, т.е. взаимодействовать с ней. Особенно это касается копирование данных. Или когда нам надо запустить два окна сразу. Чтобы это исправить теперь мы будем не просто запускать VM и работать с ней, а теперь мы будем подключаться к VM по SSH используя клиент Putty. Для того, чтобы это сделать, надо в настройках VM в разделе «Сеть» включить виртуальный хост адаптера. После чего запустить VM, дальше выполнить команду

Затем посмотрев IP-адрес машины (192.168.56.101) подключиться с помощью клиента Putty.

Stack5 — это стандартное переполнение буфера, на этот раз вводим шелл-код.

Этот уровень находится в / opt / protostar / bin / stack5

Советы

  • На данный момент может быть проще использовать кто-то другой шеллкод
  • При отладке шелл-кода используйте \ xcc (int3), чтобы остановить выполнение программы и вернуться к отладчику.
  • удалите int3s, как только ваш шеллкод будет готов.

Данный нам код уязвимой программы уже нам знаком, мы встречались уже с ним в stack4, единственное отличие здесь нет функции win(). Цель этого задания выполнить шеллкод. И так поговорим с вами о том, что такое шеллкод, что это такое и с чем его едят.

Шеллкод — это двоичный исполняемый код, его так же называют частенько Payload‘ом — боевой нагрузкой, так же шеллкод является неотъемлемой частью эксплойта. Целью шеллкода является выполнение каких-то действий, при чем самых разнообразных, эти действия закодированы в самом шеллкоде, т.е сам шеллкод если говорить простым языком это набор команд (инструкций).

Существует множество различных шеллкод’ов.

Например, шеллкод который запускает командную оболочку ‘/bin/sh‘ или ‘cmd‘ или шеллкод, который скачает файл по URL и затем его выполнит, или же шеллкод который открывает TCP-порт, через который будет осуществляться дальнейший доступ к командной оболочке, или же шеллкод который запускает какую-то определенную программу, например калькулятор. Простым языком шеллкод, это код любой наш код, который мы хотим выполнить.


Собственно шеллкод может быть разного размера и написан под разные задачи в зависимости от преследуемой цели. Т.е. шеллкод который выполняет больше действий соответственно будет большего размера и наоборот. Теперь когда нам более менее понятно, что такое шеллкод в общих чертах, посмотрим, как он выглядит.

А выглядит он так.

.е. как цепочка последовательности определенных байтов.

Что касается нашего ExploitMe. Шеллкод нам надо будет внедрить в память нашей уязвимой программы, после чего нам надо будет передать управление на шеллкод при переполнении буфера. Передача управления шеллкоду осуществляется перезаписью адреса возврата (RET) в стеке, адресом внедрённого шеллкода. Это классический метод исполнения шеллкода при написании эксплойтов.

Так, как, это первое знакомство с шеллкодом, будем использовать шеллкод, который запустит нам командную оболочку ‘/bin/sh’. Собственно это и есть цель нашего задания.

Получается эксплуатация уязвимой программы будет выглядеть следующем образом.

Мусорная строка + адрес шеллкода + сам шеллкод. Результатом этого будет выполнение нашего шеллкода, который откроет командную оболочку.

И так, пожалуй приступим. Начнем с вычисления смещения — offset. Будем использовать метод аналогичный методу с Metasploit, поэтому переходим на

Далее создадим файл в домашней директории.

И скопируем 200 байт, уникальной мусорной строки в него.
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag
Сохраняем F2,y,Enter.

Как устроены дыры в безопасности: переполнение буфера

Прим. переводчика: Это перевод статьи Питера Брайта (Peter Bright) «How security flaws work: The buffer overflow» о том, как работает переполнение буфера и как развивались уязвимости и методы защиты.

Беря своё начало с Червя Морриса (Morris Worm) 1988 года, эта проблема поразила всех, и Linux, и Windows.

Переполнение буфера (buffer overflow) давно известно в области компьютерной безопасности. Даже первый само-распространяющийся Интернет-червь — Червь Морриса 1988 года — использовал переполнение буфера в Unix-демоне finger для распространения между машинами. Двадцать семь лет спустя, переполнение буфера остаётся источником проблем. Разработчики Windows изменили свой подход к безопасности после двух основанных на переполнении буфера эксплойтов в начале двухтысячных. А обнаруженное в мае сего года переполнение буфера в Linux драйвере (потенциально) подставляет под удар миллионы домашних и SMB маршрутизаторов.

По своей сути, переполнение буфера является невероятно простым багом, происходящим из распространённой практики. Компьютерные программы часто работают с блоками данных, читаемых с диска, из сети, или даже с клавиатуры. Для размещения этих данных, программы выделяют блоки памяти конечного размера — буферы. Переполнение буфера происходит, когда происходит запись или чтение объёма данных большего, чем вмещает буфер.

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

Чтобы понять, почему происходит переполнение буфера — и почему результаты столь плачевны — нам нужно рассмотреть то, как программы используют память, и как программисты пишут код.

(Примечание автора: мы рассмотрим, в первую очередь, переполнение стекового буфера (stack buffer overflow). Это не единственный вид переполнения, но оно является классическим и наиболее изученным видом)

Стекируем

Переполнение буфера создаёт проблемы только в нативном коде — т.е. в таких программах, которые используют набор инструкций процессора напрямую, без посредников вроде Java или Python. Переполнения связаны с тем как процессор и программы в нативном коде управляют памятью. Различные операционные системы имеют свои особенности, но все современные распространённые платформы следуют общим правилам. Чтобы понять, как работают атаки, и какие бывают способы противодействия, сначала немного рассмотрим использование памяти.

Важнейшей концепцией является адрес в памяти. Каждый отдельный байт памяти имеет соответствующий числовой адрес. Когда процессор читает или записывает данные в основную память (ОЗУ, RAM), он использует адрес памяти того места, откуда происходит считывание или куда производится запись. Системная память используется не только для данных; она также используется для размещения исполняемого кода, из которого состоит программа. Это означает, что каждая из функций запущенной программы также имеет адрес.

Изначально, процессоры и операционные системы использовали адреса физической памяти: каждый адрес памяти напрямую соотносился с адресом конкретного куска RAM. Хотя, некоторые части современных операционных систем всё ещё используют физические адреса, все современные операционные системы используют схему, именуемую виртуальной памятью.

При использовании виртуальной памяти, прямое соответствие между адресом памяти и физическим участком RAM отсутствует. Вместо этого, программы и процессор оперируют в виртуальном пространстве адресов. Операционная система и процессор совместно поддерживают соответствие (mapping) между адресами виртуальной и физической памяти.

Такая виртуализация позволяет использовать несколько важных функций. Первая и важнейшая — это защищённая память. Каждый отдельный процесс получает свой собственный набор адресов. Для 32-битного процесса, адреса начинаются с нуля (первый байт) и идут до 4,294,967,295 (в шестнадцатеричном виде, 0xffff’ffff; 2^32 — 1). Для 64-битного процесса, адреса продолжаются до 18,446,744,073,709,551,615 (0xffff’ffff’ffff’ffff, 2^64 — 1). Таким образом, у каждого процесса есть свой собственный адрес 0, за ним свой адрес 1, свой адрес 2 и так далее.

(Примечание автора: Далее в статье я буду говорить о 32-битный системах, если не указано иное. В данном аспекте разница между 32-битными и 64-битными несущественна; для ясности я буду придерживаться единой битности)

Поскольку каждый процесс получает свой собственный набор адресов, эта схема является простым способом предотвратить повреждение памяти одного процесса другим: все адреса к которым процесс может обращаться принадлежат только ему. Это гораздо проще и для самого процесса; адреса физической памяти, хотя они в широком смысле работают также (это просто номера, начинающиеся с нуля), имеют особенности, которые делают их несколько неудобными в использовании. Например, они обычно не-непрерывные; например, адрес 0x1ff8’0000 используется для памяти режима системного управления процессора — небольшой кусок памяти, недоступный обычным программам. Память PCIe-карт также находится в этом пространстве. С адресами виртуальной памяти таких неудобств нет.

Что же находится в адресном пространстве процесса? Говоря в общем, существуют четыре распространённых объекта, три из которых представляют для нас интерес. Неинтересный для нас блок, в большинстве операционных систем, — «ядро операционной системы». В интересах производительности, адресное пространство обычно разделяют на две половины, нижняя из которых используется программой, а верхняя занимается адресным пространством ядра. Половина, отданная ядру, недоступна из половины занятой программой, однако само ядро может читать память программы. Это является одним из способов передачи данных в функции ядра.

В первую очередь разберёмся с исполняемой частью и библиотеками, составляющими программу. Главный исполняемый файл (main executable) и все его библиотеки загружаются в адресное пространство процесса, и все составляющие их функции, таким образом, имеют адрес в памяти.

Вторая часть используемой программой памяти используется для хранения обрабатываемых данных и обычно называется кучей (heap). Эта область, например, используется для хранения редактируемого документа, или просматриваемой веб-страницы (со всеми её объектами JavaScrit, CSS и т.п.), или карты игры, в которую играют.

Третья и важнейшая часть — стек вызовов, обычно называемый просто стеком. Это самый сложный аспект. Каждый поток в процессе имеет свой стек. Это область памяти, используемая для одновременного отслеживания как текущей функции исполняемой в потоке, так и всех предшествующих функций — тех, что были вызваны, чтобы попасть в текущую функцию. Например, если функция a вызывает функцию b, а функция b вызывает функцию c, то стек будет содержать информацию об a, b и c, в таком порядке.

Стек вызовов является специализированной версией структуры данных, называемой «стеком». Стеки являются структурами переменной длины, предназначенными для хранения объектов. Новые объекты могут быть добавлены (pushed) в конец стека (обычно называемого «вершиной» стека) и объекты могут быть сняты (popped) со стека. Только вершина стека подлежит изменению с использованием push и pop, таким образом, стек устанавливает строгий порядок сортировки: объект, который последним положили в стек, будет тем, который будет снят с него следующим.

Важнейшим объектом, хранимым в стеке вызовов, является адрес возврата (return address). В большинстве случаев, когда программа вызывает функцию, эта функция выполняет то, что должна (включая вызов других функций), а затем возвращает управление в функцию, которая её вызвала. Для возврата к вызывающей функции необходимо сохранить запись о ней: исполнение должно продолжиться с инструкции следующей после инструкции вызова. Адрес этой инструкции называется адресом возврата. Стек используется для хранения этих адресов возврата: при каждом вызове функции, в стек помещается адрес возврата. При каждом возврате, адрес снимается со стека и процессор начинает выполнять инструкцию по этому адресу.

Стековая функциональность является настолько базовой и необходимой, что большинство, если не все процессоры имеют встроенную поддержку этих концепций. Возьмём за пример процессоры x86. Среди регистров (небольших участков памяти в процессоре, доступных инструкциям), определённых в спецификации x86, два наиболее важных — eip (указатель инструкции — instruction pointer), и esp (указатель стека — stack pointer).

ESP всегда содержит адрес вершины стека. Каждый раз когда что-то добавляется в стек, значение esp уменьшается. Каждый раз, когда что-то снимается со стека, значение esp увеличивается. Это означает, что стек растёт «вниз»; по мере добавления объектов в стек, адрес хранимый в esp становится всё меньше и меньше. Несмотря на это, область памяти, на которую указывает esp, называется «вершиной стека.

Здесь мы видим простую развёртку стека с 64-символьным буфером с именем name, за ним указатель вложенного кадра (frame pointer), потом адрес возврата. В регистре esp содержится адрес вершины, в ebp — адрес указателя кадра.

EIP содержит адрес текущей инструкции. Процессор поддерживает значение eip самостоятельно. Он читает поток инструкций из памяти и изменяет значение eip соответственно, так что он всегда содержит адрес инструкции. В рамках x86 существует инструкция для вызова функций, call, а также инструкция для возврата — ret.

CALL принимает один операнд, адрес вызываемой функции (хотя есть несколько способов передать его). Когда выполняется call, указатель стека esp уменьшается на 4 байта (32 бита), и адрес инструкции следующей за call — адрес возврата — помещается в область памяти, на которую теперь указывает esp. Другими словами, адрес возврата помещается в стек. Затем, значением eip устанавливается равным адресу, переданному в качестве операнда call, и выполнение продолжается с этой точки.

RET производит обратную операцию. Простой ret не принимает операндов. Процессор сначала считывает значение по адресу памяти, хранимому в esp, потом увеличивает esp на 4 байт — снимает адрес возврата со стека. Значение помещается в eip, и выполнение продолжается с этого адреса.

(Примечание переводчика: в этом месте в авторском тексте приводится видео с демонстрацией call и ret.)

Если бы стек вызовов хранил только набор адресов возврата, проблемы бы не было. Реальная проблема приходит со всем остальным, что кладут в стек. Так выходит, что стек — это быстрое и эффективное место хранения данных. Хранение данных в куче относительно сложно: программа должна отслеживать доступное в куче место, сколько занимает каждый из объектов и прочее. При этом работа со стеком проста: чтобы разместить немного данных, достаточно просто уменьшить значение указателя. А чтобы почистить за собой, достаточно увеличить значение указателя.

Это удобство делает стек логичным местом для размещения переменных, используемых функцией. Функции нужно 256 байт буфера, чтобы принять ввод пользователя? Легко, просто отнимите 256 от указателя стека — и буфер готов. В конце функции, просто прибавьте 256 к указателю, и буфер отброшен.

Однако, у такого подхода существуют ограничения. Стек не подходит для хранения очень больших объектов: общий объём доступной памяти обычно фиксирован при создании потока и, часто, составляет примерно 1МБ в объёме. Поэтому большие объекты должны быть помещены в кучу. Стек также не применим для объектов, которые должны существовать дольше, чем выполняется одна вызванная функция. Поскольку все размещения в стеке удаляются при выходе из функции, время жизни любого из объектов в стеке не превышает времени выполнения соответствующей функции. На объекты в куче это ограничение не распространяется, они могут существовать „вечно“.

Когда мы используем программу корректно, ввод с клавиатуры сохраняется в буфере name, закрываемым нулевым (null, zero) байтом. Указатель кадра и адрес возврата не изменяются.

Стековое хранилище используется не только для явно определяемых программистом переменных; стек также используется для хранения любых значений, нужных программе. Особенно остро это проявляется в x86. Процессоры на базе x86 не отличаются большим числом регистров (всего существует 8 целочисленных регистров, и некоторые из них, как уже упомянутые eip и esp, уже заняты), поэтому функции редко имеют возможность хранить все необходимые им значения в регистрах. Чтобы освободить место в регистрах, и при этом сохранить значение для последующего использования, компилятор поместит значение регистра в стек. Значение позднее может быть снято с регистра и помещено обратно в регистр. В жаргоне компиляторов, процесс сохранения регистров с возможностью последующего использования называется spilling.

Наконец, стек часто используют для передачи аргументов функциям. Вызывающая функция помещает каждый из аргументов по очереди в стек; вызываемая функция может снять их из стека. Это не единственный способ передачи аргументов — можно использовать и регистры, например, — но это один из наиболее гибких.

Набор объектов хранимых функцией в стеке — её собственные переменные, сохранённые регистры, любые аргументы, подготавливаемые для передачи в другие функции — называются „вложенным кадром“. Поскольку данные во вложенном кадре активно используются, полезно иметь способ простой адресации к нему.

Это возможно реализовать, используя указатель стека, но это несколько неудобно: указатель стека всегда указывает на вершину, и его значение меняется по мере помещения и снятия объектов. Например, переменная может сначала быть расположена на позиции esp+4. После того, как ещё два значения положили в стек, и переменная стала располагаться по адресу esp+12. Если снять со стека одно из значений, переменная окажется на esp+8.

Описанное не является неподъёмной задачей, и компиляторы способны с ней справиться. Однако это делает использование указателя стека для доступа к чему-либо кроме вершины „стрёмным“, особенно при написании на ассемблере вручную.

Для упрощения задачи, обычным делом является ведение второго указателя, который хранит адрес „дна“ (т.е. начала) каждого кадра — значение, известное как указатель вложенного кадра (frame pointer). И на x86 даже есть регистр, который для этого обычно используют, ebp. Поскольку его значение неизменно в пределах функции, появляется способ однозначно адресовать переменные функции: значение, лежащее по адресу ebp-4, будет оставаться доступно по ebp-4 всё время жизни функции. И это полезно не только для людей — дебаггерам проще разобраться, что происходит.

Скриншот из Visual Studio демонстрирует всё это в действии на примере простой программы для x86. На процессорах x86, регистр esp содержит адрес вершины стека, в данном случае 0x0019fee0 (выделено синим). (Примечание автора: на платформе x86, стек растёт вниз, в направлении адреса памяти 0, однако эта точка всё равно сохраняет название „вершина стека“). Показанная функция хранит в стеке только переменную name, выделенную розовым цветом. Это фиксированный буфер длиной 64 байта. Поскольку это единственная переменная, её адрес тоже 0x0019fee0, такой же, как у вершины стека.

В x86 также есть регистр ebp, выделенный красным, который (обычно) выделен для хранения указателя кадра. Указатель кадра размещается сразу за переменными стека. Сразу за указателем кадра лежит адрес возврата, выделенный зелёным. Адрес возврата ссылается на фрагмент кода по адресу 0x00401048. Эта инструкция следует сразу за вызовом (call), демонстрируя то, как адрес возврата используется для продолжения исполнения там, где программа покинула вызывающую функцию.

NAME в приведённой иллюстрации относится как раз к тому роду буферов, которые регулярно переполняются. Его размер зафиксирован и составляет 64 байта. В данном случае, он заполнен набором чисел и завершается нулём. Из иллюстрации видно, что если в буфер name будет записано более 64 байт, то другие значения в стеке будут повреждены. Если записать на четыре байта больше, указатель кадра будет уничтожен. Если записать на восемь байт больше, то и указатель кадра, и адрес возврата будут перезаписаны.

Очевидно, что это ведёт к повреждению данных программы, но проблема с переполнением буфера куда серьёзнее: они ведут к выполнению [произвольного] кода. Это происходит потому, что переполненный буфер не просто перезапишет данные. Также могут оказаться перезаписаны более важные вещи, хранимые в стеке — адреса возврата. Адрес возврата контролирует то, какие инструкции процессор будет выполнять, когда закончит с текущей функцией; предполагается, что это будет какой-то адрес внутри вызывающей функции, но если это значение будет переписано переполнением буфера, оно может указывать куда угодно. Если атакующие могут контролировать переполнение буфера, то они могут контролировать и адрес возврата. Если они контролируют адрес возврата, они могут указать процессору, что делать дальше.

У процессора, скорее всего, нет красивой удобной функции „скомпрометировать машину“, которую бы запустил атакующий, но это не слишком важно. Тот же буфер, который используется для изменения адреса возврата, можно использовать для хранения небольшого куска исполнимого кода (shellcode, шеллкод), который, в свою очередь, скачает вредоносный исполнимый файл, или откроет сетевое соединение, или исполнит любые другие пожелания атакующего.

Традиционно, сделать это было тривиально просто, по причине, которая у многих вызывает удивление: обычно, каждая программа будет использовать одни и те же адреса в памяти при каждом запуске, даже если вы перезагружали машину. Это означает, что позиция буфера в стеке всякий раз будет одинакова, а значит и значение, используемое для искажения адреса возврата, каждый раз будет одинаково. Атакующему достаточно лишь выяснить этот адрес однажды, и атака сработает на любом компьютере, исполняющем уязвимый код.

Инструментарий атакующего

В идеальном мире — с точки зрения атакующего — переписанный адрес возврата может быть просто адресом буфера. И это вполне возможно, когда программа читает данные из файла или из сети.

В других случаях, атакующий должен идти на хитрости. В функциях, обрабатывающих человеко-читаемый текст, байт с нулевым значением (null) часто имеет специальное значение; такой байт обозначает конец строки, и функции используемые для манипуляции строками — копирование, сравнение, совмещение — останавливаются. когда встречают этот символ. Это означает, что если шеллкод содержит ноль, эти процедуры его сломают.

(Примечание переводчика: в этом месте в авторском тексте приводится видео с демонстрацией переполнения. В нём, в буфер помещают шеллкод и переписывают адрес возврата. Шеллкод запускает стандартный калькулятор Windows.)

Чтобы это обойти, атакующий может использовать различные приёмы. Небольшие фрагменты кода для конвертирования шеллкода с нулями в эквивалентную последовательность, избегающую проблемный байт. Так возможно пролезть даже через очень строгие ограничения; например, уязвимая функция принимает на вход только данные, которые можно набрать со стандартной клавиатуры.

Собственно адрес стека часто содержит нули, и здесь есть сходная проблема: это означает, что адрес возврата нельзя записать адрес из стекового буфера. Иногда это не страшно, потому что некоторые из функций, используемых для заполнения (и, потенциально, переполнения) буферов сами пишут нули. Проявляя некоторую осторожность, их можно использовать, чтобы поместить нулевой байт точно в нужное место, устанавливая в адресе возврата адрес стека.

Но даже когда это невозможно, ситуация обходится окольными путями (indirection). Собственно программа со всеми своими библиотеками держит в памяти огромное количество исполнимого кода. Большая часть этого кода будет иметь „безопасный“ адрес, т.е. не будет иметь нулей в адресе.

Тогда, атакующему нужно найти подходящий адрес, содержащий инструкцию вроде call esp (x86), которая использует значение указателя стека в качестве адреса функции и начинает её исполнение, чем идеально подходит для шеллкода спрятанного в стековом буфере. Атакующий использует адрес инструкции call esp для записи в качестве адреса возврата; процессор сделает лишний прыжок через этот адрес, но всё равно попадёт на шеллкод. Этот приём с прыжком через другой адрес называется „трамплином“.

Для эксплуатации переполнения, вместо того чтобы просто забить всё некими символами, атакующий пишет в буфер шеллкод: короткий участок исполнимого кода, который выполнит некое выбранное атакующим действие. Адрес возврата переписывается соответствующим адресом из буфера, заставляя процессор исполнять шеллкод при попытке возврата из процедуры.

Это работает потому, повторюсь, что программа и её библиотеки при каждом запуске размещаются в одни и те же области памяти — даже между перезагрузками и даже на разных машинах. Одним из интересных моментов в этом деле является то, что библиотеке, от которой выполняется трамплин, самой даже не нужно использовать оператор call esp. Достаточно, чтобы в ней были два подходящих байта (в данном случае, со значениями 0xffи 0xd4) идущие друг-за-другом. Они могут быть частью какой-то иной функции, или даже просто числом; x86 не привередлива к таким вещам. Инструкции x86 могут быть очень длинными (до 15 байт!) и могут располагаться по любому адресу. Если процессор начнёт читать инструкцию с середины — со второго байта четырёхбайтной инструкции, к примеру — результат будет интерпретирован как совсем иная, но всё же валидная, инструкция. Это обстоятельство делает нахождение полезных трамплинов достаточно простым.

Иногда, однако, атака не может установить адрес возврата в точности куда требуется. Несмотря на то, что расположение объектов в памяти крайне схоже, оно может слегка отличаться от машины к машине или от запуска к запуску. Например, точное расположение подверженного атаке буфера может варьироваться вверх и вниз на несколько байт, в зависимости от имени системы или её IP-адреса, или потому, что минорное обновление программы внесло незначительное изменение. Чтобы справится с этим, полезно иметь возможность указать адрес возврата который примерно верен, но высокая точность не нужна.

Это легко делается с использованием приёма, называемого „посадочной полосой“ (NOP sled, букв. „сани из NOPов“ (спасибо Halt за корректный русскоязычный термин — прим.пер.)). Вместо того, чтобы писать шеллкод сразу в буфер, атакующий пишет большое число инструкций NOP (означающих „no-op“, т.е. отсутствие операции — говорит процессору ничего не делать), иногда сотни, перед настоящим шеллкодом. Для запуска шеллкода, атакующему нужно установить адрес возврата на позицию где-то посреди этих NOPов. И если мы попали в область NOPов, процессор быстро обработает их и приступит к настоящему шеллкоду.

Иногда сложно переписать адрес возврата адресом из буфера. В качестве решения, мы можем переписать адрес возврата адресом части исполнимого кода в программе-жертве (или её библиотеках). И этот фрагмент уже передаст управление в буфер.

Во всём нужно винить C


Главный баг, который позволяет всё это сделать — записать в буфер больше, чем доступно места — выглядит как что-то, что легко избежать. Это преувеличение (хоть и небольшое) возлагать всю ответственность на язык программирования C, или его более или менее совместимых отпрысков, конкретно C++ и Objective C. Язык C стар, широко используем, и необходим для наших операционных систем и программ. Его дизайн отвратителен, и хотя всех этих багов можно избежать, C делает всё, чтобы подловить неосторожных.

В качестве примера враждебности C к безопасной разработке, взглянем на функцию gets(). Эта функция принимает один параметр — буфер — и считывает строку данных со стандартного ввода (что, обычно, означает „клавиатуру“), и помещает её в буфер. Наблюдательный читатель заметит, что функция gets() не включает параметр размера буфера, и как забавный факт дизайна C, отсутствует способ для функции gets() определить размер буфера самостоятельно. Это потому, что для gets() это просто не важно: функция будет читать из стандартного ввода, пока человек за клавиатурой не нажмёт клавишу Ввод; потом функция попытается запихнуть всё это в буфер, даже если этот человек ввёл много больше, чем помещается в буфер.

Это функция, которую в буквальном смысле нельзя использовать безопасно. Поскольку нет способа ограничить количество набираемого с клавиатуры текста, нет и способа предотвратить переполнение буфера функцией gets(). Создатели стандарта языка C быстро поняли проблему; версия спецификации C от 1999 года выводила gets() из обращения, а обновление от 2011 года полностью убирает её. Но её существование — и периодическое использование — показывают, какого рода ловушки готовит C своим пользователям.

Червь Морриса, первый само-распространяющийся зловред который расползся по раннему Интернету за пару дней в 1988, эксплуатировал эту функцию. Программа fingerd в BSD 4.3 слушает сетевой порт 79, порт finger. Finger является древней программой для Unix и соответствующим сетевым протоколом, используемым для выяснения того, кто из пользователей вошёл в удалённую систему. Есть два варианта использования: удалённую систему можно опросить и узнать всех пользователей, осуществивших вход, или можно сделать запрос о конкретном юзернейме, и программа вернёт некоторую информацию о пользователе.

К сожалению, gets() довольно глупая функция. Достаточно зажать клавишу А на клавиатуре, и она не остановится после заполнения буфера name. Она продолжит писать данные в память, перезаписывая указатель кадра, адрес возврата и всё остальное, до чего сможет дотянуться.

Каждый раз при сетевом подключении к демону finger, он начинал чтение с сети — используя gets() — в стековый буфер длиной 512 байт. При нормальной работе, fingerd затем запускал программу finger, передавая ей имя пользователя (если оно было). Программа finger выполняла реальную работу по перечислению пользователей или предоставлению информации о конкретном пользователе. Fingerd просто отвечала за сетевое соединение и запуск finger.

Учитывая, что единственный „реальный“ параметр это необязательное имя пользователя, 512 байт является достаточно большим буфером. Скорее всего ни у кого нет имени пользователя и близко такой длины. Однако, нигде в системе это ограничение не было жёстким по причине использования ужасной функции gets(). Пошлите больше 512 байт по сети и fingerd переполнит буфер. И именно это сделал Роберт Моррис (Robert Morris): его эксплоит отправлял в fingerd 537 байт (536 байт данных и перевод строки, заставлявший gets() прекратить чтение), переполняя буфер и переписывая адрес возврата. Адрес возврата был установлен просто в области стекового буфера.

Исполнимая нагрузка червя Моррис была простой. Она начиналась с 400 инструкций NOP, на случай если раскладка стека будет слегка отличаться, затем короткий участок кода. Этот код вызывал шелл, /bin/sh. Это типичный вариант атакующей нагрузки; программа fingerd запускалась под рутом, поэтому, когда при атаке она запускала шелл, шелл тоже запускался под рутом. Fingerd была подключена к сети, принимая „клавиатурный ввод“ и аналогично отправляя вывод обратно в сеть. И то и другое наследовал шелл вызванный эксплойтом, и это означало, что рутовый шелл теперь был доступен атакующему удалённо.

Несмотря на то, что использования gets() легко избежать — даже во время распространения червя Морриса была доступна версия fingerd не использовавшая gets() — прочие компоненты C сложнее игнорировать, и они не менее подвержены ошибкам. Типичной причиной проблем является обработка строк в C. Поведение, описанное ранее — останов на нулевых байтах — восходит к поведению строк в C. В языке C, строка представляет собой последовательность символов, завершаемую нулевым байтом. В C существует набор функций для работы со строками. Возможно, лучшим примером являются strcpy(), копирующая строку из одного места в другое, и strcat(), вставляющая исходную строку следом за точкой назначения. Ни одна из этих функций не имеет параметра размера буфера назначения. Обе с радостью будут бесконечно читать из источника, пока не встретят NULL, заполняя буфер назначения и беззаботно переполняя его.

Даже если строковая функция в C имеет параметр размера буфера, она реализует это способом, ведущим к ошибкам и переполнениям. В языке C есть пара функций родственных strcat() и strcpy(), называемых strncat() и strncpy(). Буква n в именах этих функций означает что они, в некотором роде, принимают размер в качестве параметра. Однако n, хотя многие наивные программисты думают иначе, не является размером буфера в который происходит запись — это число символов для считывания из источника. Если в источнике символы закончились (т.е. достигнут нулевой байт), то strncpy() и strncat() заполнят остаток нулями. Ничто в этих функциях не проверяет истинный размер назначения.

В отличии от gets(), эти функции возможно использовать безопасным образом, только это не просто. В языках C++ и Objective-C есть лучшие альтернативы этим функциям C, что делает работу со строками проще и безопаснее, однако функции C также поддерживаются в целях обратной совместимости.

Более того, они сохраняют фундаментальный недостаток языка C: буферы не знают своего размера, и язык никогда не проверяет выполняемые над буферами чтения и записи, допуская переполнение. Именно такое поведение привело к недавнему багу Heartbleed в OpenSSL. То не было переполнение, а перечтение, когда код на C в составе OpenSSL пытался прочитать из буфера больше чем тот содержал, сливая информацию наружу.

Латание дыр

Конечно, человечество разработало множество языков в которых осуществляется проверка чтения и записи в буферы, что защищает от переполнения. Компилируемые языки, такие как поддерживаемый Mozilla язык Rust, защищённые среды исполнения вроде Java и .NET, и практически все скриптовые языки вроде Python, JavaScript, Lua и Perl имеют иммунитет к этой проблеме (хотя в .NET разработчики могут явным образом отключить защиту и подвергнуть себя подобному багу, но это личный выбор).

Тот факт, что переполнение буфера продолжает оставаться частью ландшафта безопасности, говорит о популярности C. Одой из причин этого, конечно, является большое количество унаследованного кода. В мире существует огромное количество кода на C, включая ядра всех основных операционных систем и популярных библиотек, таких как OpenSSL. Даже если разработчики хотят использовать безопасный язык, вроде C#, у них могут оставаться зависимости от сторонних библиотек, написанных на C.

Производительность является другой причиной продолжающегося использования C, хотя смысл такого подхода не всегда понятен. Верно, что компилируемые C и С++ обычно выдают быстрый исполняемый код, и в некоторых случаях это действительно очень важно. Но у многих из нас процессоры большую часть времени простаивают; если бы мы могли пожертвовать, скажем, десятью процентами производительности наших браузеров, но при этом получить железную гарантию невозможности переполнения буфера — и других типичных дыр, мы может быть бы решили, что это не плохой размен. Только никто не торопится создать такой браузер.

Несмотря ни на что, C сотоварищи никуда не уходит; как и переполнение буфера.

Предпринимаются некоторые шаги по предупреждению этого рода ошибок. В ходе разработки, можно использовать специальные средства анализа исходного кода и запущенных программ, стараясь обнаружить опасные конструкции или ошибки переполнения до того, как эти баги пролезут в релиз. Новые средства, такие как AddressSanitizer и более старые, как Valgrind дают такие возможности.

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

Некоторые из этих систем нацелены на усложнение конкретных атак. Один из наборов патчей для Linux делает так, что все системные библиотеки загружаются в нижние адреса таким образом, чтобы содержать, по крайней мере, один нулевой байт в своём адресе; это существенно усложняет их использование в переполнениях эксплуатирующих обработку строк в C.

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

Возможно, важнейшим из средств защиты является механизм известный под именами W^X (»write exclusive-or execute»), DEP («data execution prevention»), NX («No Xecute»), XD («eXecute Disable»), EVP («Enhanced Virus Protection,» специфичный для AMD термин), XN («eXecute Never»), и, вероятно, другими. Здесь принцип прост. Эти системы стараются разделить память на записываемую (подходящую для буферов) и исполнимую (подходящую для библиотек и программного кода), но не одновременно ту и другую. Таким образом, даже если атакующий может переполнить буфер и контролировать адрес возврата, процессор не будет выполнять шеллкод.

Как бы вы его не назвали, это важный механизм ещё и потому, что он не требует вложений. Этот подход использует защитные меры встроенные в процессор, поскольку это часть механизма аппаратной поддержки виртуальной памяти.

Как уже говорилось ранее, в режиме виртуальной памяти каждый процесс получает свой набор частных адресов памяти. Операционная система и процессор совместно поддерживают соотношение виртуальных адресов к чему-то ещё; иногда, виртуальный адрес отображается на физическую память, иногда в часть файла на диске, а иногда в никуда, просто потому что он не распределён. Это соотнесение гранулярно и обычно происходит частями размером в 4096 байт, именуемыми страницами.

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

Одним из интересных моментов NX является то, что его можно применить к существующим программам «задним числом», просто путём обновления операционной системы до той, что поддерживает защиту. Иногда программы налетают на проблемы. JIT (Just-in-time)-компиляторы, используемые в Java и .NET, генерируют исполнимый код в памяти на этапе исполнения, и поэтому требуют память, которую можно и писать и исполнять (хотя, одновременность этих свойств не требуется). Когда ещё не было NX, вы могли исполнять код из любой памяти, которую могли читать, поэтому в таких JIT-компиляторах не было проблемы с особыми буферами чтения-записи. С появлением NX, от них требуется удостовериться, что защита памяти изменена с чтение-запись на чтение-исполнение.

Потребность в чём-то вроде NX была ясна, особенно для Microsoft. В начале 2000-х, пара червей показала, что у компании были серьёзные проблемы с безопасностью кода: Code Red, инфицировавший не менее 359000 систем под управлением Windows 2000 с сервисом Microsoft IIS Web server в июле 2001, и SQL Slammer, инфицировавший более 75000 систем с Microsoft SQL Server в январе 2003. Эти случаи хорошо ударили по репутации.

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

Естественно, эти черви были более продвинуты и в других областях. Нагрузка Code Red не просто самовоспроизводилась; она производила дефейс веб-страниц и пыталась выполнять DoS-атаки. SQL Slammer нёс в себе всё необходимое для поиска новых целей для заражения и распространения по сети — всего в нескольких сотнях байт, при этом не оставляя следов на инфицированных машинах; перезагрузите машину — и его нет. Оба червя также работали в Интернете, который был многократно больше того, в котором распространился червь Морриса, и потому число заражений было сильно выше.

Однако основная проблема — легко эксплуатируемое переполнение стекового буфера — осталась прежней. Эти черви оказались в заголовках новостей и заставили многих сомневаться в возможности использовать Windows любого рода в качестве сервера, смотрящего в Интернет. Ответом Microsoft было начать всерьёз задумываться о безопасности. Windows XP Service Pack 2 была первым продуктом с установкой на безопасность. Было сделано несколько программных изменений, включая добавление программного межсетевого экрана, модификация Internet Explorer, препятствующая тихой установке тулбаров и плагинов, а также — поддержка NX.

Аппаратное обеспечение с поддержкой NX стало входить в быт где-то с 2004 года, когда Intel представила Prescott Pentium 4, поддержка со стороны операционных систем стала обыденностью со времён Windows XP SP2. В Windows 8 они решили ещё больше форсировать этот момент, отказавшись от поддержки процессоров, не умеющих NX.

Что было после NX

Несмотря на распространение поддержки NX, переполнение буфера остаётся актуальной проблемой информационной безопасности. Причиной тому является разработка ряда способов обхода NX.

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

Возможно, лучшим кандидатом на эту роль является Unix-функция system(). Она принимает один параметр: адрес строки, представляющей собой команду для исполнения — и обычно этот параметр передаётся через стек. Атакующий может создать нужную команду и поместить её в переполняемый буфер, а поскольку (традиционно) расположение объектов в памяти неизменно, адрес этой строки будет известен и может быть помещён на стек в ходе атаки. Переписанный адрес возврата в этом случае не указывает на адрес в буфере; он указывает на функцию system(). Когда функция подверженная переполнению завершает работу, вместо возврата в вызывающую функцию она запустит system(), что приведёт к исполнению заданной атакующим команды.

Вот так можно обойти NX. Функция system(), будучи частью системной библиотеки, уже исполнима. Эксплойту не требуется исполнять код из стека; достаточно прочитать команду с него. Этот приём получил название «return-to-libc» (возврат на libc, библиотеки Unix, содержащей множество ключевых функций, включая system(), и обычно загружаемой в каждый Unix-процесс, что делает её подходящей целью для такого использования) и был изобретён в 1997 году русским экспертом по информационной безопасности Solar Designer.

Хотя этот приём и полезен, у него есть ограничения. Часто функции принимают аргумент не со стека, а через регистры. Удобно передавать команды для исполнения, но они часто содержат эти дурацкие нули, что немало мешает. Кроме того, составить последовательность из различных вызовов таким способом весьма непросто. Это возможно — прописать несколько адресов возврата вместо одного — но нет способа изменить порядок следования аргументов, используя возвращаемые значения или что-либо ещё.

Вместо заполнения буфера шелкодом, мы заполняем его последовательностью адресов возврата и данными. Эти адреса возврата передают управление существующим фрагментам исполнимого кода в программе-жертве и её библиотеках. Каждый фрагмент кода выполняет операцию и выполняет возврат, передавая управление по следующему адресу возврата.

За несколько лет, return-to-libc был обобщён для обхода этих ограничений. В конце 2001 было задокументировано несколько вариантов расширения этого способа: возможность нескольких вызовов и решение проблемы нулевых байтов. Более сложный способ, решавший большую часть этих проблем, был формально описан в 2007 году: return-oriented-programming (ROP, возвратно-ориентированное программирование).

Здесь используется тот же принцип что и в return-to-libc и трамплине, но более обобщённый. Там где трамплин использует единственный фрагмент кода для передачи исполнения шеллкоду в буфере, ROP использует много фрагментов кода, называемых «гаджетами» в оригинальной публикации. Каждый гаджет следует определённому шаблону: он выполняет некую операцию (запись значения в регистр, запись в память, сложение регистров, и т.п.), за которой следует команда возврата. То самое свойство, что делает x86 пригодным для трамплина работает и здесь; системные библиотеки, загруженные в память процессом, содержат сотни последовательностей которые можно интерпретировать как «действие и возврат», а значит, могут быть использованы для ROP-атак.

Для объединения гаджетов в одно целое используется длинная последовательность адресов возврата (а также любых полезных и необходимых данных) записанных в стек в ходе переполнения буфера. Инструкции возврата прыгают с гаджета на гаджет, в то время как процессор редко (или никогда) вызывает функции, а только возвращается из них. Интересно то, что по крайней мере на x86, число и разнообразие полезных гаджетов таково, что атакующий в прямом смысле может делать всё что угодно; это подмножество x86, используемое особым образом, зачастую является Тьюринг-полным (хотя полный спектр возможностей будет зависеть от загружаемых программой библиотек, и следственно перечнем доступных гаджетов).

Как и в случае с return-to-libc, весь действительно исполнимый код берётся из системных библиотек, и как следствие защита вроде NX бесполезна. Большая гибкость этого подхода означает, что эксплойты могут делать то, что сложно организовать последовательностью return-to-libc, например, вызывая функции принимающие аргументы через регистры, использовать возвращаемые значения одних функций в других и прочее.

Нагрузка в ROP-атаках бывает разной. Иногда это простой код для получения шелла (доступа к командному интерпретатору). Другим распространённым вариантом является использование ROP для вызова системной функции для изменения NX-параметров страницы памяти, меняя их с записываемых на исполнимые. Сделав это, атакующий может использовать обычную, не-ROP нагрузку.

Рандомизация

Эта слабость NX давно известна, и эксплойты такого типа шаблоны: атакующий заранее знает адрес стека и системных библиотек в памяти. Всё зиждется на этом знании, а потому очевидным решением является лишить атакующего этого знания. Именно этим занимается ASLR (Address Space Layout Randomization, Рандомизация развёртки адресного пространства): он делает случайной позицию стека и расположение в памяти библиотек и исполнимого кода. Обычно они меняются при каждом запуске программы, перезагрузке или некоторой их комбинации.

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

ASLR во многом сопутствует NX, закрывая такие крупные дыры как возврат к libc или ROP. К несчастью, он несколько менее прозрачен, чем NX. Не считая JIT-компиляторов и ряда других специфичных случаев, NX может быть безопасно внедрён в существующие программы. ASLR более проблематичен: с ним программы и библиотеки не могут полагаться в своей работе на значение адреса, в который они загружены.

В Windows, например, это не должно быть большой проблемой для DLL. В Windows, DLL всегда поддерживали загрузку в разные адреса, а вот для EXE это может быть проблемой. До ASLR, EXE всегда загружались в адрес 0x0040000 и могли полагаться на этот факт. С внедрением ASLR это уже не так. Чтобы предотвратить возможные проблемы, Windows по умолчанию требует от программ явного указания поддержки ASLR. Люди, думающие о безопасности, могут, однако, изменить это поведение по умолчанию, заставив Windows включить ASLR для всех программ и библиотек. Это почти никогда не вызывает проблем.

Ситуация вероятно хуже в Linux на x86, поскольку подход к реализации ASLR на этой платформе даёт потерю производительности до 26 процентов. Более того, этот подход требует компиляции программ и библиотек с поддержкой ASLR. Нет способа администратору сделать ASLR принудительным, как в Windowsю (на x64 потеря производительности пусть и не уходит совсем, но значительно снижается)

Когда ASLR активен, он даёт сильную защиту от простого взлома. Однако, он не совершенен. Например, одним из ограничений является степень случайности, которую можно получить, что особенно заметно на 32-битных системах. Хотя в адресном пространстве 4 миллиарда различных адресов, не все они доступны для загрузки библиотек или размещения стека.

Для этого существует множество ограничений. Некоторые из них состоят в широте целей. В общем случае, операционная система предпочитает загружать библиотеки близко друг-к-другу на одном из концов адресного пространства процесса, чтобы как можно больше непрерывного места было доступно приложению. Вам не хочется получить по одной библиотеке через каждые 256МБ памяти, поскольку тогда возможное наибольшее унитарное выделение памяти будет меньше 256МБ, что ограничивает возможность приложения работать с данными большого объёма.

Исполнимые файлы и библиотеки обычно должны быть загружены так чтобы начинаться, по крайней мере, на границе страницы. Обычно, это означает, что они должны быть загружены в адрес, делимый на 4096. Различные платформы могут и иметь подобные ограничения для стека; Linux, например, начианет стек на адресе делимом на 16. Системы с ограничением по памяти иногда вынуждены ещё более ограничить случайность, чтобы иметь возможность всё разместить.

Результаты бывают различными, но иногда атакующий могут угадать нужный адрес, с высокой вероятностью попадания. Даже невысокого шанса — скажем, один из 256 — может быть достаточно в некоторых ситуациях. Когда атакуешь веб-сервер, который автоматически перезапустит рухнувший процесс, не важно, что 255 из 256 атак приведут к краху процесса. Он будет перезапущен, и можно попробовать снова.

Но на 64-битных системах, адресное пространство так велико, что такой подход не поможет. У атакующего может быть только один шанс из миллиона или даже один из миллиарда, и это достаточно мало, чтобы это не было важно.

Угадывание и падение не слишком хорошая стратегия для атаки, скажем, браузеров; ни один пользователь не будет перезапускать браузер 256 раз кряду лишь бы дать атакующему шанс. В результате, эксплуатация такой уязвимости в системе с активными NX и ASLR не может быть произведена без посторонней помощи.

Такая помощь может быть нескольких видов. В браузере можно использовать JavaScript или Flash — и то и другое содержит JIT-компиляторы генерирующие исполнимый код — для заполнения памяти аккуратно сконструированным исполнимым кодом. Это создаёт что-то вроде большой посадочной полосы, приём под названием «heap spraying» («напыление кучи»). Другим подходом может быть нахождение вторичного бага, позволяющего раскрыть адреса библиотек или стека в памяти, давая атакующему достаточно информации для создания специфичного набора возвратных адресов для ROP.

Третий подход также был популярен в браузерах: использовать библиотеки, не умеющие ASLR. Старые версии, например, плагинов Adobe PDF или Microsoft Office не поддерживали ASLR, и Windows по умолчанию не форсирует ASLR. Если атакующий может вызвать загрузку такой библиотеки (например, загрузив PDF в скрытом фрейме браузера), то об ASLR можно уже не беспокоиться, а использовать эту библиотеку для целей ROP.

Война без конца

Между теми, кто эксплуатирует уязвимости и теми, кто защищает, идёт постоянная гонка вооружений. Мощные защитные системы, вроде ASLR и NX, поднимают планку, усложняя использование недостатков, и благодаря им мы оставили времена простого переполнения буфера позади, но умные атакующие могут найти комбинацию дыр и обойти эти защитные меры.

Эскалация продолжается. Набор Microsoft EMET («Enhanced Mitigation Experience Toolkit», «расширенный набор инструментов противодействия») включает ряд полу-экспериментальных средств защиты, которые могут обнаруживать heap spraying или попытки вызова определённых критичных функций в ROP-эксплойтах. Но в непрерывной цифровой войне, даже часть этих приёмов уже побеждена. Это не делает их бесполезными — сложность (а значит и цена) эксплуатации уязвимостей возрастает с каждым применённым средством противодействия — но это напоминание о необходимости постоянной бдительности.

fflush\flushall()\_ flushall() и т.д. не очистят буфер в C

Во всех программах, которые я должен выполнять для проекта, я сталкиваюсь с проблемой входного буфера, пока я очистил его с помощью цикла while, но мне сказали, что теперь этого не делать (вы можете видеть, что он прокомментирован).

Я попытался использовать flushall(), _flushall() и все альтернативы, которые предлагает Visual Studio, но ни один из них не разрезал его.

Что я делаю не так?

Реальная техника, в большинстве случаев, заключается не в том, чтобы вообще пытаться смыть stdin .

Для этого необходимо взаимодействовать с stdin одинаково, каждый раз. На практике это обычно не смешивает стили ввода.

Если использовать форматированный вход (например, scanf() ), никогда не используйте линейно-ориентированный ввод (например, fgets() ) или ориентированный на символы вход (например, getc() ) в том же потоке. Аналогично, если использовать fgets() для чтения, никогда не используйте форматированный ввод или символьно-ориентированный ввод в том же потоке.

Причина в том, что разные стили ввода реагируют по-разному на некоторые специфические символы — особенно на новую строку и пробелы. Когда вы смешиваете эти стили ввода, возникают нежелательные взаимодействия. scanf() , например, часто перестает читать, если встречает новую строку ( ‘\n’ ) и оставляет эту новую строку в потоке. fgets() с другой стороны, также остановится, как только он достигнет новой строки, но удалит эту новую строку из потока. Следовательно, если есть вызов scanf() за которым следует fgets(. stdin) , вызов fgets() часто возвращается немедленно — без чтения ожидаемых данных.

Кроме того, при использовании scanf() необходимо быть осторожным с выбором строк формата, чтобы избежать некоторых взаимодействий между последующими вызовами.

Илон Маск рекомендует:  Что такое код substr_count
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL