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

Где живут глобальные буферы?

Есть два места, где могут быть расположены глобальные и статические переменные:

  • сегмент для инициализированных данных
  • сегмент для неинициализированных данных (сегмент BSS)

Те переменные, которые не имеют явного инициализатора, переходят в сегмент BSS и автоматически инициализируются нулями.

Вот как выглядит память:

high address
+-----------------------------+
| command line arguments and  |
| environment variables       |
+-----------------------------+
|           stack             |
+-------------+---------------+
|             |               |
|             V               |
|                             |
|                             |
|                             |
|                             |
|             ^               |
|             |               |
+-------------+---------------|
|            heap             |
+-----------------------------+
|   uninitialized data (BSS)  |
|    (initialized to zero)    |
+-----------------------------+
|       initialized data      |
+-----------------------------+
|            text             |
+-----------------------------+
          low address

Пример глобального переполнения буфера

Вот очень простой пример глобального переполнения буфера:

Как я могу использовать глобальное переполнение буфера?

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

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

Вот простая программа, которая принимает парольную фразу и печатает секретную фразу, если пароль правильный:

strcpy вызов переполняется buffer, если пароль содержит более 15 символов ( strcpy добавляет \0 в конец строки). В результате мы можем перезаписать флаг access:

(Давайте воспользуемся Python для генерации некоторых строк)

$ gcc -g gbo.c -o gbo
$ ./gbo wrong
access denied
$ ./gbo `python3 -c "print('x' * 16 + 'y')"`
this is a secret

Это происходит потому, что обе переменные buffer и access находятся в сегменте для неинициализированных данных, а access следует в памяти за buffer. Но фактическое расположение этих переменных может зависеть от того, что порядок глобальных переменных в памяти не определен. Так что приведенный выше код может быть неуязвим.
Есть интересная деталь — проблема исчезнет, ​​если мы инициализируем флаг access значением при его объявлении:

char access = 'n';

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

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

Обычно куча начинается где-то после сегментов данных и BSS. Но фактический адрес может отличаться. Рассмотрим следующий код:

Он определяет глобальный буфер buffer и буфер allocated в куче. Затем он копирует тестовую строку в буфер allocated. Затем он выводит адреса и содержимое буферов. Наконец, он копирует первый параметр командной строки в глобальный буфер и снова выводит allocated.
На моем ноутбуке с Linux следующая команда вызывает segfault:

$ ./gbo `python3 -c "print('x' * 2**12)"`
buffer address     = 0x601070
allocated address  = 0x773010
allocated address - buffer address = 1515424
allocated (before) = test
Segmentation fault (core dumped)

Здесь мы пытаемся поместить строку из 4096 символов «x» в buffer, которая переполняет ее. Адрес allocated равен 0x773010, но он меняется каждый раз при запуске программы из-за динамического выделения памяти. Обратите внимание, что разница между адресами намного больше (1515424), чем 4096. В результате происходит сбой, потому что мы пытаемся записать на недопустимый адрес. Невозможно перезаписать объекты в куче, если мы можем переполнить глобальный буфер. Но может быть возможно просто сбой приложения.

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

Указатель функции содержит адрес функции. Указатель функции может использоваться для вызова функции. Довольно просто. Вот простой пример с перезаписью указателя функции:

Программа аналогична приведенному выше примеру с флагом access, но вместо установки флага она играет с указателем на функцию. Во-первых, указатель функции не инициализирован. Затем он помещает адрес do_something в указатель func. Если указанный пароль правильный, он помещает адрес функции print_secret в указатель func. Наконец, он вызывает функцию, адрес которой был помещен в func.

strcpy вызов переполняется buffer, если длина указанного параметра командной строки превышает 15 символов ( strcpy добавляет \0 в конец строки). Поскольку и func, и buffer не инициализированы, они находятся в одном и том же сегменте данных для неинициализированных данных. В результате мы можем перезаписать указатель функции func:

$ gcc -g gbo.c -o gbo
$ ./gbo `python3 -c "print('w' * 256)"`
Segmentation fault (core dumped)

Мы только что перезаписали указатель функции func адресом 0x77777777 (0x77 — это ASCII-код 'w'). Затем программа попыталась вызвать функцию по этому адресу. Поскольку указатель функции указывает на недопустимый адрес, это привело к ошибке сегментации. Но просто крах — это не весело. Гораздо интереснее заставить программу запускать то, что мы хотим. Давайте сделаем так, чтобы она вызывала функцию print_secret, даже если мы передаем неверный пароль. Во-первых, мы должны выяснить, каков адрес функции print_secret. GDB может помочь нам здесь:

$ gdb --args ./gbo test
Reading symbols from ./gbo...done.
(gdb) break gbo.c:36
Breakpoint 1 at 0x40068c: file gbo.c, line 36.
(gdb) run
Starting program: /home/artem/tmp/gbo test

Breakpoint 1, main (argc=2, argv=0x7fffffffdcf8) at gbo.c:36
36	    func();
(gdb) p func
$1 = (void (*)(void)) 0x7777777777777777
(gdb) p print_secret 
$2 = {void (void)} 0x400607 
(gdb) quit

Теперь мы знаем адрес функции print_secret — это 0x400607. Затем нам нужно передать такую ​​строку программе, чтобы она перезаписала указатель func на 0x400607. Нам необходимо принять во внимание следующее:

  • Нам нужно сначала записать 16 байт, чтобы заполнить buffer
  • Нам нужно помнить, что мы находимся в 64-битной системе (в моем случае), поэтому нам нужно записать 8 байт, чтобы перезаписать указатель func
  • Нам нужно помнить, что мы находимся в системе с прямым порядком байтов (в моем случае), поэтому 0x400607 следует перевернуть — 0x070640

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

$ ./gbo `python3 -c "print('w' * 16 + '\x00\x00\x00\x00\x00\x07\x06\x40')"`
this is a secret

Чтение конфиденциальных данных

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

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

Смягчение

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

Первоначально опубликовано на https://blog.gypsyengineer.com 18 марта 2017 г.