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

В этом посте я хочу дать вам представление о том, как, казалось бы, простая функция C может привести к нарушению выполнения программы. Мы рассмотрим, как использовать адрес возврата функции и возможные последствия, которые это может иметь для поведения программы.

Немного теории

В подзаголовке я упомянул эксплуатацию двоичных файлов — это широкая тема кибербезопасности, которая сводится к поиску уязвимости в программе и использованию ее для получения контроля над оболочкой или модификации функций программы. Если это вас не пугает, позвольте мне перефразировать это для вас. Если кому-то удастся эксплуатировать вашу систему с помощью методов бинарной эксплуатации, вы пиздец. Он или она не только получает доступ ко всей системе, но также может использовать ее для выполнения других вредоносных задач, таких как DDoS.

Чтобы продемонстрировать это на простом примере вызова функции в коде C, нам нужно кое-что знать о выполнении программы и «действующих лицах» за кулисами. Первая из — регистры.

Регистры

Существуют различные регистры, необходимые для выполнения программы, т. е. программные инструкции. Регистры — это сверхбыстрая память, в которой хранится несколько разных указателей, указывающих вперед и назад по всей памяти для выполнения инструкций программы:

  • Указатель стека (SP)указывает на вершину стека (наименьший числовой адрес) — нижняя часть стека находится по фиксированному адресу. Указатель стека (SP) также зависит от реализации. Он может указывать на последний адрес в стеке или на следующий свободный доступный адрес после стека.
  • Указатель кадра (FP), а также локальный базовый указатель (LB) указывает на начало кадра логического стека, который помещается и извлекается при вызове функции. В процессорах Intel указатель кадра упоминается как BP (EBP), который я также буду использовать в следующих разделах. Поскольку BP фиксирован, мы можем использовать его для обращения к переменным в кадре стека по смещению
  • Фрейм стека (SF) состоит из входных параметров функции (справа налево), адреса возврата, предыдущего BP и его локальных переменных
  • Указатель инструкции (IP) указывает на расположение следующей инструкции — хотя это и не совсем связано с «указателями стека», полезно упомянуть об этом, чтобы избежать путаницы

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

Есть много других регистров, но упомянутых достаточно для выполнения обсуждаемого нами эксплойта. Таким образом, на верхнем изображении показан снимок стека, но остается под вопросом, как этот стек растет или сжимается. Это подводит нас к следующему разделу.

Пролог функции

Я, вероятно, не совсем ошибусь, если утверждаю, что наиболее частая задача, которую выполняет ваш код, — это вызов функции. И перед каждым вызовом функции происходит то, что называется прологом функции. В программировании на языке ассемблера пролог функции представляет собой несколько строк кода в начале функции, которые подготавливают стек и регистры для использования внутри функции. А пролог звучит так:

1.) Сохраните СТАРЫЙ BP, поместив его в стек (SP соответственно обновится)

Зачем нужно сохранять СТАРЫЙ БП?

💡Поскольку после выхода из подпрограммы/функции ей необходимо иметь обратную ссылку на вызывающий кадр стека. Помните — на локальные переменные можно ссылаться, добавляя или вычитая смещение из BP.

Но разве не для этого нужен обратный адрес?

💡НЕТ. Адрес возврата не указывает на предыдущий кадр стека — он указывает на следующую инструкцию в вызывающем методе.

2.) Скопируйте SP в BP, чтобы создать новый BP — указывая на начало буфера локальных переменных в памяти.

3.) Передвиньте SP, чтобы зарезервировать место для кадра логического стека, а именно, локальные переменные функции

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

Взлом

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

Концепция выглядит следующим образом:

  1. Найти адрес возврата в стеке
  2. Создайте указатель на это место
  3. Изменить значение адреса возврата, т. е. на которое указывает указатель значения

Мы продемонстрируем это в следующем коде:

Это базовый код C, я уверен, что большинство из вас уже понимает. Основная идея состоит в том, чтобы создать указатель (строка 10)на ячейку памяти RETURN ADDRESS в стеке и изменить его значение, чтобы перепрыгнуть через строка 19. АДРЕС ВОЗВРАТА Место в памяти можно вычислить путем смещения места в памяти buffer1 — массива char введите переменные (строка 8). Это смещение можно рассчитать как сумму смещения от buffer1до BP и размера BP. Помогите себе, посмотрев на график выше.

💡Размер указателя в 64-битных системах составляет 8 байт, а в 32-битных — 4 байта.

