Скриншот интерфейса дизассемблера IDA Pro
IDA Pro — знаменитый дизассемблер, который уже много лет используют исследователи информационной безопасности во всем мире. Мы в Positive Technologies также применяем этот инструмент. Более того, нам удалось разработать собственный процессорный модуль дизассемблера для микропроцессорной архитектуры NIOS II, который повышает скорость и удобство анализа кода.
Сегодня я расскажу об истории этого проекта и покажу, что получилось в итоге.
Предыстория
Все началось в 2016 году, когда для анализа прошивки в одной задаче нам пришлось разработать собственный процессорный модуль. Разработка велась с нуля по мануалу Nios II Classic Processor Reference Guide, который тогда был наиболее актуальным. Всего на эту работу ушло около двух недель.
Процессорный модуль разрабатывался для версии IDA 6.9. Для скорости был выбран IDA Python. В месте, где обитают процессорные модули, — подкаталоге procs внутри установочного каталога IDA Pro — есть три модуля на Python: msp430, ebc, spu. В них можно подсмотреть, как устроен модуль и как может быть реализована базовая функциональность дизассемблирования:
- разбор инструкций и операндов,
- их упрощение и вывод на экран,
- создание смещений, перекрестных ссылок, а также кода и данных, на которые они ссылаются,
- обработка конструкций switch,
- обработка манипуляций со стеком и стековыми переменными.
Примерно такая функциональность и была реализована на тот момент. К счастью, инструмент пригодился и в процессе работы над еще одной задачей, в ходе которой, уже год спустя, активно использовался и дорабатывался.
Опытом создания процессорного модуля я решил поделиться с сообществом на конференции PHDays 8. Выступление вызвало интерес (видео доклада опубликовано на сайте PHDays), на нем присутствовал даже создатель IDA Pro Ильфак Гильфанов. Один из его вопросов был — реализована ли поддержка 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 нет.
В докладе на PHDays смещения стояли как нерешенная задача. Для решения проблемы мы схитрили: 32-битное смещение только с младшей части — по базе. База вычисляется как старшая часть, сдвинутая влево на 16 бит.
При таком подходе перекрестная ссылка образуется только с команды занесения младшей части 32-битного смещения.
В свойствах смещения видна база и отмечено свойство, чтобы рассматривать ее как число, чтобы не формировалось большое количество перекрестных ссылок на сам адрес, который принимаем как базу.
В коде под NIOS II встречается следующий механизм занесения 32-битных чисел в регистр. Сначала в регистр заносится старшая часть смещения командой movhi. Затем к ней присоединяется младшая часть. Сделано это может быть тремя способами (командами): сложением 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 существует отдельный мануал. Согласно ему, одним из самых полных и современных вариантов набора инструкций custom является набор инструкций для работы с плавающей точкой — NIOS II Floating Point Hardware 2 Component (FPH2). После реализации разбора команд FPH2 пример будет выглядеть так.
По мнемонике двух последних команд убеждаемся, что они действительно выполняют одно и то же действие — команду fadds.
Переходы по значению регистра
В исследуемых прошивках часто встречается ситуация, когда выполняется прыжок по значению регистра, в который перед этим заносится 32-битное смещение, определяющее место прыжка.
Рассмотрим участок кода.
В последней строчке происходит прыжок по значению регистра, при этом видно, что прежде в регистр заносится адрес процедуры, которая начинается в первой строчке примера. В данном случае очевидно, что прыжок совершается в ее начало.
После добавления функционала распознавания прыжков получается следующий вид.
Рядом с командой jmp r8 выводится адрес, куда происходит прыжок, если его удалось вычислить. Также формируется перекрестная ссылка между командой и адресом, куда происходит прыжок. В данном случае ссылку видно в первой строчке, сам прыжок выполняется с последней строчки.
Значение регистра gp (global pointer), сохранение и загрузка
Распространенным является использование глобального указателя, который настраивается на какой-либо адрес, и относительно него происходит адресация переменных. В NIOS II для хранения глобального указателя используется регистр gp (global pointer). В определенный момент, как правило в процедурах инициализации прошивки, в регистр gp заносится значение адреса. Процессорный модуль обрабатывает эту ситуацию; для иллюстрации этого далее приведены примеры кода и окно вывода IDA Pro при включенных отладочных сообщениях в процессорном модуле.
В данном примере процессорный модуль находит и вычисляет значение регистра gp в новой базе. При закрытии базы idb значение 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-ю версию, и он в ней успешно заработал.
Единственный непонятный момент: при загрузке нового бинарного файла под NIOS II в IDA 7 не происходит начального автоматического анализа, который присутствует в IDA 6.9.
Заключение
Помимо базовой функциональности дизассемблирования, примеры которой есть в SDK, в процессорном модуле реализовано много различных возможностей, облегчающих труд исследователя кода. Понятно, что все это можно сделать и вручную, но, к примеру, когда на бинарный файл с прошивкой размером в пару мегабайтов встречаются тысячи и десятки тысяч смещений разных видов, — зачем тратить на это время? Пусть это сделает для нас процессорный модуль. Ведь как помогают приятные возможности быстрой навигации по исследуемому коду с помощью перекрестных ссылок! Это делает IDA таким удобным и приятным инструментом, каким мы его знаем.
Автор: Антон Дорфман, Positive Technologies
Комментариев нет:
Отправить комментарий