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

Материал из SecSem Wiki
Перейти к навигации Перейти к поиску

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

Реализация

(gdb) disassemble bar 
Dump of assembler code for function bar:
   0x080484f7 <+0>:	push   %ebp
   0x080484f8 <+1>:	mov    %esp,%ebp
   0x080484fa <+3>:	sub    $0x100,%esp
   0x08048500 <+9>:	pushl  0x8(%ebp)
   0x08048503 <+12>:	lea    -0x100(%ebp),%eax
   0x08048509 <+18>:	push   %eax
   0x0804850a <+19>:	call   0x8048380 <strcpy@plt>
   0x0804850f <+24>:	add    $0x8,%esp
   0x08048512 <+27>:	nop
   0x08048513 <+28>:	leave  
   0x08048514 <+29>:


Для начала проверяется возможность перезаписи младших разрядов регистра EBP. Например, через вывоз ошибки.

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

Или можно рассмотреть значение регистра EBP для строк длины 255 и 256, поставив точку останова после вызова strcpy. В первом случает значение EBP будет 0xffffd61c.

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

А во втором - 0xffffd600. Значит новый адрес возврата, расположенный внутри buf лежит по адресу 0xffffd604

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

Далее необходимо вычислить смещение нового адреса возврата относительно buf. Сам buf расположен на стеке по адресу 0xffffd510.

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

Итого, получим смещение 0xffffd604-0xffffd510=0xf4. Проверим вычисления, переписав адрес возврата на "BBBB".

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

Последним шагом необходимо собрать эксплоит, заменив адрес возврата на адрес внутри 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)

Ссылки