Я WittsEnd2, основатель Ragnar Security. Ранее в этом году я писал серию статей о реверс-инжиниринге baremetal-прошивки. Мы проанализировали и использовали прошивку Raspberry Pi Zero, разработанную для UMDCTF. Это дало нам понимание ограничений работы с устройствами, использующими этот тип прошивки. Теперь мы рассмотрим это с другой точки зрения: мы напишем собственную прошивку!

Почему мы работаем над написанием собственного встроенного ПО в блоге, посвященном кибербезопасности? Понимание того, как разработчики создают свое программное обеспечение, может помочь специалистам по безопасности определить, почему в жизненном цикле разработки возникают недостатки. Это помогает нам понять проблемы, связанные с ним, и может помочь найти ошибки / эксплойты раньше, чтобы их можно было смягчить. В результате мы станем лучше выполнять свою работу, имея несколько точек зрения.

Начнем!

Необходимые знания

Перед тем, как приступить к разработке встроенного ПО без операционной системы, необходимо освоить несколько навыков:

  • Понимание компилируемого языка: невозможно написать прошивку без ее компиляции. Поэтому знание того, как программировать на таких языках, как C, C++ и Rust, поможет создать хорошую основу для написания прошивки. В этом руководстве мы сосредоточимся конкретно на C.
  • Понимание языков ассемблера. Умение читать языки ассемблера поможет понять, что происходит во время выполнения вашего кода. Это становится особенно полезным при отладке и управлении определенными аспектами встроенного устройства (например, драйверами). В этом руководстве мы сосредоточимся на ARM и AARCH64 (64-разрядная версия ARM).
  • Понимание основ работы аппаратного обеспечения: встроенное ПО для встраиваемых систем отличается от встроенного ПО для стандартного компьютера. Некоторые входы и выходы не совпадают (например, последовательные контакты, JTAG и т. д.). Знание различных интерфейсов и того, как работает встроенная система, будет иметь решающее значение. В этом руководстве мы сосредоточимся на Raspberry Pi Zero и Raspberry Pi 3.

Настройка для проекта

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

  • Среда разработки Linux. Хотя это возможно в Windows, большинство встраиваемых систем используют Linux для компиляции прошивки. На самом деле, большинство встраиваемых операционных систем основаны на Unix, но мы не будем с ними работать, так как сосредоточимся на «голом железе».
  • Система сборки — это позволяет нам создавать прошивку после написания для нее кода.
  • Среда отладки — для этого мы будем использовать GDB; однако нам нужно удаленно подключиться к процессу, запускающему нашу прошивку. Мы напишем скрипт, который поможет нам в этом.
  • Среда разработки программного обеспечения — это полностью зависит от вас; однако я бы рекомендовал использовать Visual Studio Code или Vim.

Создание системы сборки

Система сборки автоматизирует/скриптует шаги, необходимые для создания прошивки. Поскольку это не простая программа на языке C, для создания встроенного программного обеспечения необходимо выполнить много шагов. В результате потребуется использование Make, GDB как для 32-битной, так и для 64-битной ARM (я использовал gdb-arm-none-eabi и gdb-aarch64-linux-gnu).

Makefiles имеют решающее значение, поскольку они определяют, как мы собираем нашу прошивку. В частности, они будут содержать команды для компиляции всех файлов C (преобразование их в объектные файлы, создание исполняемых файлов и т. д.). Также они будут содержать команды, необходимые для обработки образа прошивки (например, objcopy). Кроме того, мы можем использовать файлы Makefile, чтобы определить, компилируем ли мы для отладки или производства.

Отладочная среда

Есть несколько ключевых аспектов хорошей среды отладки:

  • Нам нужно, чтобы им было легко пользоваться
  • Нам нужно легко повторно использовать
  • Нам нужно быстро найти проблемы в нашей прошивке

Оптимальное решение — использовать QEMU (5.0+). QEMU — это эмулятор в Linux, и он может подключаться к GDB. В одной командной строке мы можем выполнить прошивку и выполнить ее шаг за шагом. Альтернативный подход заключается в использовании JTAG или последовательных портов для отладки; однако это более сложно, чем необходимо для разработки прошивки. Мы вернемся к этому в следующей статье (когда будем готовы к отладке оборудования).

QEMU 5.0+ имеет предварительно настроенные эмуляторы для Raspberry Pi от Zero до 3. Это позволяет нам запускать образ прошивки с одним параметром командной строки:

Из этого следует отметить три вещи:

  • Мы используем qemu-sytstem-aarch64, который содержит эмуляции встроенных устройств для 32- и 64-битных устройств.
  • -M указывает, какую машину вы хотите использовать. В данном случае примером является Raspberry Pi 3 B.
  • Нам нужно передать образ ядра и передать серийный номер в стандартный вывод. Это способ, которым изображение действительно запускается, и мы можем видеть результат. Существуют дополнительные опции для других видов вывода (например, stdio встроенной системы); однако в этом руководстве мы сосредоточимся на последовательном порте в QEMU.

Вот полные файлы Makefile, которые я использую. Это отличный шаблон для вас, чтобы начать создавать свои образы прошивки. Обратите внимание, что в этом примере Makefile я говорю компилятору не использовать какие-либо стандартные библиотеки. В результате нам придется реализовать такие вещи, как memset, malloc, printf и т. д.

Важные файлы и папки для этой среды

Прежде чем мы приступим к написанию кода, нам нужно понять, где что находится. В корневом каталоге у нас есть несколько ключевых файлов:

  • Скрипты компоновщика — в этом Makefile он называется link
  • Makefile — уже объяснено
  • debug.gdb/debug-gef.gdb — об этом позже
  • Common & Main folders — папки для кода
  • Include folder - папка для заголовков.

Сценарии компоновщика будут способом распределения памяти (используя компоновщик GNU). Здесь мы определим общие вещи, такие как раздел кода (.text), раздел BSS и т. д. Ниже приведен скрипт компоновщика, с которым мы будем работать в этом руководстве:

В папках Common, Main и Include мы будем выполнять большую часть нашей работы. Почему у нас три папки? Исключительно в организационных целях. Вы можете настроить make-файл так, чтобы он содержал сколько угодно (или несколько) папок (и решить, что в них находится). Вот почему у меня такая структура папок:

  • Common — здесь я храню весь код типа «стандартной библиотеки». Здесь у нас есть код, специфичный для реализации Raspberry Pi, такой как UART, и код, основанный на управлении памятью.
  • Main — В отличие от Common, я использую эту папку для написания фич для своей прошивки. Самый важный файл, который у меня есть в папке, это код моей загрузочной сборки (start.S).
  • Включает — сохраняет все заголовки как для Common, так и для Main.

Что теперь?

Теперь мы готовы приступить к созданию прошивки, которую мы начнем во второй части! Мы создадим прошивку «Hello World» и создадим некоторые служебные функции для использования. Поскольку это baremetal, программа «Hello World» потребует значительно больше усилий, чем обычно, поскольку нам нужно настроить последовательные соединения, некоторые функции обработки памяти и заголовки, которые будут управлять поведением оборудования Raspberry Pi.

Если вы еще этого не сделали, следите за нами на наших социальных платформах:

Твиттер: https://twitter.com/ragnarsecurity

Reddit: https://www.reddit.com/user/ragnarsecurity

Здесь, на Medium!