И как печать строки привела к выполнению кода? !!

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

В этом посте мы рассмотрим уязвимости строки формата. Это своего рода уязвимость, которая использует функцию строки формата для утечки информации, выполнения кода и DoS.

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

Что такое строка формата?

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

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

Например, следующий фрагмент кода показывает, как работает printf () на языке C. Оператор будет выводить разные предложения в зависимости от того, что содержится в переменной name.

printf(“Hello, my name is %s.”, name);

Если переменная name содержит строку «Vickie», инструкция printf () выведет:

Hello, my name is Vickie.

Функции и параметры форматирования

Помимо printf (), существует ряд функций форматирования, которые используют строки формата для вывода. Например, в C есть printf (), fprintf (), sprintf (), snprintf (), printf_s (), fprintf_s (), sprintf_s (), snprintf_s () и так далее. Дополнительную информацию об этих функциях см. В C docs.

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

  • % d - для целых чисел со знаком в десятичной системе счисления,
  • % u для целого числа без знака в десятичной системе счисления,
  • % x - это целое число без знака в шестнадцатеричном формате,
  • а % s указывает, что данные должны быть указателем на строку.

В соответствии с форматом данных, продиктованным спецификаторами формата, функции форматирования извлекают запрошенные данные из стека.

printf(“A is the number %d, B is the string %s”, A, &B);

Вышеупомянутая функция printf () попытается получить значение A и адрес строки B из стека.

Как можно использовать функции форматирования

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

printf(“If the attacker can control me, you’re in trouble.”, A, B);

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

Небольшая заметка о стеке

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

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

Утечка информации о программе с использованием функций форматирования

Так что же происходит, когда злоумышленник предоставляет больше спецификаторов формата, чем аргументов функции, чтобы заполнить его место? Когда есть два спецификатора формата, но только один аргумент функции для предоставления значения, что будет делать printf ()?

printf(“A is the number %d, reading stack data: %x”, A);

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

Таким образом, злоумышленник может просто предоставить следующую строку для чтения больших объемов данных в стеке:

printf(“%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x”); 
-> will print the next 20 items on the stack

Злоумышленник может даже получить прямой доступ к аргументу i th в стеке, используя специальный спецификатор формата case:

printf("%10$x"); 
-> will print the tenth element next on the stack

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

Чтение данных из любого места в памяти

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

Но как вы контролируете адрес, к которому обращается % s? Вам нужно будет поместить адрес в стек и произвести % s разыменование этого адреса!

И удобно, что строка формата (которую вы полностью контролируете) хранится в стеке! Таким образом, если вы можете поместить адрес в строку формата и получить % s для его разыменования, вы сможете получить доступ к данным за пределами стека.

printf(“\xef\xbe\xad\xde%x%x%x%s”, A, B, C);

Например, приведенное выше приведет к тому, что printf () напечатает строку, расположенную по адресу 0xdeadbeef. Серия % x используется для перемещения по стеку к месту строки формата, а необходимое количество % x будет варьироваться от случая к случаю. % s указывает printf () обрабатывать первые четыре байта строки формата как указатель на строку, которая будет напечатана.

Эта инфографика слева показывает, как этот стек выглядит во время выполнения функции printf () выше.

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

Предоставляя дополнительный % s, злоумышленник заставляет printf () получить доступ к еще одному значению из стека и рассматривать его как 4-байтовый указатель на строку.

Итак, printf () выводит строку, расположенную по адресу 0xdeadbeef, который является адресом, указанным первыми четырьмя байтами нашей строки формата.

Перезапись памяти в любом месте

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

Например, следующий код сохранит целое число 5 в переменной num_char.

int num_char; 
printf(“11111%n”, &num_char);

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

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

int num_char;
printf(“%10d%n”, 0, &num_char); 
-> will print "          0", num_char is 10

А с помощью модификаторов длины злоумышленники могут контролировать объем записываемых данных с помощью точного управления. Например, % n запишет 4 байта в целевой адрес, а % hn только 2 байта.

printf(“%10d%n”, 0, &num_char); -> writes 4 bytes to &num_char
printf(“%10d%hn”, 0, &num_char); -> writes 2 bytes to &num_char

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

Сбой программы с использованием функций форматирования

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

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

Строка эксплойта будет выглядеть примерно так:

printf (“%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s”);

Чем больше % s содержится в строке формата, тем выше вероятность обнаружения недопустимого адреса.

Предотвращение атак на строку формата

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

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

Короче говоря, пользовательский ввод должен идти сюда:

printf(“string: %s”, USER_INPUT);

И не здесь:

printf(USER_INPUT, A, B);

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

Заключение

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

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