Главная » Хабрахабр » Разрабатываем процессорный модуль NIOS II для IDA Pro

Разрабатываем процессорный модуль NIOS II для IDA Pro

image

Скриншот интерфейса дизассемблера IDA Pro

Мы в Positive Technologies также применяем этот инструмент. IDA Pro — знаменитый дизассемблер, который уже много лет используют исследователи информационной безопасности во всем мире. Более того, нам удалось разработать собственный процессорный модуль дизассемблера для микропроцессорной архитектуры NIOS II, который повышает скорость и удобство анализа кода.

Сегодня я расскажу об истории этого проекта и покажу, что получилось в итоге.

Предыстория

Все началось в 2016 году, когда для анализа прошивки в одной задаче нам пришлось разработать собственный процессорный модуль. Разработка велась с нуля по мануалу Nios II Classic Processor Reference Guide, который тогда был наиболее актуальным. Всего на эту работу ушло около двух недель.

9. Процессорный модуль разрабатывался для версии IDA 6. В месте, где обитают процессорные модули, — подкаталоге procs внутри установочного каталога IDA Pro — есть три модуля на Python: msp430, ebc, spu. Для скорости был выбран IDA Python. В них можно подсмотреть, как устроен модуль и как может быть реализована базовая функциональность дизассемблирования:

  • разбор инструкций и операндов,
  • их упрощение и вывод на экран,
  • создание смещений, перекрестных ссылок, а также кода и данных, на которые они ссылаются,
  • обработка конструкций switch,
  • обработка манипуляций со стеком и стековыми переменными.

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

Выступление вызвало интерес (видео доклада опубликовано на сайте PHDays), на нем присутствовал даже создатель IDA Pro Ильфак Гильфанов. Опытом создания процессорного модуля я решил поделиться с сообществом на конференции PHDays 8. На тот момент ее не было, но уже после выступления я пообещал сделать соответствующий релиз модуля. Один из его вопросов был — реализована ли поддержка IDA Pro версии 7. Вот тут-то и началось самое интересное.

Я значительно переработал модуль, добавил ряд новых возможностей, в том числе решив те проблемы, которые раньше победить не получалось. Теперь самым свежим стал мануал от Intel, который использовался для сверки и проверки на наличие ошибок. Вот что получилось. Ну и, конечно, добавил поддержку 7-й версии IDA Pro.

Программная модель NIOS II

NIOS II — это программный процессор, разработанный для ПЛИС фирмы Altera (сейчас часть Intel). С точки зрения программ он имеет следующие особенности: порядок байтов little endian, 32-битное адресное пространство, 32-битный набор инструкций, то есть на кодирование каждой команды используется фиксировано по 4 байта, 32 регистра общего и 32 специального назначения.

Дизассемблирование и кодовые ссылки

Итак, мы открыли в IDA Pro новый файл, с прошивкой под процессор NIOS II. После установки модуля мы увидим его в списке процессоров IDA Pro. Выбор процессора представлен на рисунке.

Учитывая, что каждая команда занимает 4 байта, сгруппируем байты по четыре, тогда все будет выглядеть примерно так. Предположим, что в модуле пока не реализован даже базовый разбор команд.

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

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

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

Далее примеры кода будут в таком же виде — с комментариями, чтобы не смотреть в мануал по NIOS II, а сразу понять, что происходит в участке кода, который приведен в качестве примера. Здесь, если вы впервые столкнулись с ассемблерным кодом новой для вас архитектуры, с помощью комментариев можно понять, что происходит.

Псевдоинструкции и упрощение команд

Часть команд NIOS II являются псевдоинструкциями. Для таких команд нет отдельных опкодов, и сами они моделируются как частные случаи других команд. В процессе дизассемблирования выполняется упрощение инструкций — замена определенных сочетаний на псевдоинструкции. Псевдоинструкции в NIOS II можно в целом разделить на четыре вида:

  • когда один из источников регистр zero (r0) и его можно убрать из рассмотрения,
  • когда в команде имеется отрицательное значение и команда заменяется на противоположную,
  • когда условие заменяется на противоположное,
  • когда 32-битное смещение заносится в двух командах по частям (младшая и старшая) и это заменяется на одну команду.

Были реализованы первые два вида, поскольку замена условия особо ничего не дает, а 32-битные смещения имеют больше вариантов исполнения, чем представлено в мануале.

Например, для первого вида рассмотрим код.

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

После реализации обработки псевдоинструкций получаем этот же участок кода, но теперь он выглядит уже более читаемым, и вместо вариаций команд or и add мы получаем вариации команды mov.

Стековые переменные

Архитектура NIOS II поддерживает стек, причем помимо указателя стека sp есть еще указатель на стековый фрейм fp. Рассмотрим пример небольшой процедуры, в которой используется стек.