Чтобы выяснить, каково смещение между buffer1 и BP, мы будем использовать GDB — GNU Debugger. Это не учебник по GDB, поэтому я просто перечислю команды, которые вы можете запустить, чтобы получить расположение в памяти обоих и вычислить смещение.

gcc binexploit1.c -g -o binexploit -fno-stack-protector # compile
gdb binexploit # open GDB shell
(gdb) break function # set a breakpoint
(gdb) run # run code to the breakpoint
(gdb) step # take the next step
(gdb) print/x &buffer1 # print memory address of buffer1
(gdb) info registers # print register information - in the output look for rbp row and the memory address it is pointing to

Теперь, когда у нас есть оба этих значения, мы можем рассчитать смещение между ними с помощью простой шестнадцатеричной арифметики (используйте Hex Calculator, если необходимо) и добавить размер BP > (в моем случае 8 байт, поскольку я запускаю его на 64-разрядной машине). Тогда это наше значение X в верхней части скрипта.

Почему мы берем сумму buffer1 + Xи не вычитаем?

💡Стек растет вниз — в сторону младших адресов. Именно так это реализовано на многих компьютерах, включая процессоры Intel, Motorola, SPARC и MIPS.

Затем нам нужно изменить значение адреса возврата, чтобы перепрыгнуть через присваивание x = 1;(строка 19) и вывести "x: 0”в следующей строке. Поскольку у нас уже есть указатель ret, указывающий на ячейку памяти RETURN ADDRESS, нам просто нужно изменить его значение так, чтобы оно было строкой 20, а не строкой 19 (в коде). Проще говоря, мы могли бы сделать это, сначала установив Y в ноль и увеличивая его до тех пор, пока нужные строки не будут пропущены, или мы можем дизассемблировать память с помощью GDB — это на ваше усмотрение. Вот команды, которые нужно запустить в GDB:

gdb binexploit # open GDB shell
(gdb) disassemble main # disassemble main function memory

Вывод должен быть похож на этот:

Где мы можем видеть смещение между строкой 10 (операция присваивания) и строкой 11 («подготовка» для функции печати) составляет 7 байтов, что составляет наше значение Y.

💡Вышеприведенный фрагмент кода, или я должен сказать, вывод — это код сборки, который ваша конкретная архитектура ЦП способна понять и выполнить. Возможно, вам захочется посмотреть глубже, чтобы лучше понять, как я определил, где начинаются операторы printf.

Обратите внимание, что такие атаки в реальном мире обычно легко обнаруживаются и предотвращаются, но тем не менее, я нахожу это особенно интересным, если вы хотите узнать кое-что о стеке. Точно так же мы могли бы добиться того же, используя память кучи, но я оставлю эти страдания вам 😉

Вот и все для этого поста — я готовлю вторую часть этого поста, где мы рассмотрим аналогичный подход, где мы не только откладываем выполнение до более позднего этапа программы, но также выполняем произвольную команду, например запуск оболочка или скрипт Python.

Вместо заключения

Прежде чем я закончу этот урок, я хочу бросить вам вызов:

Встроке 12попробуйте изменитьbuffer1наbuffer2, а затем соответственно обновите X и Y, чтобы добиться того же результата, что и мы ("x: 0”).

Дайте мне знать ваш результат в комментарии.

Спасибо за прочтение! 😎 Если вам понравилась эта статья, нажмите кнопку хлопка ниже 👏

Это много значит для меня, и это помогает другим людям увидеть историю. Скажи привет на Linkedin | Твиттер

Хотите начать читать эксклюзивные истории на Medium? Используйте эту реферальную ссылку 🔗

Если вам понравился мой пост, вы можете купить мне хот-дог 🌭

Вы инженер-энтузиаст, которому не хватает возможности составить убедительный и вдохновляющий технический контент? Наймите меня на Upwork 🛠️

Ознакомьтесь с остальными моими материалами на Teodor J. Podobnik, @dorkamotorka и подпишитесь на меня, чтобы не пропустить новые, ура!

Из статей Infosec: Каждый день в Infosec появляется много информации, за которой трудно уследить. Подпишитесь на нашу еженедельную рассылку, чтобы БЕСПЛАТНО получать все последние тенденции в области информационной безопасности в виде 5 статей, 4 тем, 3 видео, 2 репозиториев и инструментов GitHub и 1 оповещения о вакансиях!