Бинарные уязвимости/Stack overflow

Материал из SecSem Wiki
Версия от 14:18, 27 ноября 2020; Dzeni (обсуждение | вклад) (Способы защиты)
Перейти к навигации Перейти к поиску

Адресное пространство процесса

Пример содержимого адресного пространства процесса на x86

Адресное пространство процесса на x86/amd64 - это совокупность виртуальных адресов, доступная программе. Размер адресного пространства на x86 без дополнительных способов расширения - 4 Гб, он разделённый на kernel-space (2/1 Гб) и user-space (2/3 Гб). На x86-64 размер адресного пространства 2**48 - старшие 16 бит адреса все равны или 0, или 1. Такие адреса называются каноничными, все другие - неканоничными. В случае попытки обращения к неканоничному адресу возникает general protection exception (#GP). В случае x86-64 каноничность адресов можно использовать при проведении анализа содержимого памяти (так адрес из адресного пространства ядра будет иметь префикс 0xFFF, а из пользовательского - 0x000).

В *nix в user-space части адресного пространства содержится:
- запускаемый исполняемый файл
- динамические *.so библиотеки
- mmap() области (анонимные аллокации и отмапленные файлы)
- стек
- куча
- отмапленные из ядра области (vsyscall/vvar/vdso)
- различные служебные структуры

В pwndbg/gef/peda содержимое адресного пространства можно посмотреть с помощью команды vmmap:

Вывод команды vmmap в pwndbg

В gdb можно использовать команду info proc map, а без отладчика содержимое можно посмотреть через файловую систему /proc с помощью команды cat /proc/<self>/maps.


Stack buffer overflow

Переполнение буфера в стеке происходит, когда программа не проверяет должным образом размер буфера, выделенного на стеке, при записи в него. Например, так делают известные функции gets, strcpy. Рассмотрим пример функции с таким типом уязвимости:

void foo(char *arg) {
  char local[256];
  strcpy(local, arg);
}

В данной функции определяется локальный массив local, размещающийся на стеке и содержащий 256 элементов, и функция копирует аргумент arg с помощью функции strcpy. Данная функция копирует arg, переданный во втором аргументе, в local до тех пор пока не встретит в ней нулевой байт. При этом сам нулевой байт также будет скопирован. Чтобы понять, почему это может быть опасно, нужно рассмотреть содержимое стекового кадра функции foo. Предположим, что мы скомпилировали данный код в 32-битную программу и соглашение о вызовах функции - cdecl. Тогда стековый кадр будет выглядеть так:

Стековый кадр функции foo

На данной схеме стрелочкой показано направление роста стека в адресном пространстве (от старших к младшим адресам). Далее последовательно в стеке располагаются:
- аргумент функции foo
- адрес возврата - адрес, на который перейдет управление после окончания исполнения функции foo
- значение регистра ebp, являющееся указателем стекового кадра вызывающей foo функции
- опциональный паддинг (обычно используется, что локальные аргументы были выравнены по 16 байт)
- локальный массив local

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

Эксплуатация

Самый простой способ эксплуатации данной уязвимости заключается в том, чтобы поместить в стек шеллкод (набор опкодов, которые исполняют нужное нам, например, вызывают командный интерпретатор/bin/sh) и записать в качестве адреса возврата его адрес: Возможный способ эксплуатации уязвимости в функции foo

Тогда после завершения функции foo управление перейдет на шеллкод.


Способы защиты

Существует большое количество различных средств защиты от эксплойтов (exploit mitigations) как на уровне компилятора, так и операционной системы. Рассмотрим некоторые их них.

Stack guard value

Guard value (cookie/canary) в общем смысле - это значение, которое помещается между буфером и данными, которые нужно защитить. Таким образом, любое переполнение буфера ведёт к перетиранию этого значения. И перед любым использованием защищаемых данных необходимо проверить это значение. В случае стека данное значение помещается между локальными данными и сохраненным регистром (r/e)bp: Место размещения stack cookie в стековом кадре функции foo

Заметим, что было неправильно помещать его между адресом возврата и (r/e)bp, т.к. его изменение тоже может привести к эксплуатации. Stack guard value также называется stack cookie, stack canary или stack smashing protector.

Данное значение обычно формируется один раз случайным образом (при этом почти всегда содержит нулевой байт) при запуске программы и сохраняется в структуру под названием thread control block (TCB). Указатель на данную структуру хранится в регистре fs в случае x86-64 и gs в x86. В pwndbg есть специальные команды gsbase/fsbase, которые можно использовать для просмотра значения в данных регистрах:

Пример использования команды gsbase для просмотра stack cookie
Пример использования команды fsbase для просмотра stack cookie









Также для получения содержимого данных регистров может использоваться метод, описанный здесь. Из данных рисунков видно, что TCB замаплен с правами на запись - это означает, что при наличии соответствующей уязвимости и знания адреса TCB можно переписать stack cookie.

Теперь рассмотрим, как же именно используется stack cookie: в прологе функции stack cookie сохраняется в стек сразу после (r/e)bp и проверяется перед выходом из функции. Ниже приведены ассемблерные листинги функции foo, скомпилированной соответственно без и с stack guard value:

Дизассемблированный листинг фукнции foo без stack cookie
Дизассемблированный листинг фукнции foo со stack cookie















Как видно из листинга справа в прологе функции на стек сохраняется значение из регистра gs по смещению 0x14 - как раз по данному смещению в случае x86 располагается поле stack_guard, содержащие cookie. В эпилоге функции перед выполнением инструкции ret cookie, сохраненные на стеке, сравниваются со значением, сохраненным в TCB. В случае, если они не совпадают, выполняется функция __stack_check_fail, выводящая сообщение об ошибке и завершающая программу.

ASLR

Чтобы затруднить атаки класса переиспользования кода (code reuse), такие как возврат в libc, адреса стека, кучи, и загрузки библиотек рандомизируются при каждом запуске программы. Это называется address space layout randomization.

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

Как и многие другие защиты, ASLR не является абсолютной. Если в программе есть баг, позволяющий читать память за границей массива на стеке, с большой вероятностью вы там найдете адрес, ведущий в libc. Например, функция main возвращается куда-то в недра __libc_start_main, и если получится узнать адрес возврата, то посчитав смещение этого места относительно начала libc (для этой конкретной версии libc!), вы сможете узнать адрес загрузки библиотеки - и, соответственно, адреса всех функций, смещения которых фиксированы относительно базового адреса загрузки.

NX

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

На уровне пользовательской программы это реализовано флагом prot в системных вызовах mmap и mprotect. Сам пользовательский код редко использует эти системные вызовы напрямую: ядро системы, динамический загрузчик (ld.so) и libc сами выделяют память с правильными правами доступа.

Фактически это означает, что чтобы в таких условиях добиться выполнения произвольного кода (или хотя бы выходящего за спецификацию исходной программы), придется провести атаку переиспользования кода (code reuse) в том или ином виде. Например, переписать указатель на какую-то безобидную функцую на system.

Safe stack

Safe stack – механизм сохранения указателей (адреса возврата, указатели на функции и т.п.) в отдельной изолированной области памяти, доступ к которой производится только с использованием специальных проверок корректности обращения к памяти.

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

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


Ссылки