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

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

English version

О глобальной области памяти (global memory)

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

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

Те переменные, которые не инициализированы явно (то есть им не присваивается никакое значение в момент объявления), располагаются в BSS сегменте, после чего они автоматически заполняются нулями.

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

         старшие адреса
+-----------------------------+
| параметры командной строки  |
| и переменные окружения      |
+-----------------------------+
|           стек              |
+-------------+---------------+
|             |               |
|             V               |
|                             |
|                             |
|                             |
|                             |
|             ^               |
|             |               |
+-------------+---------------|
|            куча             |
+-----------------------------+ 
| неинициализированные данные |
|       (BSS сегмент)         |
|     (заполнено нулями)      |
+-----------------------------+
|  инициализированные данные  |
+-----------------------------+
|            код              |
+-----------------------------+
        младшие адреса

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

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

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

Как обычно, все зависит, от того, что именно и как переполняется. Вот несколько типичных факторов:

  • какие данные хранятся в памяти рядом с переполняемым буфером
  • можем ли мы писать в память рядом с переполняемым буфером
  • можем ли мы читать из памяти рядом с переполняемым буфером

Посмотрим на несколько примеров.

Перезапись важных данных

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

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

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

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

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

Здесь есть еще одна интересная деталь. Проблема уйдет, если мы явно инициализируем переменную access при ее объявлении:

char access = 'n';

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

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

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

Сначала определяем buffer в глобальной области памяти и allocated в куче. Потом копируем строку “test” в буфер allocated. Дальше печатаем адреса и содержимое буферов. И в самом конце копируем первый параметр командной строки в глобальный буфер и опять печатаем allocated.
На моем Линуксе следующая команда вызывает segfault:

artem@artem-laptop:~/tmp$ ./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)

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

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

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

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

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

artem@artem-laptop:~/tmp$ gcc -g gbo.c -o gbo
artem@artem-laptop:~/tmp$ ./gbo `python3 -c "print('w' * 256)"`
Segmentation fault (core dumped)

Мы только что записали в func адрес 0x77777777 (0x77 это ASCII-код символа ‘w’). Далее наше наивное приложение попыталось вызвать функцию, которая располагается по этому адресу. Так как этот адрес некорректный, мы получили segfault. Но простое падение приложение это не интересно. Интереснее заставить приложение выполнить то, что мы хотим. Предположим, что мы хотим вызвать функцию print_secret , но мы не знаем пароля. Сначала выясним адрес функции print_secret. GDB поможет нам с этим:

artem@artem-laptop:~/tmp$ 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. Дальше нам нужно передать приложению такую строку, чтобы оно записало адрес 0x400607 в указатель func. Для этого надо учесть следующее:

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

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

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

Чтение конфиденциальных данных из глобальной памяти

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

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

Как предотвратить переполнение буфера

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

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Share

Leave a Reply

avatar
  Subscribe  
Notify of