Бинарные уязвимости/Off-by-one: различия между версиями
Dzeni (обсуждение | вклад) м (→Эксплуатация) |
Dzeni (обсуждение | вклад) м (→Реализация) |
||
Строка 58: | Строка 58: | ||
Для начала проверяется возможность перезаписи младших разрядов регистра '''EBP'''. | Для начала проверяется возможность перезаписи младших разрядов регистра '''EBP'''. | ||
Например, через вывоз ошибки. | Например, через вывоз ошибки. | ||
+ | |||
[[Файл:Step-1.JPG|650px|border|эксплуатация off-by-one]] | [[Файл:Step-1.JPG|650px|border|эксплуатация off-by-one]] | ||
Текущая версия на 04:44, 29 ноября 2022
Содержание
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. Рассмотрим стековые кадры этих функций.
Эксплуатация
Способ эксплуатации данной уязвимости заключается в том, передать строку длиной 256, которая содержит новый адрес возврата на shellcode и сам shellcode. Таким образом, при копировании в buf, будет изменен EBP (младший бит затерт нулем), регистр станет указывать выше по стеку (в buf) на адрес возврата, указывающий на shell code.
Реализация
(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.
Например, через вывоз ошибки.
Или можно рассмотреть значение регистра EBP для строк длины 255 и 256, поставив точку останова после вызова strcpy. В первом случает значение EBP будет 0xffffd61c.
А во втором - 0xffffd600. Значит новый адрес возврата, расположенный внутри buf лежит по адресу 0xffffd604
Далее необходимо вычислить смещение нового адреса возврата относительно buf. Сам buf расположен на стеке по адресу 0xffffd510.
Итого, получим смещение 0xffffd604-0xffffd510=0xf4. Проверим вычисления, переписав адрес возврата на "BBBB".
Последним шагом необходимо собрать эксплоит, заменив адрес возврата на адрес внутри buf где будет лежать shell code.
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!");
будет последовательно происходить следующее:
- при вызове межмодульной (библиотечной) функции произойдет обращение к соответствующей записи в .plt
- обращение к соответствующей записи в .got.plt в поисках адреса; адрес будет не найден (т.к. это первый вызов данной функции)
- будет запущен код поиска адреса, найденный адрес записан в .dot.plt, вызвана функция puts
При вызове
puts("Hi world!");
будет последовательно происходить следующее:
- при вызове межмодульной (библиотечной) функции произойдет обращение к соответствующей записи в .plt
- обращение к соответствующей записи в .got.plt в поисках адреса; адрес будет найден
- передача адреса, возврат в .plt
- вызов resolver для вызова функции puts
Уязвимость
Уязвимость в данном случае может появиться, если у атакующего есть возможность перезаписать адрес функции в .got.plt. Заменив адрес возврата, на адрес контролируемой памяти, атакующий может добиться выполнения произвольного кода, при повторном вызове межмодульной функции.
Relocation read only(RELRO)
Для защиты от данной уязвимости был введен механизм RELRO, ограничивающий права на запись. Он имеет два уровня: частичный (ограничивает запись в .got) и полный (ограничивает также запись в .got.plt)