Можно предположить, что регистр ra сохраняется в стековой переменной, а затем восстанавливается из нее. Очевидно, что в стеке резервируется место под локальные переменные.

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

Функция в примере имеет тип __fastcall и ее аргументы в регистрах r4 и r5 заносятся в стек для вызова подпроцедуры, которая имеет тип _stdcall. Теперь код выглядит немного понятней, и уже можно именовать стековые переменные и разбирать их назначение, переходя по перекрестным ссылкам.

32-битные числа и смещения

Особенность NIOS II в том, что за одну операцию, то есть при выполнении одной команды, можно как максимум занести в регистр непосредственное значение размером в 2 байта (16 бит). С другой стороны, регистры процессора и адресное пространство являются 32-битными, то есть для адресации в регистр нужно занести 4 байта.

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

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

В свойствах смещения для старшей части используется нестандартный тип HIGHA16, иногда используется тип HIGH16, для младшей части — LOW16.

Сложности возникают при формировании операндов как смещений для двух отдельных команд. В самом вычислении 32-битных чисел из двух частей ничего сложного нет. Примеров, как это реализовать (тем более на Python), в IDA SDK нет. Вся эта обработка ложится на процессорный модуль.

Для решения проблемы мы схитрили: 32-битное смещение только с младшей части — по базе. В докладе на PHDays смещения стояли как нерешенная задача. База вычисляется как старшая часть, сдвинутая влево на 16 бит.

При таком подходе перекрестная ссылка образуется только с команды занесения младшей части 32-битного смещения.

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

Сначала в регистр заносится старшая часть смещения командой movhi. В коде под NIOS II встречается следующий механизм занесения 32-битных чисел в регистр. Сделано это может быть тремя способами (командами): сложением addi, вычитанием subi, логическим ИЛИ ori. Затем к ней присоединяется младшая часть.

Например, в следующем участке кода регистры настраиваются на 32-битные числа, которые потом заносятся в регистры — аргументы перед вызовом функции.

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

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

В этом примере определить конечные 32-битные числа (смещения) с ходу уже не получится. Рассмотрим случай, когда при занесении младшей части используется вычитание.

После применения вычисления 32-битных чисел получится следующий вид.

Здесь получили смещение на строку «10/22/08». Здесь мы видим, что теперь, если адрес есть в адресном пространстве, на него формируется смещение, и значение, которое образовалось в результате соединения младшей и старшей частей, рядом уже не выводится. Чтобы остальные смещения указывали на валидные адреса, увеличим немного сегмент.

После увеличения сегмента получаем, что теперь все вычисленные 32-битные числа являются смещениями и указывают на валидные адреса.

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

То, которое вычисляется в регистре r8, потом заносится в стек.

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

Чтение и запись относительно базы

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

При этом также в зависимости от размерности операции выставляется размер самой переменной. После обработки таких ситуаций получаем смещения на переменные из самих команд чтения и записи.

Конструкции switch

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

Далее идут блоки кода, на которые есть ссылки из данных, причем в конце каждого блока происходит прыжок на одну и ту же метку. Поток исполнения останавливается на регистровом переходе jmp r2. Выше также можно увидеть проверку количества случаев и прыжок по умолчанию. Очевидно, что это конструкция switch и эти отдельные блоки обрабатывают конкретные случаи из нее.

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

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

Для экономии места приведены первые пять ее элементов. Сама таблица со смещениями на варианты выглядит следующим образом.

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

Используется обратный проход по пути исполнения с учетом возможных переходов, разрывающих схему, с установкой внутренних переменных, которые сигнализируют различные состояния распознавателя. Процессорный модуль NIOS II распознает switch с такими «посторонними» инструкциями между основными командами, а также с переставленными местами основными командами и с разрывающими схему переходами. В итоге распознается порядка 10 различных вариантов организации switch, встреченных в прошивках.

Инструкция custom

В архитектуре NIOS II есть интересная особенность — инструкция custom. Она дает доступ к 256 задаваемым пользователем инструкциям, которые возможны в архитектуре NIOS II. В своей работе, помимо регистров общего назначения, инструкция custom может обращаться к специальному набору из 32 custom-регистров. После реализации логики разбора команды custom получаем следующий вид.

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

Согласно ему, одним из самых полных и современных вариантов набора инструкций custom является набор инструкций для работы с плавающей точкой — NIOS II Floating Point Hardware 2 Component (FPH2). По инструкции custom существует отдельный мануал. После реализации разбора команд FPH2 пример будет выглядеть так.

По мнемонике двух последних команд убеждаемся, что они действительно выполняют одно и то же действие — команду fadds.

Переходы по значению регистра

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

