Я начал заниматься инфобезопасностью в тот же год, когда NSA осчастливило нас Ghidra. С тех пор она уверенно стала самым востребованным инструментом в моём арсенале для реверс-инжиниринга и исследования уязвимостей. Она бесплатная, расширяемая и поддерживает некоторые из более странных архитектур, с которыми нам приходится сталкиваться.
Но порог входа у неё высокий.
Эта статья — итог того, чему я научился за, возможно, слишком много часов перед слепящим и устаревшим интерфейсом Ghidra. Основной фокус здесь — анализ прошивок, так что исследователи вредоносного ПО и охотники за багами в ОС могут немного разочароваться. Но полезные приёмы должны найтись для всех. Все примеры взяты из реальных исследовательских проектов, которыми мы занимались.
Стоит сразу оговориться: реверс-инжиниринг — процесс небыстрый. Чтобы полностью понять, как работает система, могут уйти недели. Улитка всё-таки взбирается на Фудзи… рано или поздно.
Содержание
Определяем адрес загрузки
Ищем таблицу векторов прерываний
Задаём карту памяти
Аннотируем как можно больше
Ищем функции по строкам
Не пропускаем строки на стеке
Охотимся за магическими значениями
Планирование в RTOS
BSim для поиска библиотечных функций
Импорт исходного кода
Закладки
Погружаемся в ассемблерный код
Выжимаем максимум из скриптов и плагинов
Большие языковые модели
Определяем адрес загрузки
Прошивка редко загружается в память по нулевому адресу. Во многих архитектурах первые адреса зарезервированы под начальный указатель стека и вектор сброса. CPU переходит по вектору сброса, чтобы продолжить выполнение. Благодаря этому можно загружать разные прошивки: например, основной прикладной код или загрузчик восстановления.
Если не задать адрес загрузки, дизассемблер создаст некорректные ссылки на память — на функции, строки и другие объекты — и заведёт вас не туда.
Базовый адрес можно определить несколькими способами:
По даташиту микросхемы. Например, хорошо задокументировано, что STM32 загружает прошивку по адресу 0x0800 0000.
По IVT, см. следующий раздел. Если обработчики прерываний находятся по адресам 0x20061ae2, 0x2004200c и т.д., базовый адрес, скорее всего, равен 0x20000000.
По ссылкам на строки. Возьмите смещение строк и поищите в коде потенциальные указатели на них. Для строки по смещению 0x5eea4 ищите ссылки на 0x5eea4 с учётом порядка байтов. Повторите то же для другой строки, потом ещё для одной. Если у всех строк находятся потенциальные ссылки с одинаковыми старшими байтами, например 0x1005eea4, 0x10014a2 и так далее, вы определили базовый адрес. При ручной проверке будут ложные срабатывания, но можно использовать инструменты, которые сразу берут выборку из сотен строк и проверяют, при каком базовом адресе получается больше всего валидных ссылок.
По другим известным структурам прошивки. binbloom находит в автомобильных прошивках базы UDS, которые содержат указатели на функции-обработчики. Как и метод с IVT, он определяет базовый адрес по старшему байту каждого указателя на функцию. В вашей прошивке также могут быть отладочные таблицы, сопоставляющие имена функций с адресами.


Ищем таблицу векторов прерываний
IVT часто начинается с нулевого смещения и представляет собой таблицу адресов обработчиков прерываний.
Задайте для каждой записи тип данных «указатель» — горячая клавиша p — и сможете переходить к коду обработчика двойным щелчком. Запустите дизассемблирование горячей клавишей d.
Первая запись таблицы обычно указывает на обработчик сброса. Либо вторая, если первая запись — это начальный указатель стека. Это точка входа прошивки. Дизассемблировать обработчик сброса полезно перед запуском Auto Analysis: так анализ пройдёт по большинству функций в корректном порядке выполнения.
Назначение и расположение каждой записи IVT документируются в даташите микросхемы.


Задаём карту памяти
Флеш-память уже будет здесь с тем базовым адресом, который вы задали ранее. Но Ghidra не помечает её как область только для чтения, хотя обычно предполагается, что прошивка не пишет или не может писать в загруженный образ флеш-памяти.
У строк и других структур данных только для чтения из-за этого будут странные ссылки:

Пометьте область как доступную только для чтения, и они появятся как ожидается:

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


