Бинарные уязвимости/Off-by-one: различия между версиями

Материал из SecSem Wiki
Перейти к навигации Перейти к поиску
м (Off-by-one)
м (Off-by-one)
Строка 29: Строка 29:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
В данном коде вызывается функция '''main''', которая проверяет длину входа и вызывает функцию '''foo'''. Функция '''foo''' вызывает функцию '''bar'''. В функции '''bar''' определяется локальный массив '''buf''', размещающийся на стеке и содержащий 256 элементов, и функция копирует аргумент '''arg''' с помощью функции '''strcpy'''. Данная функция копирует '''arg''', переданный во втором аргументе, в '''buf''' до тех пор пока не встретит в ней нулевой байт. При этом сам нулевой байт также будет скопирован, и может быть затереть младший бит '''EBP'''.
+
В данном коде вызывается функция '''main''', которая проверяет длину входа и вызывает функцию '''foo'''. Функция '''foo''' вызывает функцию '''bar'''. В функции '''bar''' определяется локальный массив '''buf''', размещающийся на стеке и содержащий 256 элементов, и функция копирует аргумент '''arg''' с помощью функции '''strcpy'''. Данная функция копирует '''arg''', переданный во втором аргументе, в '''buf''' до тех пор пока не встретит в ней нулевой байт. При этом сам нулевой байт также будет скопирован, и может затереть младший бит '''EBP'''.
 
Рассмотрим стековые кадры этих функций.
 
Рассмотрим стековые кадры этих функций.
  

Версия 11:03, 26 ноября 2021

Off-by-one

Копирование исходной строки в целевой буфер может привести к off-by-one на стеке, если длина исходной строки равна длине целевого буфера. Когда длины совпадают, единственный NULL байт копируется выше целевого буфера. Поскольку целевой буфер расположен в стеке, то единственный NILL байт может перезаписать младший бит EBP, хранящийся в стеке, что может привести к выполнению произвольного кода.

Рассмотрим пример уязвимого кода

void foo(char* arg);
void bar(char* arg);

void foo(char* arg) {
 bar(arg); /* [1] */
}

void bar(char* arg) {
 char buf[256];
 strcpy(buf, arg); /* [2] */
}

int main(int argc, char *argv[]) {
 if(strlen(argv[1])>256) { /* [3] */
  printf("Attempted Buffer Overflow\n");
  fflush(stdout);
  return -1;
 }
 foo(argv[1]); /* [4] */
 return 0;
}

В данном коде вызывается функция main, которая проверяет длину входа и вызывает функцию foo. Функция foo вызывает функцию bar. В функции bar определяется локальный массив buf, размещающийся на стеке и содержащий 256 элементов, и функция копирует аргумент arg с помощью функции strcpy. Данная функция копирует arg, переданный во втором аргументе, в buf до тех пор пока не встретит в ней нулевой байт. При этом сам нулевой байт также будет скопирован, и может затереть младший бит EBP. Рассмотрим стековые кадры этих функций.

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

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

Способ эксплуатации данной уязвимости заключается в том, передать строку длиной 256, которая содержит новый адрес возврата на shellcode и сам shellcode. Таким образом, при копировании в buf, будет изменен EBP (младший бит затерт нулем), регистр станет указывать выше по стеку (в buf) на адрес возврата, указывающий на shell code.

эксплуатация off-by-one


GOT/PLT

в любой системе есть два типа двоичных файлов: статически связанные и динамически связанные. Статически связанные двоичные файлы являются самодостаточными, содержат весь код, необходимый для их работы в одном файле, и не зависят от каких-либо внешних библиотек. Динамически подключаемые двоичные файлы не включают в себя множество функций, но полагаются на системные библиотеки для обеспечения части функций. Например, когда ваш двоичный файл использует puts для печати некоторых данных, фактическая реализация puts является частью системной библиотеки C.

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

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

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

Для реализации relocation в ELF файлах есть несколько специальных разделов

  • .got - (Global Offset Table) таблица смещений для всех внешних символов
  • .plt - (Procedure Linkage Table) таблица привязки процедур. заглушки, которые ищут адрес в .got.plt и либо переходят по адресу(ели он найден), либо запускают код поиска адреса.
  • .got.plt - got для plt. содержит либо целевые адреса (если они были записаны), либо адрес обратно в .plt для запуска поиска.

Рассмотрим пример кода.

int main(int argc, char **argv) {
  puts("Hello world!");
  puts("Hi world!");
  exit(0);
}

При вызове

puts("Hello world!");

будет последовательно происходить следующее:

puts-1

  1. при вызове межмодульной (библиотечной) функции произойдет обращение к соответствующей записи в .plt
  2. обращение к соответствующей записи в .got.plt в поисках адреса; адрес будет не найден (т.к. это первый вызов данной функции)
  3. будет запущен код поиска адреса, найденный адрес записан в .dot.plt, вызвана функция puts

При вызове

puts("Hi world!");

будет последовательно происходить следующее:

puts-1

  1. при вызове межмодульной (библиотечной) функции произойдет обращение к соответствующей записи в .plt
  2. обращение к соответствующей записи в .got.plt в поисках адреса; адрес будет найден
  3. передача адреса, возврат в .plt
  4. вызов resolver для вызова функции puts

Уязвимость

Уязвимость в данном случае может появиться, если у атакующего есть возможность перезаписать адрес функции в .got.plt. Заменив адрес возврата, на адрес контролируемой памяти, атакующий может добиться выполнения произвольного кода, при повторном вызове межмодульной функции.

Relocation read only(RELRO)

Для защиты от данной уязвимости был введен механизм RELRO, ограничивающий права на запись. Он имеет два уровня: частичный (ограничивает запись в .got) и полный (ограничивает также запись в .got.plt)

Ссылки