Рассмотрим участок кода.

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

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

Также формируется перекрестная ссылка между командой и адресом, куда происходит прыжок. Рядом с командой jmp r8 выводится адрес, куда происходит прыжок, если его удалось вычислить. В данном случае ссылку видно в первой строчке, сам прыжок выполняется с последней строчки.

Значение регистра gp (global pointer), сохранение и загрузка

Распространенным является использование глобального указателя, который настраивается на какой-либо адрес, и относительно него происходит адресация переменных. В NIOS II для хранения глобального указателя используется регистр gp (global pointer). В определенный момент, как правило в процедурах инициализации прошивки, в регистр gp заносится значение адреса. Процессорный модуль обрабатывает эту ситуацию; для иллюстрации этого далее приведены примеры кода и окно вывода IDA Pro при включенных отладочных сообщениях в процессорном модуле.

При закрытии базы idb значение gp сохраняется в базе. В данном примере процессорный модуль находит и вычисляет значение регистра gp в новой базе.

При загрузке уже существующей базы idb и если значение gp уже было найдено, оно загружается из базы, что показано в отладочном сообщении в следующем примере.

Чтение и запись относительно gp

Распространенными операциями являются чтение и запись со смещением относительно регистра gp. Например, в следующем примере выполняются три чтения и одна запись такого вида.

Поскольку значение адреса, которое хранится в регистре gp, мы уже получили, то можно адресовать такого рода чтения и записи.

После добавления обработки ситуаций чтения и записи относительно регистра gp получим более удобную картину.

Здесь можно увидеть, к каким переменным идет обращение, отследить их использование и выявить их назначение.

Адресация относительно gp

Встречается другое использование регистра gp для адресации переменных.

Например, здесь мы видим, что регистры настраиваются относительно регистра gp на некоторые переменные или области данных.

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

Здесь уже видно, на какие области относительно gp настраиваются регистры, и становится более понятным, что происходит.

Адресация относительно sp

Аналогичным образом в следующем примере регистры настраиваются на некоторые области памяти, на этот раз относительно регистра sp — указателя стека.

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

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

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

Перекрестные ссылки из кода на поля структур

Задание структур и их использование в IDA Pro может облегчить анализ кода.

В случае если чтение и запись поля разнесены в коде на большом расстоянии, в анализе могут помочь перекрестные ссылки. Глядя на этот участок кода, можно понять, что поле field_8 инкрементируется и, возможно, является счетчиком наступления какого-либо события.

Рассмотрим саму структуру.

Хотя обращения к полям структур есть, как видим, перекрестных ссылок с кода на элементы структур не образовалось.

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

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

Нестыковки между мануалом и реальностью

В мануале при декодировании некоторых команд определенные биты должны принимать строго определенные значения. Например, для команды возврата из исключения eret биты 22–26 должны быть равны 0x1E.

Вот пример этой команды из одной прошивки.

Открывая другую прошивку в месте с похожим контекстом, встречаем иную ситуацию.

Судя по окружению, и даже похожему адресу, это должна быть одна и та же команда. Эти байты не преобразовались автоматически в команду, хотя обработка всех команд есть. Это та же команда eret, за тем исключением, что биты 22–26 не равны 0x1E, а равны нулю. Посмотрим внимательно на байты.

Теперь он не совсем соответствует мануалу, но соответствует действительности. Приходится немного исправить разбор этой команды.

Поддержка IDA 7

Начиная с версии IDA 7.0 достаточно сильно поменялся API, предоставляемый IDA Python для обычных скриптов. Что же касается процессорных модулей — тут изменения колоссальны. Несмотря на это процессорный модуль NIOS II удалось переделать под 7-ю версию, и он в ней успешно заработал.

9. Единственный непонятный момент: при загрузке нового бинарного файла под NIOS II в IDA 7 не происходит начального автоматического анализа, который присутствует в IDA 6.

Заключение

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

Автор: Антон Дорфман, Positive Technologies


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Вышел PostgreSQL 11

Специальный выпуск POSTGRESSO, посвященный выходу официального релиза версии 11. На улице PostgreSQL праздник. После четырех beta вышла PostgreSQL 11 General Availability, то есть официальная версия. В анонсе есть даже приветственное слово Брюса Момджана: «готовя этот релиз, сообщество особенно заботилось о ...

Вольтметр для батареек: карманный гаджет для смартфона с «крокодильчиками»

Компания FTlab во многом известна как производитель мобильных полупроводниковых датчиков с разъемом Jack, каждый из которых определяет либо уровень ультрафиолета, либо температуру и влажность, либо электромагнитное излучение и даже уровень радиации. Устройства имеют довольно ограниченную применимость из-за характеристик, и скорее ...