Последняя задача — добавить расположение регистров, чтобы понимать, с какой периферией взаимодействует подпрограмма. Иначе вы будете видеть только ссылки на неизвестный адрес.
Файлы CMSIS System View Description (SVD) перечисляют эти расположения для ARM-микроконтроллеров. Их можно загрузить в Ghidra с помощью скрипта SVD-Loader (https://github.com/leveldown-security/SVD-Loader-Ghidra). Скрипт также задаёт для регистров периферии типы данных в виде хорошо описанных структур:

У процессоров M68000 расположение периферии зависит от регистров базового адреса, настраиваемых во время выполнения: VBR, RAMBAR и MBAR. Посмотрите в последовательности инициализации прошивки, как они задаются:

Аннотируем как можно больше
Даже если функция или адрес данных кажутся неважными, но у вас есть хотя бы догадка об их назначении, присвойте им метку горячей клавишей l. Это поможет позже, когда вы увидите ссылки на них в других местах. Для бинарников с символами добавляйте к своим меткам префикс, чтобы отличать их от автоматически сгенерированных.
Добавляйте комментарии — горячая клавиша ;.
Имена переменных и исправление их типов данных (Ctrl-l) помогают разобраться в декомпиляции, особенно когда возвращаешься к проекту после длинных выходных. В какой-то момент вы дойдёте до того, что будете восстанавливать подпрограммы в компилируемые фрагменты C.
Ищем функции по строкам
Это очевидный приём. Поскольку из прошивок часто удалены символы, строки остаются единственной формой документации, которую поставщик вам всё-таки оставил.
Отладочные логи, проверки assert и сообщения об исключениях часто дают полный прототип функции: по нему можно переименовать функцию и задать корректные типы параметров.

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

Не пропускаем строки на стеке
Строки могут собираться на стеке, а не храниться во время компиляции в статических секциях данных. Зачем так делают? Ради оптимизации — например, четырёхбайтовые строки можно обрабатывать в 32-битных регистрах, — обфускации или осознанных проектных решений, связанных с выделением памяти, временем жизни объектов и ограничениями реального времени.


Охотимся за магическими значениями
Это менее очевидный способ идентификации функций. Используйте поиск по памяти и поиск скалярных значений для известных констант: например, magic cookie DHCP (0x63825363), ARP EtherType (0x0806) или AES S-Box.


Планирование в RTOS
Операционные системы реального времени усложняют реверс-инжиниринг ещё на один уровень. Глобальные ресурсы делятся между подпрограммами, чаще встречаются выделение памяти в куче и объекты, переданные по ссылке, а отделить код ядра от прикладного кода бывает непросто.
Полезно понять, как запускаются задачи. Функция создания задачи принимает указатель на функцию, которая реализует эту задачу. Если поискать вызовы этой функции, вы получите список всех указателей на функции прикладного кода.
При создании задач имена задач также часто передаются аргументами для отладки. Поэтому вместе с полезными указателями на функции вы получаете возможность присвоить им осмысленные имена. FreeRTOS определяет xTaskCreate так:
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode, // Указатель на входную функцию задачи const char * const pcName, // Имя задачи const configSTACK_DEPTH_TYPE uxStackDepth, // Количество слов для стека void *pvParameters, // Аргументы задачи UBaseType_t uxPriority, // Приоритет планирования TaskHandle_t *pxCreatedTask // Дескриптор созданной задачи );
Часто под каждый сетевой сервис выделена отдельная задача, поэтому такое понимание критически важно, если вы, например, хотите сосредоточиться на реверс-инжиниринге собственного протокола.

BSim для поиска библиотечных функций
Ghidra BSim — это плагин для нечёткого сопоставления кода с базой известных функций.
Если вы знаете, какие библиотеки использует прошивка, например RTOS или стек TCP/IP, можно самостоятельно скомпилировать эти библиотеки с отладочными строками или взять публичные предкомпилированные релизы. Затем вы загружаете их в Ghidra, и она создаёт базу с сигнатурами всех библиотечных функций.
После этого вы сравниваете функции из своей прошивки с базой BSim. В отличие от Function ID, BSim выполняет нечёткое сопоставление. Ему удаётся сопоставить значительную часть функций даже тогда, когда библиотека другой версии или собрана другим компилятором под другую архитектуру. За два клика можно скопировать имя функции и любые пользовательские типы данных.

Импорт исходного кода
Попробуйте импортировать заголовочные файлы библиотек, которые, как вы знаете, использует прошивка. Ghidra заполнит Data Type Manager всеми typedef, структурами и прототипами функций. Константы препроцессора можно использовать для аннотирования декомпиляции через Set Equate… — горячая клавиша e.
Заставить это работать бывает непросто. Парсер исходного кода в Ghidra не слишком умён, и порядок импортов имеет значение. Также нужно задать правильные константы препроцессора, чтобы пройти условные блоки #ifdef, и добавить заголовочные файлы стандартной библиотеки C в путь поиска include-файлов, если библиотека на них опирается. Полноценное окружение C не требуется: существуют минимальные наборы стандартных заголовочных файлов.

Закладки
Ghidra заполняет таблицу закладок во время автоанализа.
Здесь можно найти таблицы адресов, полезные для идентификации функций, «неопределённые» функции — найденные по известным последовательностям операндов, а не по ссылкам, — встроенные медиафайлы и места, где дизассемблирование не удалось.
Закладки также можно ставить вручную или через плагины.

Погружаемся в ассемблерный код
Если только вы не подбираетесь к конкретной уязвимости или не исследуете архаичную систему с вручную написанным ассемблерным кодом либо нестандартными приёмами компиляции, основная часть реверс-инжиниринга будет проходить в окне декомпиляции.
Так проще отслеживать ход логики и аннотировать код типами данных и осмысленными именами.
Но перевод в псевдо-C не всегда получается идеальным. Ghidra часто неверно определяет количество параметров функции или размер переменных. Иногда она вообще не может декомпилировать код.
Тревожными признаками могут быть переменные с именами in_, in_stack_, unaff_ и extraout_ в окне декомпилятора.
В таких случаях критически важно читать ассемблерный код. Разберитесь, какое соглашение о вызовах используется в вашей архитектуре: передаются ли параметры через стек или через регистры, в каком порядке они читаются и какие регистры функция может перезаписать.
Без отладочных символов Ghidra не умеет надёжно определять вариадические функции и функции с соглашением thiscall, например printf(char *fmt, …) и object::method(). Задавайте их вручную в окне Edit Function Signature.


Выжимаем максимум из скриптов и плагинов
Я уже упоминал про SVD-Loader, но есть много других скриптов, которые автоматизируют процессы или аннотируют интересную функциональность.
Например, я успешно использовал VxWorks Symbol Table Finder, Leaf Blower для поиска форматных строк и функций из string.h, FindCrypt и Stack String Explorer.
Большие языковые модели
Я часто загружаю дизассемблированный код и результаты декомпиляции из Ghidra в LLM. Это хорошо помогает быстро распознавать распространённые алгоритмы: выделение памяти, контрольные суммы и криптографические процедуры. В самом простом варианте достаточно без затей скопировать вывод Ghidra в чат-бот вроде ChatGPT и дать простой промпт — уже можно получить полезные наблюдения.
Мы также успешно использовали специализированные ИИ-расширения.
GhidrAssist (https://github.com/jtang613/GhidrAssist/) поддерживает разные API LLM и предоставляет окно чата с неплохим шаблоном промпта по умолчанию. Дополнительный контекст можно давать через текстовые документы, написанные вручную, или справочные руководства. Кроме того, GhidrAssist можно поручить автономно выполнять циклы исследования для анализа более сложной логики с саморефлексией.
Он также поддерживает MCP-серверы Model Context Protocol, чтобы улучшать результат за счёт автоматической декомпиляции и аннотирования символов. Тот же автор сделал для этой цели GhidrAssistMCP, но мы также успешно использовали GhidraMCP от LaurieWired (https://github.com/LaurieWired/GhidraMCP).
Как обычно советуют при работе с ИИ: он галлюцинирует, поэтому проверяйте результат. А для кода с коммерчески чувствительной информацией используйте локальный экземпляр или сервис с корпоративной защитой данных.



Реверс-инжиниринг редко сводится к одному инструменту: приходится разбираться в адресах загрузки, таблицах прерываний, соглашениях о вызовах, поведении рантайма и странностях конкретной системы. Часть этих тем можно отдельно разобрать на бесплатных уроках OTUS — ни них можно задать вопросы, проверить формат обучения и закрыть пробелы в понимании низкоуровневого анализа.
1 июля в 20:00. «Классические методы перехвата управления в Linux». Записаться
23 июля в 20:00. «Фаззинг и реверс: как понять, что делает программа, найти в ней ошибки». Записаться
Полный список бесплатных уроков июня смотрите в дайджесте.
