КулЛиб - Классная библиотека! Скачать книги бесплатно 

GHIDRA. Полное руководство [Крис Игл] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]
Крис Игл, Кара Нэнс

GHIDRA
Полное руководство

THE GHIDRA
BООK
The Definitive Cuide

by Chris Eagle and Kara Nance

San Francisco

GHIDRA
Полное руководство

Крис Игл, Кара Нэнс

Москва, 2022

УДК 004.4
ББК 32.97
И26
Игл К., Нэнс К.
И26 GHIDRA. Полное руководство / пер. с анг. А. А. Слинкина. – М.: ДМК Пресс,
2022. – 750 с.: ил.
ISBN 978-5-97060-942-2
Платформа Ghidra, ставшая итогом более десяти лет работы в Агентстве
национальной безопасности, была разработана для решения наиболее
трудных задач обратной разработки (Reverse Engeneering – RE). После
раскрытия исходного кода этого инструмента, ранее предназначавшегося
только для служебного пользования, один из лучших в мире дизассемблеров и интуитивно понятных декомпиляторов оказался в руках всех специа­
листов, стоящих на страже кибербезопасности.
Эта книга, рассчитанная равно на начинающих и опытных пользователей,
поможет вам во всеоружии встретить задачу RE и анализировать файлы,
как это делают профессионалы.

УДК 004.4
ББК 32.97

Title of English-language original: The Ghidra Book: The Definitive Guide,
ISBN 9781718501027, published by No Starch Press Inc. 245 8th Street, San
Francisco, California United States 94103. The Russian-Language 1st edition
Copyright © 2020 by DMK Press Publishing under license by No Starch Press
Inc. All rights reserved.”
Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.

ISBN 978-1-71850-102-7 (англ.) © 2020 Chris Eagle and Kara Nance
ISBN 978-5-97060-942-2 (рус.) © Оформление, издание, перевод,
ДМК Пресс, 2022

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

ОБ АВТОРАХ
Крис Игл занимается обратной разработкой уже 40 лет.
Он автор книги «The IDA Pro Book», вышедшей в издательстве
No Starch Press, и пользуется большим авторитетом как преподаватель обратной разработки. Его перу принадлежат многочисленные статьи по инструментам обратной разработки, он
часто выступает на таких мероприятиях, как Blackhat, Defcon
и Shmoocon.
Кара Нэнс – частный консультант по безопасности. В течение
многих лет работала профессором информатики. Была членом
совета директоров проекта Honeynet и много раз выступала
с докладами на различных конференциях по всему миру. Обожает разрабатывать расширения Ghidra и регулярно читает
курсы по Ghidra.

О ТЕХНИЧЕСКОМ РЕЦЕНЗЕНТЕ
Брайан Хэй много лет занимался обратной разработкой, был
профессором и разработчиком программного обеспечения. Выступал на многих конференциях, читал курсы, а в настоящее
время работает старшим научным сотрудником в компании,
занимающейся исследованиями в области безопасности. Специализируется на проектировании и разработке виртуализированных сред для обучения и тестирования новых впечатляющих инструментов, таких как Ghidra.

КРАТКОЕ СОДЕРЖАНИЕ

https://t.me/it_boooks

Об авторах....................................................................... 6
О техническом рецензенте........................................... 6
Оглавление..................................................................... 8
Благодарности.............................................................. 17
Введение....................................................................... 18
Часть I. Введение......................................................... 25

Глава 1. Введение в дизассемблирование........................................................ 27
Глава 2. Обратная разработка и инструменты дизассемблирования..................43
Глава 3. Первое знакомство с Ghidra................................................................ 65

Часть II. Основы использования Ghidra.................... 73

Глава 4. Начало работы с Ghidra...................................................................... 75
Глава 5. Отображение данных в Ghidra.......................................................... 93
Глава 6. Дизассемблирование в Ghidra.......................................................... 135
Глава 7. Управление дизассемблированием................................................. 175
Глава 8. Типы данных и структуры данных.................................................. 211
Глава 9. Перекрестные ссылки........................................................................ 259
Глава 10. Графы................................................................................................ 277

Часть III. Поставить Ghidra себе на службу............ 299

Глава 11. Коллективная обратная разработка программ............................ 301
Глава 12. Настройка Ghidra............................................................................. 331
Глава 13. Расширение взгляда на мир Ghidra.............................................. 355
Глава 14. Основы написания скриптов для Ghidra...................................... 387
Глава 15. Eclipse и GhidraDev.......................................................................... 423
Глава 16. Необслуживаемый режим Ghidra.................................................. 457

Часть IV. Дополнительные темы.............................. 483

Глава 17. Загрузчики Ghidra........................................................................... 485
Глава 18. Процессорные модули в Ghidra...................................................... 537
Глава 19. Декомпилятор Ghidra...................................................................... 571
Глава 20. Зависимость от компилятора.......................................................... 591

Часть V. Реальные приложения............................... 623

Глава 21. Анализ обфусцированного кода..................................................... 625
Глава 22. Изменение двоичного кода............................................................. 673
Глава 23. Определение разности двоичных файлов
и отслеживание версий.................................................................... 705
Приложение. Ghidra для пользователей IDA................................................ 731

ОГЛАВЛЕНИЕ
ЧАСТЬ I. ВВЕДЕНИЕ.................................................... 25
Глава 1. Введение в дизассемблирование................................................ 27
Теория дизассемблирования.......................................................................... 28
Что делает дизассемблер................................................................................ 29
Зачем нужен дизассемблер............................................................................ 30
Анализ вредоносного ПО............................................................................ 31
Анализ на уязвимость................................................................................. 31
Анализ интероперабельности.................................................................... 32
Проверка компилятора............................................................................... 32
Отображение команд в процессе отладки................................................ 33
Как работает дизассемблер............................................................................ 33
Базовый алгоритм дизассемблирования.................................................. 33
Алгоритм линейной развертки.................................................................. 35
Алгоритм рекурсивного спуска.................................................................. 37
Резюме.............................................................................................................. 42
Глава 2. Обратная разработка и инструменты
дизассемблирования....................................................................................... 43
Средства классификации............................................................................... 44
file.................................................................................................................. 44
PE Tools......................................................................................................... 47
PEiD.............................................................................................................. 48
Обзорные инструменты................................................................................... 49
nm.................................................................................................................. 49
ldd.................................................................................................................. 52
objdump......................................................................................................... 55
otool............................................................................................................... 56
dumpbin........................................................................................................ 56
c++filt............................................................................................................. 57
Инструменты глубокой
инспекции......................................................................................................... 59
strings............................................................................................................ 59
Дизассемблеры............................................................................................ 61
Резюме.............................................................................................................. 63
Глава 3. Первое знакомство с Ghidra......................................................... 65
Лицензионная политика Ghidra................................................................... 66
Версии Ghidra.................................................................................................. 66
Ресурсы поддержки Ghidra............................................................................ 66
Скачивание Ghidra......................................................................................... 68
Установка Ghidra............................................................................................. 68
Запуск Ghidra.............................................................................................. 70
Резюме.............................................................................................................. 71

8

Оглавление

ЧАСТЬ II. ОСНОВЫ ИСПОЛЬЗОВАНИЯ GHIDRA...... 73
Глава 4. Начало работы с Ghidra................................................................ 75
Запуск Ghidra ................................................................................................. 75
Создание нового проекта................................................................................ 77
Загрузка файла в Ghidra............................................................................ 78
Использование простого двоичного загрузчика...................................... 82
Анализ файлов в Ghidra................................................................................. 84
Результаты автоматического анализа...................................................... 88
Поведение рабочего стола во время начального анализа.......................... 89
Сохранение работы и выход....................................................................... 90
Советы по организации рабочего стола Ghidra........................................... 91
Резюме.............................................................................................................. 92
Глава 5. Отображение данных в Ghidra.................................................... 93
Браузер кода.................................................................................................... 94
Окна браузера кода......................................................................................... 97
Окно листинга........................................................................................... 100
Создание дополнительных окон дизассемблера....................................... 105
Представление графа функции в Ghidra................................................... 106
Окно деревьев программы............................................................................ 112
Окно дерева символов................................................................................... 113
Импортируемые объекты.......................................................................... 114
Экспортируемые объекты......................................................................... 115
Функции..................................................................................................... 115
Метки.......................................................................................................... 116
Классы........................................................................................................ 116
Пространства имен.................................................................................... 117
Окно диспетчера типов данных................................................................... 117
Окно консоли.................................................................................................. 118
Окно декомпилятора..................................................................................... 118
Другие окна Ghidra....................................................................................... 121
Окно байтов................................................................................................ 121
Окно определенных данных........................................................................ 123
Окно определенных строк............................................................................ 125
Окна таблицы символов и ссылок на символы.......................................... 126
Окно карты памяти....................................................................................... 130
Окно графа вызовов функции...................................................................... 131
Резюме............................................................................................................ 132
Глава 6. Дизассемблирование в Ghidra.................................................. 135
Навигация по листингу дизассемблера...................................................... 136
Имена и метки........................................................................................... 136
Навигация в Ghidra...................................................................................... 137
Перейти к................................................................................................... 139
История навигации................................................................................... 139
Кадры стека.................................................................................................... 141
Механизмы вызова функций................................................................... 141
Оглавление

9

Соглашения о вызове................................................................................ 144
Дополнительные сведения о кадре стека............................................... 150
Размещение локальных переменных..................................................... 151
Примеры кадров стека.............................................................................. 152
Представления стека в Ghidra..................................................................... 157
Анализ кадров стека в Ghidra................................................................. 158
Кадры стека в листинге дизассемблера................................................. 159
Анализ кадра стека с помощью декомпилятора.................................... 162
Локальные переменные как операнды.................................................. 164
Редактор кадра стека в Ghidra................................................................ 165
Поиск............................................................................................................... 168
Поиск по тексту программы..................................................................... 169
Поиск в памяти.......................................................................................... 171
Резюме............................................................................................................ 173
Глава 7. Управление дизассемблированием......................................... 175
Манипулирование именами и метками..................................................... 176
Переименование параметров и локальных переменных..................... 177
Переименование меток............................................................................. 182
Добавление новой метки.......................................................................... 183
Редактирование меток.............................................................................. 185
Удаление метки......................................................................................... 187
Навигация по меткам............................................................................... 187
Комментарии................................................................................................. 187
Концевые комментарии............................................................................ 189
Предварительные и заключительные комментарии............................ 190
Вводные комментарии.............................................................................. 190
Повторяемые комментарии...................................................................... 192
Комментарии для параметров и локальных переменных................... 192
Аннотации.................................................................................................. 193
Базовые преобразования кода..................................................................... 194
Изменение параметров отображения кода............................................ 194
Форматирование операндов команд....................................................... 196
Манипулирование функциями............................................................... 198
Преобразование данных в код (и наоборот)........................................... 202
Основы преобразования данных................................................................. 203
Задание типов данных............................................................................. 204
Работа со строками.................................................................................... 206
Определение массивов.............................................................................. 208
Резюме............................................................................................................ 209
Глава 8. Типы данных и структуры данных.......................................... 211
В чем смысл этих данных?........................................................................... 212
Распознавание структур данных в коде..................................................... 215
Доступ к элементам массива.................................................................... 215
Доступ к полям структуры....................................................................... 228
Массивы структур...................................................................................... 234
Создание структур в Ghidra......................................................................... 236

10

Оглавление

Создание новой структуры....................................................................... 237
Редактирование полей структуры........................................................... 240
Наложение структур................................................................................. 242
Введение в обратную разработку кода на C++.......................................... 244
Указатель this............................................................................................ 245
Виртуальные функции и vf-таблицы...................................................... 246
Жизненный цикл объекта........................................................................ 251
Декорирование имен................................................................................. 253
Идентификация типа во время выполнения......................................... 254
Отношения наследования........................................................................ 256
Справочные материалы по обратной разработке кода на C++............ 257
Резюме............................................................................................................ 258
Глава 9. Перекрестные ссылки.................................................................. 259
Базовые сведения о ссылках........................................................................ 260
Перекрестные (обратные) ссылки........................................................... 261
Пример анализа ссылок........................................................................... 265
Окна управления ссылками......................................................................... 271
Окно перекрестных ссылок...................................................................... 272
Ссылки на................................................................................................... 273
Ссылки на символы................................................................................... 273
Дополнительные способы работы со ссылками..................................... 274
Резюме............................................................................................................ 276
Глава 10. Графы.............................................................................................. 277
Простые блоки................................................................................................ 278
Графы функций............................................................................................. 279
Графы вызовов функций.............................................................................. 290
Деревья........................................................................................................... 297
Резюме............................................................................................................ 297

ЧАСТЬ III. ПОСТАВИТЬ GHIDRA
СЕБЕ НА СЛУЖБУ...................................................... 299
Глава 11. Коллективная обратная разработка программ................. 301
Коллективная работа.................................................................................... 302
Подготовка сервера Ghidra.......................................................................... 303
Разделяемые проекты................................................................................... 307
Создание разделяемого проекта.............................................................. 307
Управление проектом............................................................................... 310
Меню окна проекта....................................................................................... 311
Меню File.................................................................................................... 311
Меню Edit .................................................................................................. 314
Меню Project............................................................................................... 316
Репозиторий проекта.................................................................................... 319
Управление версиями............................................................................... 321
Пример........................................................................................................ 324
Резюме............................................................................................................ 330
Оглавление

11

Глава 12. Настройка Ghidra........................................................................ 331
Браузер кода.................................................................................................. 332
Реорганизация окон.................................................................................. 332
Редактирование параметров инструментов........................................... 334
Редактирование параметров инструмента............................................. 337
Специальные средства редактирования
для некоторых инструментов................................................................... 338
Сохранение конфигурации браузера кода............................................. 340
Окно проекта в Ghidra.................................................................................. 340
Меню Tools..................................................................................................... 346
Рабочие пространства................................................................................... 352
Резюме............................................................................................................ 353
Глава 13. Расширение взгляда на мир Ghidra...................................... 355
Импорт файлов.............................................................................................. 356
Анализаторы.................................................................................................. 359
Модели слов................................................................................................... 360
Типы данных.................................................................................................. 362
Создание новых архивов типов данных................................................. 365
Идентификаторы функций.......................................................................... 369
Плагин Function ID....................................................................................... 371
Пример применения плагина Function ID: UPX................................... 374
Пример применения плагина Function ID:
профилирование статической библиотеки............................................. 379
Резюме............................................................................................................ 385
Глава 14. Основы написания скриптов для Ghidra............................. 387
Диспетчер скриптов...................................................................................... 388
Окно диспетчера скриптов....................................................................... 388
Панель инструментов диспетчера скриптов.......................................... 390
Разработка скриптов..................................................................................... 391
Написание скриптов на Java (не JavaScript!)........................................ 391
Пример редактирования скрипта: поиск
по регулярному выражению.................................................................... 393
Скрипты на Python................................................................................... 399
Поддержка других языков....................................................................... 401
Введение в Ghidra API.................................................................................. 402
Интерфейс Address.................................................................................... 403
Интерфейс Symbol..................................................................................... 403
Интерфейс Reference................................................................................. 403
Класс GhidraScript.................................................................................... 404
Функции манипулирования программой.............................................. 410
Класс Program............................................................................................ 411
Интерфейс Function.................................................................................. 413
Интерфейс Instruction.............................................................................. 413
Примеры скриптов Ghidra............................................................................ 414
Пример 1: перечисление функций.......................................................... 414
Пример 2: перечисление команд............................................................. 415

12

Оглавление

Пример 3: перечисление перекрестных ссылок.................................... 416
Пример 4: нахождение вызовов функции.............................................. 417
Пример 5: эмуляция поведения языка ассемблера.............................. 419
Резюме............................................................................................................ 422
Глава 15. Eclipse и GhidraDev.................................................................... 423
Eclipse............................................................................................................. 423
Интеграция с Eclipse................................................................................. 424
Запуск Eclipse............................................................................................ 425
Редактирование скриптов в Eclipse........................................................ 426
Меню GhidraDev............................................................................................ 427
GhidraDevNew........................................................................................ 428
Навигация в обозревателе пакетов......................................................... 434
Пример: проект модуля анализатора......................................................... 441
Шаг 1: постановка задачи........................................................................ 443
Шаг 2: создать модуль в Eclipse............................................................... 443
Шаг 3: написать анализатор.................................................................... 443
Шаг 4: протестировать анализатор в Eclipse......................................... 451
Шаг 5: добавить анализатор в Ghidra.................................................... 451
Шаг 6: тестирование анализатора в Ghidra........................................... 452
Резюме............................................................................................................ 455
Глава 16. Необслуживаемый режим Ghidra.......................................... 457
Приступая к работе....................................................................................... 458
Шаг 1: запуск Ghidra................................................................................ 459
Шаги 2 и 3: создать новый проект Ghidra в указанном месте............. 459
Шаг 4: импортировать файл в проект..................................................... 460
Шаги 5 и 6: автоматический анализ файла, сохранение и выход...... 460
Флаги и параметры................................................................................... 465
Написание скриптов..................................................................................... 475
HeadlessSimpleROP................................................................................... 475
Автоматизированное создание базы данных FidDb.............................. 480
Резюме............................................................................................................ 482

ЧАСТЬ IV. ДОПОЛНИТЕЛЬНЫЕ ТЕМЫ................... 483
Глава 17. Загрузчики Ghidra...................................................................... 485
Анализ неизвестного файла......................................................................... 487
Загрузка PE-файла Windows вручную....................................................... 488
Пример 1: модуль загрузчика SimpleShellcode......................................... 502
Шаг 0: шаг назад....................................................................................... 503
Шаг 1: поставить задачу........................................................................... 506
Шаг 2: создать модуль в Eclipse............................................................... 507
Шаг 3: разработать загрузчик.................................................................. 507
Шаг 4: добавить загрузчик в Ghidra....................................................... 514
Шаг 5: протестировать загрузчик в Ghidra............................................ 515
Пример 2: простой загрузчик
шелл-кода из исходных файлов................................................................... 517
Оглавление

13

Обновление 1: изменить ответ на опрос импортера.............................. 518
Обновление 2: найти шелл-код в исходном коде................................... 518
Обновление 3: преобразовать шелл-код в байтовые значения............ 519
Обновление 4: загрузить преобразованный байтовый массив............ 520
Результаты................................................................................................. 520
Пример 3: простой загрузчик шелл-кода в формате ELF.................... 522
Организационные мероприятия............................................................. 523
Формат заголовков ELF............................................................................ 524
Определение поддерживаемых спецификаций загрузки.................... 525
Загрузить содержимое файла в Ghidra.................................................. 527
Отформатировать байты данных и добавить точку входа................... 528
Файлы определений языков.................................................................... 529
Opinion-файлы........................................................................................... 530
Результаты................................................................................................. 532
Резюме............................................................................................................ 535
Глава 18. Процессорные модули в Ghidra.............................................. 537
Знакомство с процессорным модулем Ghidra............................................ 539
Процессорные модули в Eclipse............................................................... 539
SLEIGH....................................................................................................... 541
Руководства по процессорам.................................................................... 543
Модификация процессорного модуля Ghidra............................................ 545
Постановка задачи.................................................................................... 547
Пример 1: добавление команды в процессорный модуль.................... 547
Пример 2: модификация команды в процессорном модуле................. 556
Вариант 1: записать в EAX константу.................................................... 556
Пример 3: добавление регистра в процессорный модуль..................... 567
Резюме............................................................................................................ 570
Глава 19. Декомпилятор Ghidra................................................................ 571
Анализ с помощью декомпилятора............................................................. 571
Параметры анализа.................................................................................. 572
Окно декомпилятора..................................................................................... 575
Пример 1: редактирование в окне декомпилятора............................... 576
Пример 2: функции, не возвращающие управление............................ 582
Пример 3: автоматизированное создание структуры........................... 584
Резюме............................................................................................................ 590
Глава 20. Зависимость от компилятора.................................................. 591
Высокоуровневые конструкции................................................................... 592
Предложения switch................................................................................. 592
Пример: сравнение компиляторов gcc и Microsoft C/C++..................... 599
Параметры компилятора.............................................................................. 602
Пример 1: оператор деления по модулю................................................ 603
Пример 2: тернарный оператор............................................................... 606
Пример 3: встраивание функций............................................................ 608
Реализация зависящих от компилятора особенностей C++..................... 610
Перегрузка функций................................................................................. 611
Реализации RTTI...................................................................................... 612

14

Оглавление

Нахождение функции main......................................................................... 617
Пример 1: от _start к main с компилятором gcc для Linux x86-64...... 618
Пример 2: от _start к main с компилятором clang
для FreeBSD x86-64................................................................................... 619
Пример 3: от _start к main с компилятором Microsoft’s C/C++............ 620
Резюме............................................................................................................ 621

ЧАСТЬ V. РЕАЛЬНЫЕ ПРИЛОЖЕНИЯ..................... 623
Глава 21. Анализ обфусцированного кода............................................. 625
Противодействие обратной разработке....................................................... 626
Обфускация................................................................................................ 626
Методы противодействия статическому анализу ................................. 627
Обфускация импортированной функции............................................... 643
Методы противодействия динамическому анализу.............................. 648
Статическая деобфускация двоичных файлов в Ghidra.......................... 654
Скриптовая деобфускация....................................................................... 654
Эмуляторная деобфускация..................................................................... 661
Резюме............................................................................................................ 670
Глава 22. Изменение двоичного кода...................................................... 673
Планирование заплаты................................................................................ 674
Поиск того, что нуждается в изменении..................................................... 675
Поиск в памяти.......................................................................................... 675
Поиск прямых ссылок............................................................................... 676
Поиск командных паттернов................................................................... 677
Поиск конкретных типов поведения....................................................... 682
Наложение заплаты...................................................................................... 683
Внесение простых изменений.................................................................. 683
Внесение нетривиальных изменений..................................................... 690
Экспорт файлов............................................................................................. 694
Форматы экспорта из Ghidra................................................................... 695
Двоичный формат экспорта..................................................................... 696
Экспорт с применением скрипта............................................................. 697
Пример: латание двоичного файла............................................................. 699
Резюме............................................................................................................ 703
Глава 23. Определение разности двоичных файлов
и отслеживание версий................................................................................ 705
Разность двоичных файлов.......................................................................... 706
Инструмент Program Diff......................................................................... 708
Пример: объединение двух проанализированных файлов.................. 712
Сравнение функций................................................................................... 717
Окно сравнения функций........................................................................ 717
Пример: сравнение криптографических функций.................................... 720
Отслеживание версий.................................................................................. 727
Концепции, относящиеся к отслеживанию версий............................... 728
Резюме........................................................................................................... 730
Оглавление

15

Ghidra для пользователей IDA................................................................... 731
Основы............................................................................................................ 731
Создание базы данных............................................................................. 732
Основные окна и навигация.................................................................... 734
Дерево символов........................................................................................ 737
Скрипты.......................................................................................................... 738
Резюме............................................................................................................ 738
Предметный указатель................................................................................. 739

БЛАГОДАРНОСТИ
Эта книга не состоялась бы без помощи и поддержки со стороны исключительно профессионального коллектива издательства No Starch Press. Билл Поллок и Барбара Яин поддержали идею написать книгу о Ghidra, отражающую наше видение,
и мы глубоко ценим их веру в нас, не ослабевавшую на протяжении всего пути. Отзывы Атабаска Уитши о первых главах
стали для нас ценным подспорьем и указали верное направление. Постоянная поддержка Лорел Чан и ее терпеливые ответы на все наши вопросы помогли превратить книгу в готовый
продукт, которым мы очень гордимся. Мы также хотим поблагодарить всех, кто оставался «за кулисами», за тяжелую работу
по претворению мечты в реальность, в т. ч. Катрину Тэйлор,
Бартона Д. Рида, Шарон Уилки и Даниэля Фостера.
Мы также благодарим технического редактора Брайана Хэя,
который проштудировал наши пространные словеса и примеры. Его знания и опыт работы с Ghidra стали гарантией безоши­
бочности содержания книги с технической точки зрения, а его
опыт преподавания позволил изложить материал так, чтобы
он представлял интерес как для начинающих, так и для опытных инженеров.
Мы признательны всей команде разработчиков Ghidra, прошлых и нынешних, из Агентства национальной безопасности
за то, что они создали Ghidra и поделились ей со всем миром,
сделав проектом с открытым исходным кодом.
Кара выражает благодарность Бену за терпение, которое он
проявлял, когда она изучала технологию, и Кэти за терпение,
проявленное во время написания книги. Она также благодарит Йена за вдохновляющее введение и Дики и Ленору, которые никогда не теряли веру в нее. Наконец, она благодарит
Брайа­на за юмор и непрекращающуюся ежедневную и ежечасную поддержку. Не будь этой поддержки, книга не увидела бы
свет.

Благодарности

17

ВВЕДЕНИЕ

Принимаясь за написание этой книги, мы ставили себе целью познакомить с Ghidra нынешних
и будущих специалистов по обратной разработке. В руках опытного инженера Ghidra упрощает
процесс анализа и позволяет настраивать и расширять свои возможности под потребности конкретного
пользователя, так чтобы они соответствовали привычному ему
технологическому процессу. Также Ghidra вполне доступна
начинающим инженерам, чему немало способствует включенный в нее декомпилятор, который позволяет лучше понять связи между высокоуровневым языком программирования и лис­
тингами дизассеблера человеку, только вступающему в мир
анализа двоичного кода.
Писать книгу о Ghidra нелегко. Ghidra – сложный инструмент
с открытым исходным кодом, который постоянно развивается.
Наш текст описывает движущуюся мишень, поскольку сообщест­
во Ghidra продолжает улучшать и расширять возможности программы. Как и во многих других новых проектах с открытым исходным кодом, рождение Ghidra ознаменовалось серией быстро
сменяющих друг друга выпусков. Основная цель авторов состояла в том, чтобы на фоне развития Ghidra все же предложить читателям широкий и глубокий фундамент для понимания и эффективного использования текущей и будущих версий Ghidra в их
работе по обратной разработке. Насколько это возможно, мы старались сделать книгу независимой от версии. По счастью, новые
18

Введение

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

Об этой книге

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

На кого рассчитана эта книга?

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

Структура книги

Эта книга разделена на пять частей. Часть I содержит введение в дизассемблирование, обратную разработку и сам проект
Ghidra. В части II рассматриваются базовые приемы использования Ghidra. Часть III демонстрирует настройку и автоматизацию Ghidra. В части IV более глубоко объясняются конкретные типы модулей Ghidra и вспомогательные концепции.
В части V показано, как применить Ghidra в некоторых реальных ситуациях, с которыми может столкнуться специалист по
обратной разработке.
Введение

19

Часть I. Введение
Глава 1. Введение в дизассемблирование
В этой вводной главе мы познакомимся с теорией и практикой дизассемблирования и обсудим некоторые плюсы и минусы двух наиболее распространенных алгоритмов дизассемб­
лирования.
Глава 2. Инструменты дизассемблирования и обратной
разработки
В этой главе обсуждаются основные категории инструментов
обратной разработки и дизассемблирования.
Глава 3. Первое знакомство с Ghidra
Здесь мы впервые встретимся с Ghidra, узнаем о ее истоках,
о том, как ее получить и начать пользоваться этим свободным
комплектом инструментов с открытым исходным кодом.

Часть II. Основы использования Ghidra
Глава 4. Начало работы с Ghidra
В этой главе начинается наше путешествие в мир Ghidra.
Мы увидим Ghidra в действии, для чего создадим проект, проанализируем файл и познакомимся с графическим интерфейсом Ghidra.
Глава 5. Отображение данных в Ghidra
Здесь мы познакомимся с браузером кода (CodeBrowser), главным аналитическим средством Ghidra, и его основными окнами.
Глава 6. Дизассемблирование в Ghidra
В этой главе мы изучим концепции, необходимые для понимания процесса дизассемблирования в Ghidra.
Глава 7. Управление дизассемблированием
В этой главе мы научимся дополнять анализ Ghidra и управлять процессом дизассемблирования в своих целях.
Глава 8. Типы данных и структуры данных
В этой главе мы научимся распознавать и манипулировать
простыми и сложными структурами данных, встречающимися
в компилированных программах.
20

Введение

Глава 9. Перекрестные ссылки
Эта глава посвящена подробному рассмотрению перекрестных
ссылок, их графическому представлению и той важной роли,
которую они играют в понимании поведения программы.
Глава 10. Графы
В этой главе мы познакомимся с графическими возможностями Ghidra и графами как средством анализа двоичного
кода.

Часть III. Поставить Ghidra себе на службу
Глава 11. Коллективная обратная
разработка программ
В этой главе представлена уникальная возможность Ghidra –
использование в качестве инструмента коллективной работы.
Мы узнаем, как сконфигурировать сервер Ghidra и сделать
проект доступным другим аналитикам.
Глава 12. Настройка Ghidra
Здесь мы начнем настраивать Ghidra, конфигурируя проекты
и инструменты, так чтобы они отвечали нашему технологическому процессу анализа.
Глава 13. Расширение взгляда на мир Ghidra
В этой главе мы научимся генерировать и применять сигнатуры библиотек и другого специального содержимого, чтобы
Ghidra могла распознавать новые конструкции в двоичном
коде.
Глава 14. Основы написания скриптов для Ghidra
В этой главе мы познакомимся с основами написания скриптов для Ghidra на Python и Java с применением встроенного
редактора Ghidra.
Глава 15. Eclipse и GhidraDev
В этой главе мы поднимем написание скриптов на новый
уровень, интегрировав Eclipse с Ghidra и воспользовавшись
мощными скриптовыми возможностями, предоставляемыми
этой конфигурацией, в частности приведем рабочий пример
построе­ния нового анализатора.
Введение

21

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

Часть IV. Дополнительные темы
Глава 17. Загрузчики Ghidra
Здесь мы более глубоко познакомимся с тем, как Ghidra импортирует и загружает файлы. У нас будет возможность создать
новые загрузчики для обработки ранее не распознаваемых типов файлов.
Глава 18. Процессоры Ghidra
В этой главе мы рассмотрим язык Ghidra SLEIGH, предназначенный для определения архитектуры процессора. Мы изучим
процесс добавления новых процессоров и команд в Ghidra.
Глава 19. Декомпилятор Ghidra
Здесь мы подробнее рассмотрим одну из самых популярных
возможностей Ghidra: декомпилятор. Вы увидите, как он работает на внутреннем уровне и какой вклад вносит в процесс
анализа.
Глава 20. Зависимость от компилятора
Эта глава посвящена вариациям кода в зависимости от компилятора и целевой платформы.

Часть V. Реальные приложения
Глава 21. Анализ обфусцированного кода
Мы узнаем, как использовать Ghidra для анализа обфусцированного кода в статическом контексте, так чтобы код не нужно
было исполнять.
Глава 22. Изменение двоичного кода
В этой главе мы узнаем о некоторых способах использования
Ghidra для изменения двоичного кода в процессе анализа –
как внутри самой Ghidra, так и для создания «залатанных»
версий оригинальных двоичных файлов.
22

Введение

Глава 23. Определение разности двоичных файлов
и отслеживание версий
В этой, последней, главе приводится обзор средств Ghidra, позволяющих вычислить дельту между двумя двоичными файлами, а также краткое введение в дополнительные средства
отслеживания версий.
Приложение. Ghidra для пользователей IDA
Опытные пользователи IDA найдут в этом приложении информацию о соответствии между терминологией и сходной функциональностью IDA и Ghidra.
Код, встречающийся в листингах, можно найти
на сайтах https://nostarch.com/GhidraBook/ и
https://ghidrabook.com/, а также на сайте издательства
https://dmkpress.com/catalog/computer/securuty/978-5-97060-942-2/

ПРИМЕЧАНИЕ

Часть I
Введение

1

ВВЕДЕНИЕ
В ДИЗАССЕМБЛИРОВАНИЕ

Вам, наверное, интересно, чего ожидать
от книги о Ghidra. Конечно, эта книга
целиком посвящена Ghidra, но она не задумывалась как «Руководство пользователя Ghidra». Мы хотели использовать Ghidra
как инструмент для обсуждения методов обратной разработки, полезных при анализе широкого круга программ – от уязвимых приложений до вредоносного программного обеспечения. Там, где это оправдано, мы
будем подробно описывать шаги применения Ghidra
для выполнения конкретных действий в данной ситуа­
ции. В результате получилась обзорная экскурсия по
всем возможностям Ghidra, начиная с самых простых
задач, решаемых в ходе начального исследования файла, и заканчивая настройкой Ghidra для более сложных
задач обратной разработки. Мы не ставили себе целью
рассмотреть весь потенциал Ghidra. Но средства, наиболее полезные для обратной разработки, мы включили.

Эта книга поможет вам сделать Ghidra самым эффективным
орудием в своем арсенале.
Прежде чем с головой погрузиться в специфику Ghidra, поговорим об основах процесса дизассемблирования и других средствах обратной разработки откомпилированного кода. Хотя эти
средства необязательно покрывают весь спектр возможностей
Ghidra, каждое из них соответствует какой-то части функцио­
нальности Ghidra и позволяет лучше понять ее конкретные
особенности. Далее в этой главе процесс дизассемблирования
описывается на верхнем уровне.

ТЕОРИЯ ДИЗАССЕМБЛИРОВАНИЯ
Каждый, кто хоть немного изучал языки программирования,
вероятно, знает о различных поколениях языков, но для тех,
кто все это время проспал, дадим краткую справку.
Языки первого поколения. Это самая низшая форма языков, программируют на них, записывая команды в двоичном
или шестнадцатеричном виде, а прочитать такой код могут
немногие умельцы. На этом уровне трудно отличить данные
от команд, потому что выглядят они одинаково. Языкипервого поколения называют еще машинными языками, или
байт-кодом, а написанные на них программы – двоичными.
Языки второго поколения. Их еще называют языками ассемблера, от машинных языков их отделяет только простой
поиск в таблице, позволяющий сопоставить комбинациям битов, или кодам операций (опкодам), короткие, но легко запоминающиеся последовательности символов – мнемонические
коды, облегчающие программистам запоминание команд.
Ассемблером называется программа, которая транслирует
код на языке ассемблера в машинный код, пригодный для
выполнения. Помимо мнемонических кодов команд, полный
язык ассемблера обычно включает директивы, указывающие ассемблеру, как размещать код и данные в памяти.
Языки третьего поколения. Это следующий шаг в направлении выразительности естественных языков – вводятся ключевые слова и конструкции, используемые в качестве
строительных блоков, из которых создаются программы.
28

ГЛАВА 1

Языки третьего поколения обычно не зависят от платформы, хотя написанные на них программы могут быть платформенно зависимыми из-за использования особенностей
конкретной операционной системы. Говоря о языках третьего поколения, чаще всего вспоминают FORTRAN, C и Java.
Для трансляции программы на язык ассемблера или прямо
на машинный язык (или его приблизительный эквивалент,
например байт-код) обычно используются компиляторы.
Языки четвертого поколения. Такие существуют, но
к теме этой книги не имеют отношения и потому не рассмат­
риваются.

ЧТО ДЕЛАЕТ ДИЗАССЕМБЛЕР
В традиционной модели разработки ПО компиляторы, ассемб­
леры и компоновщики используются по отдельности либо совместно для создания исполняемых программ. Чтобы проделать обратный путь (т. е. подвергнуть программу обратной
разработке), мы применяем инструменты, обращающие процессы ассемблирования и компиляции. Неудивительно, что
они называются дизассемблерами и декомпиляторами. Диз­
ассемблер обращает процесс ассемблирования, так что на выходе мы ожидаем получить код на языке ассемблера (а на вход
подаем код на машинном языке). Задача декомпилятора – восстановить код на языке высокого уровня, получив на входе код
на языке ассемблера или даже на машинном языке.
Обещание «восстановить исходный код» звучит очень заманчиво на конкурентном рынке программного обеспечения, поэтому разработка хороших декомпиляторов остается областью активных исследований в информатике. Ниже перечислено лишь
несколько из множества причин, затрудняющих декомпиляцию.
► В процессе компиляции теряется часть информации.
На уровне машинного языка не существует имен переменных и функций, а тип информации можно определить
только по характеру использования данных, поскольку явных объявлений типов нет. Видя, что копируется
32 бита данных, мы должны проделать исследовательскую работу, чтобы установить, что это такое: 32-разрядВведение в дизассемблирование

29

ное целое число, 32-разрядное число с плавающей точкой
или 32-разрядный указатель.
► Компиляция – это операция вида «многие ко многим». Это означает, что исходную программу можно
транслировать на язык ассемблера разными способами,
а коду на машинном языке могут соответствовать разные
конструкции на исходном коде. Поэтому декомпиляция
только что откомпилированного файла может дать исходный файл, сильно отличающийся от оригинала.
► Декомпиляторы зависят от языка и библиотек. Обработка двоичного файла, порожденного компилятором
Delphi, с помощью декомпилятора, рассчитанного на генерирование C-кода, может привести к очень странным
результатам. Аналогично прогон двоичного файла для
Windows через декомпилятор, который ничего не знает
о Windows API, вряд ли даст что-то полезное.
► Для точной декомпиляции двоичного файла необходим почти идеальный дизассемблер. Любая ошибка
или пропуск на этапе дизассемблирования почти наверняка отразится на декомпилированном коде. Правильность
дизассемблированного кода можно проверить, прибегнув
к справочным руководствам по соответствующему процессору, но для проверки правильности результата декомпиляции нет никаких канонических справочных руководств.
В Ghidra встроен декомпилятор, который мы будем изучать
в главе 19.

ЗАЧЕМ НУЖЕН ДИЗАССЕМБЛЕР
Цель инструментов дизассемблирования часто состоит в том,
чтобы разобраться в программе, исходный код которой недоступен. Перечислим типичные ситуации:
► анализ вредоносного ПО;
► анализ программ с закрытым исходным кодом на уязвимость;
► анализ интероперабельности программ с закрытым исходным кодом;
30

ГЛАВА 1

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

Анализ вредоносного ПО
Авторы вредоносного ПО, если только это не скрипты, редко
оказывают нам любезность, предоставляя исходный код своих
творений. А в отсутствие исходного кода наши возможности
понять, как ведет себя вредонос, крайне ограничены. Есть два
основных вида анализа: динамический и статический. Динамический анализ подразумевает выполнение вредоносного
кода в тщательно контролируемом окружении (песочнице),
когда за всеми аспектами поведения наблюдают с помощью
различных инструментальных утилит. Напротим, статический анализ – это попытка понять, что делает программа,
читая ее код, который в случае вредоносного ПО чаще всего
состоит только из листинга дизассемблера и, возможно, лис­
тинга декомпилятора.

Анализ на уязвимость
Для простоты разобьем весь процесс аудита безопасности на
три этапа: обнаружение уязвимости, анализ уязвимости и разработка эксплойта. Одни и те же шаги выполняются вне зависимости от того, есть исходный код или нет, однако объем
усилий резко возрастает, если имеется только двоичный код.
Первый этап – найти место в программе, потенциально допус­
кающее эксплуатацию. Для этого часто применяются динамические методы, например фаззинг1, но то же самое можно
сделать (обычно с гораздо большими усилиями) с помощью статического анализа. После того как проблема выявлена, часто
требуется дальнейший анализ, чтобы понять, допускает ли она
эксплуатацию, и если да, то при каких условиях.
1

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

31

Нахождение переменных, которыми атакующий может
с пользой для себя манипулировать, – важный ранний шаг
процесса обнаружения. Листинги дизассемблера дают достаточный уровень детализации, чтобы точно понять, как компилятор решил размещать переменные в памяти. Например,
бывает полезно знать, что 70-байтовый массив символов, объявленный программистом, компилятор округлил до 80 байт.
Кроме того, листинги дизассемблера – единственный способ
точно выяснить, как компилятор решил упорядочить переменные, объявленные глобально или внутри функций. Знание
пространственных отношений между переменными зачастую
необходимо для разработки эксплойта. Так, совместно используя дизассемблер и отладчик, удается создать эксплойт.

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

Проверка компилятора
Поскольку цель компилятора (или ассемблера) – генерирование машинного кода, часто необходимы хорошие инструменты
дизассемблирования, которые проверят, действует ли компилятор в соответствии с проектными спецификациями. Аналитикам также интересно найти дополнительные возможности
для оптимизации выхода компилятора. Да и с точки зрения
безопасности хорошо бы убедиться, что сам компилятор невозможно скомпрометировать, так что он будет оставлять закладки в сгенерированном коде.
32

ГЛАВА 1

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

КАК РАБОТАЕТ ДИЗАССЕМБЛЕР
Разобравшись с целями дизассемблирования, самое время заняться вопросом о том, как в действительности работает диз­
ассемблер. Рассмотрим типичную внушающую страх задачу,
стоящую перед дизассемблером: взять предложенные 100 КБ,
отличить код от данных, перевести код на язык ассемблера для представления пользователю и не наделать по дороге
ошибок. Можно было бы пристегнуть сюда кучу специальных
запросов, например попросить дизассемблер найти функции,
распознать таблицы переходов или выявить локальные переменные, – все это сильно затрудняет его работу.
Чтобы учесть все наши требования, дизассемблер должен сделать выбор из множества алгоритмов обработки передаваемых
ему файлов. Качество сгенерированных листингов напрямую
зависит от качества используемых алгоритмов и их реализации.
В этом разделе мы обсудим два главных алгоритма, применяемых в настоящее время для дизассемблирования машинного кода. По ходу обсуждения мы будем отмечать их недостатки,
чтобы вы были готовы к ситуациям, когда кажется, что дизассемблер не работает. Понимая ограничения дизассемблера, вы
сможете вмешаться вручную и повысить качество результата.

Базовый алгоритм дизассемблирования
Для начала разработаем простой алгоритм, который на входе
принимает код на машинном языке, а на выходе порождает код
на языке ассемблера. По ходу дела вы получите представлеВведение в дизассемблирование

33

ние о проблемах, предположениях и компромиссах, сопровож­
дающих процесс автоматического дизассемблирования.
1. Первый шаг – найти область кода, подлежащую дизассемб­
лированию. Далеко не всегда это так просто, как может показаться. Часто команды перемешаны с данными, и важно
отличать одного от другого. В самом типичном случае, когда дизассемблируется исполняемый файл, известен формат
этого файла, например Portable Executable (PE) в Windows
или Executable and Linkable Format (ELF) во многих Unixсистемах. В этих форматах обычно имеются механизмы (часто
в виде иерархических заголовков файла) нахождения секций
файла, содержащих код, а также точек входа в этот код1.
2. Зная адрес команды, мы должны прочитать значение или
значения, находящиеся по этому адресу (или смещению от
начала файла), и найти в таблице мнемоническую команду ассемблера, соответствующую данному коду операции.
В зависимости от сложности системы команд процессора этот процесс может быть нетривиальным и, возможно,
включает ряд дополнительных операций, например интерпретацию префиксов, модифицирующих поведение команды, и определение того, какие операнды необходимы
команде. Если команды имеют переменную длину, как
в системе команд Intel x86, то не исключено, что для полного дизассемблирования одной команды придется прочитать дополнительные байты.
3. После того как команда прочитана из файла и все ее операнды декодированы, форматируется эквивалентная ей
команда на языке ассемблера, и результат записывается
в листинг дизассемблера. Бывает так, что есть выбор из нескольких вариантов синтаксиса языка ассемблера. Например, для ассемблера x86 есть два основных формата: Intel
и AT&T.
4. Завершив вывод команды, мы должны продвинуться к началу следующей и повторить весь процесс, пока не будут
дизассемблированы все команды в файле.

1

34

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

ГЛАВА 1

Синтаксис ассемблера X86:
AT&T и INTEL
Есть два основных варианта синтаксиса кода на языке ассемблера: AT&T и Intel. Хотя тот и другой – языки второго поколения, их
синтаксис существенно различается – от записи переменных, констант и доступа к регистрам до переопределения размера сегментов и команд, описания косвенности и задания смещений. Синтаксис AT&T выделяется использованием префикса % в именах
всех регистров, префикса $ в именах литералов (так называемых
непосредственных операндов) и порядком операндов: исходный
операнд находится слева, а конечный справа. В синтаксисе AT&T
команда прибавления 4 к регистру EAX имеет вид add $0x4,%eax.
В ассемблере GNU (as) и многих других инструментах GNU, включая gcc и gdb, по умолчанию используется синтаксис AT&T.
Синтаксис Intel отличается отсутствием префиксов у литералов
и регистров и противоположным порядком записи операндов: исходный справа, конечный слева. Та же команда сложения в синтаксисе Intel имеет вид add eax,0x4. Из ассемблеров, в которых используется синтаксис Intel, отметим Microsoft Assembler (MASM)
и Netwide Assembler (NASM).

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

Алгоритм линейной развертки
В случае алгоритма линейной развертки дизассемблер применяет прямолинейный подход к нахождению команд, подлежащих дизассемблированию: там, где одна команда кончается,
начинается другая. Поэтому самые трудные решения – откуда
начать и когда остановиться. Чаще всего предполагается, что
всё находящееся в секциях программы, помеченных как код
(обычно это указывается в заголовках исполняемого файла), –
команды машинного языка. Дизассемблирование начинается
с первого байта в секции кода и продвигается вперед линейно,
пока не будет достигнут конец секции. Не предпринимается
Введение в дизассемблирование

35

никаких попыток учесть поток выполнения, распознавая команды нелинейного перехода, например ветвления.
В процессе дизассемблирования может поддерживаться указатель, отмечающий начало текущей команды. По ходу дела
вычисляется длина каждой команды, и это знание используется для определения адреса начала следующей команды. Для
систем команд с фиксированной длиной (например, MIPS) диз­
ассемблирование немного проще, поскольку для разграничения команд не нужно прилагать усилий.
Главное преимущество алгоритма линейной развертки – то,
что он полностью покрывает все имеющиеся в программе секции кода. А его главный недостаток – то, что он не учитывает
возможность смешивания кода и данных. Это хорошо видно
в листинге 1.1, где показан результат обработки функции диз­
ассемблером, основанным на алгоритме линейной развертки.
40123f: 55 push ebp
401240: 8b ec mov ebp,esp
401242: 33 c0 xor eax,eax
401244: 8b 55 08 mov edx,DWORD PTR [ebp+8]
401247: 83 fa 0c cmp edx,0xc
40124a: 0f 87 90 00 00 00 ja 0x4012e0
401250: ff 24 95 57 12 40 00 jmp DWORD PTR [edx*4+0x401257]
 401257: e0 12 loopne 0x40126b
401259: 40 inc eax
40125a: 00 8b 12 40 00 90 add BYTE PTR [ebx-0x6fffbfee],cl
401260: 12 40 00 adc al,BYTE PTR [eax]
401263: 95 xchg ebp,eax
401264: 12 40 00 adc al,BYTE PTR [eax]
401267: 9a 12 40 00 a2 12 40 call 0x4012:0xa2004012
40126e: 00 aa 12 40 00 b2 add BYTE PTR [edx-0x4dffbfee],ch
401274: 12 40 00 adc al,BYTE PTR [eax]
401277: ba 12 40 00 c2 mov edx,0xc2004012
40127c: 12 40 00 adc al,BYTE PTR [eax]
40127f: ca 12 40 lret 0x4012
401282: 00 d2 add dl,dl
401284: 12 40 00 adc al,BYTE PTR [eax]
401287: da 12 ficom DWORD PTR [edx]
401289: 40 inc eax
40128a: 00 8b 45 0c eb 50 add BYTE PTR [ebx+0x50eb0c45],cl
401290: 8b 45 10 mov eax,DWORD PTR [ebp+16]
401293: eb 4b jmp 0x4012e0

Листинг 1.1. Дизассемблирование методом линейной развертки
36

ГЛАВА 1

Эта функция содержит предложение switch, и компилятор
решил реализовать его с помощью таблицы переходов, в которой хранятся адреса меток case. Кроме того, компилятор решил
разместить таблицу переходов в самой функции. Команда jmp 
ссылается на таблицу адресов . К сожалению, дизассемб­лер
рассматривает таблицу адресов как последовательность команд и неправильно генерирует следующий за ней код на языке ассемблера.
Если рассматривать 4-байтовые группы в таблице переходов
 как значения, записанные в прямом порядке байтов1, то мы
увидим, что каждая представляет собой указатель на близлежащий адрес, т. е. конечные адреса команд переходов (004012e0,
0040128b, 00401290, …). Таким образом, команда loopne  – и не
команда вовсе, а свидетельство того, что алгоритм линейной
развертки не способен отличить встроенные данные от кода.
Алгоритм линейной развертки используется в движках диз­
ассемблирования, входящих в состав отладчика GNU (gdb), отладчика Microsoft WinDbg, и в утилите objdump.

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

Последовательные команды
Последовательная команда передает управление команде, непосредственно следующей за ней. Примерами могут служить
простые арифметические команды, в частности add; команды
копирования из регистра в память (mov); команды манипулирования стеком (push и pop). Для таких команд дизассемблер ведет себя так же, как в случае алгоритма линейной развертки.
1

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

37

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

Команды безусловного перехода
Команда безусловного перехода не согласуется с моделью линейного выполнения, поэтому обрабатывается алгоритмом рекурсивного спуска по-другому. Как и в случае последовательных команд, выполнение может пойти только по одному пути,
но следующая выполняемая команда необязательно находится сразу после команды перехода. Как видно из листинга 1.1,
такого требования нет и в помине. Поэтому нет никаких причин дизассемблировать байты, следующие за командой безусловного перехода.
Дизассемблер пытается определить конечный адрес безусловного перехода и продолжает дизассемблирование с этого
адреса. К сожалению, некоторые безусловные переходы вызывают трудности. Если конечный адрес команды перехода
зависит от значения, вычисляемого во время выполнения, то
определить его с помощью статического анализа невозможно.
Иллюстрацией может служить команда x86 jmp rax. Регистр
rax содержит разумное значение, только когда программа работает. А во время статического анализа в этом регистре ничего
полезного нет, поэтому мы не можем определить адрес перехода и не знаем, с какого места продолжать дизассемблирование.

38

ГЛАВА 1

Команды вызова функций
Команда вызова функции похожа на команду безусловного
перехода (и дизассемблер точно так же не может определить
целевой адрес в команде типа call rax), отличие только в том,
что после выполнения функции управление обычно возвращается команде, непосредственно следующей за командой
вызова. В этом отношении команда вызова похожа на команду условного перехода, т. к. порождается два пути выполнения. Целевой адрес команды вызова добавляется в список
адресов для отложенного дизассемблирования, а следующая
за ней команда дизассемблируется сразу, как в алгоритме
линейной развертки.
Метод рекурсивного спуска может давать сбои, если программа ведет себя не так, как ожидается, при возврате из
функции. Например, функция может намеренно изменить
адрес возврата, так что после завершения управление возвращается совсем не туда, куда ожидает дизассемблер. Простой
пример приведен в следующем листинге, где некорректно написанная функция badfunc прибавляет 1 к адресу возврата,
перед тем как вернуть управление вызывающей стороне.
badfunc proc near
48 FF 04 24 inc qword ptr [rsp] ; увеличивает сохраненный адрес возврата на 1
C3 retn
badfunc endp
; ------------------------------------label:
E8 F6 FF FF FF call badfunc
05 48 89 45 F8 add eax, F8458948h

В результате управление не попадает на команду add ,
следующую за вызовом badfunc. Ниже показано, что должен
был бы сделать дизассемблер.

Введение в дизассемблирование

39

badfunc proc near
48 FF 04 24 inc qword ptr [rsp]
C3 retn
badfunc endp
; ------------------------------------label:
E8 F6 FF FF FF call badfunc
05 db 5 ;раньше это был первый байт команды add
48 89 45 F8 mov [rbp-8], rax

В этом листинге лучше виден поток управления в случае,
когда функция badfunc возвращает управление команде
mov . Важно понимать, что метод линейной развертки тоже
не сможет правильно дизассемблировать этот код, хотя и по
другой причине.

Команды возврата
В некоторых ситуациях у алгоритма рекурсивного спуска
не оказывается путей, по которым можно следовать. Команда возврата из функции (например, ret в случае x86) не содержит никакой информации о том, какая команда будет выполняться следующей. Если бы программа работала, то адрес
возврата был бы извлечен из стека времени выполнения,
и выполнение продолжилось бы с этого адреса. Но у дизассемблера нет доступа к стеку, поэтому он просто «упирается
в стенку». Именно в этой точке алгоритм рекурсивного спуска
обращается к списку адресов для отложенного дизассемблирования. Адрес выбирается из списка, и дизассемблирование
продолжается с этого адреса. Этот рекурсивный процесс и дал
название алгоритму.
Одно из главных достоинств алгоритма рекурсивного спус­
ка – его непревзойденная способность отличать код от данных.
Поскольку он основан на анализе потока управления, шансов неправильно дизассемблировать данные как код гораздо
меньше. А его основной недостаток – неспособность следовать
по косвенным путям в коде, как, например, бывает, когда в командах перехода или вызова используются таблицы указателей, в которых хранятся конечные адреса. Однако благодаря

40

ГЛАВА 1

добавлению некоторых эвристик для отличения указателей от
кода дизассемблеры на основе рекурсивного спуска могут обес­
печить весьма полное покрытие кода и прекрасно различают
код и данные. В листинге 1.2 показан результат работы диз­
ассемблера, встроенного в Ghidra, над тем же предложением
switch, что в листинге 1.1.
0040123f PUSH EBP
00401240 MOV EBP,ESP
00401242 XOR EAX,EAX
00401244 MOV EDX,dword ptr [EBP + param_1]
00401247 CMP EDX,0xc
0040124a JA switchD_00401250::caseD_0
switchD_00401250::switchD
00401250 JMP dword ptr [EDX*0x4 + ->switchD_00401250::caseD_0] = 004012e0
switchD_00401250::switchdataD_00401257
00401257 addr switchD_00401250::caseD_0
0040125b addr switchD_00401250::caseD_1
0040125f addr switchD_00401250::caseD_2
00401263 addr switchD_00401250::caseD_3
00401267 addr switchD_00401250::caseD_4
0040126b addr switchD_00401250::caseD_5
0040126f addr switchD_00401250::caseD_6
00401273 addr switchD_00401250::caseD_7
00401277 addr switchD_00401250::caseD_8
0040127b addr switchD_00401250::caseD_9
0040127f addr switchD_00401250::caseD_a
00401283 addr switchD_00401250::caseD_b
00401287 addr switchD_00401250::caseD_c
switchD_00401250::caseD_1
0040128b MOV EAX,dword ptr [EBP + param_2]
0040128e JMP switchD_00401250::caseD_00040128E

Листитнг 1.2. Результат дизассемблирования методом
рекурсивного спуска
Заметим, что эта секция двоичного кода была распознана
как предложение switch и соответственно отформатирована.
Понимание процесса рекурсивного спуска поможет нам выявить ситуации, когда дизассемблер Ghidra ведет себя неоптимально, и выработать стратегии улучшения результата.

Введение в дизассемблирование

41

РЕЗЮМЕ
Важно ли глубоко понимать алгоритмы дизассемблирования
при использовании дизассемблера? Нет. Полезно ли это? Да!
Бороться со своими инструментами – последнее дело, на которое стоит тратить время, занимаясь обратной разработкой.
Одно из многих преимуществ Ghidra заключается в том, что ее
дизассемблер интерактивный, он предоставляет массу возможностей для управления процессом и отмены своих решений. Поэтому очень часто мы получаем полный и точный листинг.
В следующей главе мы рассмотрим ряд инструментов, доказавших свою полезность во многих возникающих при обратной разработке ситуациях. Хотя они и не связаны напрямую
с Ghidra, многие из них оказали на Ghidra влияние, и знакомство с ними поможет лучше понять различные окна в пользовательском интерфейсе Ghidra.

2

ОБРАТНАЯ РАЗРАБОТКА
И ИНСТРУМЕНТЫ
ДИЗАССЕМБЛИРОВАНИЯ

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

СРЕДСТВА КЛАССИФИКАЦИИ
Сталкиваясь с незнакомым файлом, мы часто хотим получить
ответ на простые вопросы, например: «Что это за зверь такой?»
Первое правило – никогда не полагаться на расширение файла, желая узнать, что в нем находится. Это не только первое, но
второе, третье и четвертое правила. Смирившись с мыслью, что
расширения файла не имеют никакого смысла, можете познакомиться с одной или несколькими из описанных ниже утилит.

file
Команда file – стандартная утилита, входящая в состав большинства операционных систем типа *nix, а также в подсистему Windows для Linux (Windows Subsystem for Linux – WSL)1.
Пользователи Windows могут получить эту команду, установив
Cygwin или MinGW2. Команда file пытается определить тип
файла, анализируя некоторые поля в нем. Иногда file распо­
знает хорошо известные строки, например #!/bin/sh (скрипт
оболочки) или (HTML-документ).
Файлы с нетекстовым содержимым сложнее. В таких случаях file пытается определить, соответствует ли структура файла
какому-нибудь известному формату. Часто она ищет некоторые
признаки (называемые магическими числами)3, уникальные
для файлов данного типа. Ниже приведены примеры некоторых магических чисел и соответствующих им типов файлов.

1
2
3

44

См. https://docs.microsoft.com/en-us/windows/wsl/about/.
О Cygwin см. http://www.cygwin.com/. О MinGW см. http://www.mingw.org/.
Магическое число – это специальный признак, описанный в спецификациях некоторых форматов файлов, наличие которого означает соответствие данной спецификации. Иногда выбор магических чисел – шутка.
Так, признак MZ в заголовке исполняемого файла MS-DOS – инициалы
Марка Збиковски, одного из первых архитекторов MS-DOS, а шестнадцатеричное значение 0xcafebabe, ассоциированное с файлами классов в Java
(с расширением .class), было выбрано, потому что эта последовательность
легко запоминается.

ГЛАВА 2

Формат исполняемого файла
00000000 4D 5A 90 00 03
MZ..............
00000010 B8 00 00 00 00
........@.......
Графический формат Jpeg
00000000 FF D8 FF E0 00
JFIF.....`
00000010 00 60 00 00 FF
.`.....C........
.class-файл Java
00000000 CA FE BA BE 00
.......2......>.
00000010 00 3F 09 00 40
.?..@.A..B..C.D.

Windows PE
00 00 00 04 00 00 00 FF FF 00 00
00 00 00 40 00 00 00 00 00 00 00

10 4A 46 49 46 00 01 01 01 00 60 ......
DB 00 43 00 0A 07 07 08 07 06 0A

00 00 32 00 98 0A 00 2E 00 3E 08
00 41 08 00 42 0A 00 43 00 44 0A

Команда file распознает много форматов, в т. ч. несколько типов текстовых ASCII-файлов, различные исполняемые
файлы и файлы данных. Правила проверки магических чисел хранятся в магическом файле. Где этот файл находится по
умолчанию, зависит от операционной системы, типичные мес­
тоположения: /usr/share/file/magic, /usr/share/misc/magic
и /etc/magic. Дополнительные сведения о магических файлах
смотрите в документации по программе file.
В некоторых случаях file умеет распознавать вариации
в пределах заданного типа файла. В листинге ниже показано, что file распознает не только несколько вариантов формата ELF, но также выдает информацию о том, как файл
был скомпонован (статически или динамически) и был ли
он обработан программой strip, удаляющей информацию
о символах.
ghidrabook# file ch2_ex_*
ch2_ex_x64: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/l, for GNU/Linux
3.2.0, not stripped
ch2_ex_x64_dbg: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/l, for GNU/Linux
3.2.0, with debug_info, not stripped
ch2_ex_x64_static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux),
statically linked, for GNU/Linux 3.2.0, not stripped
ch2_ex_x64_strip: ELF 64-bit LSB shared object, x86-64, version 1
(SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux
3.2.0, stripped
Обратная разработка...

45

ch2_ex_x86:


ch2_ex_x86_dbg:


ch2_ex_x86_static:


ch2_ex_x86_strip:


ch2_ex_Win32:
ch2_ex_x64:

ELF 32-bit LSB shared object, Intel 80386, version 1
(SYSV), dynamically linked, interpreter /lib/ld-, for
GNU/Linux 3.2.0, not stripped
ELF 32-bit LSB shared object, Intel 80386, version 1
(SYSV), dynamically linked, interpreter /lib/ld-, for
GNU/Linux 3.2.0, with debug_info, not stripped
ELF 32-bit LSB executable, Intel 80386, version 1
(GNU/Linux), statically linked, for GNU/Linux 3.2.0,
not stripped
ELF 32-bit LSB shared object, Intel 80386, version 1
(SYSV), dynamically linked, interpreter /lib/ld-, for
GNU/Linux 3.2.0, stripped
PE32 executable (console) Intel 80386, for MS Windows
PE32+ executable (console) x86-64, for MS Windows

Среда WSL
Подсистема Windows для Linux (WSL) предлагает командное
окружение GNU/Linux прямо в Windows без необходимости создавать виртуальную машину. В процессе установки WSL пользователь выбирает дистрибутив Linux, после чего может запускать
его в WSL. Тем самым пользователь получает доступ к стандартным командным утилитам (grep, awk), компиляторам (gcc, g++),
интерпретаторам (Perl, Python, Ruby), сетевым утилитам (nc,
ssh) и многому другому. После установки WSL многие программы, написанные для Linux, можно откомпилировать и выполнить
в системах Windows.

Утилита file и ей подобные не дают стопроцентно верного
результата. Вполне может случиться, что файл распознан неправильно, потому что он случайно содержит характерные признаки определенного формата. Можете убедиться в этом сами,
заменив в шестнадцатеричном редакторе первые 4 байта любого файла магической последовательностью Java: CA FE BA BE.
После этого file некорректно идентифицирует модифицированный файл как откомпилированный класс Java. Аналогично
текстовый файл, содержащий всего два символа MZ, будет идентифицирован как исполняемый файл MS-DOS. В процессе обратной разработки лучше всего не доверять полностью результату любого инструмента, не сопоставив результаты нескольких
инструментов и не проанализировав данные вручную.
46

ГЛАВА 2

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

PE Tools
PE Tools – набор инструментов, полезный для анализа выполняемых процессов и исполняемых файлов в системах Windows1.
На рис. 2.1 показан основной интерфейс PE Tools – отображается список активных процессов и предоставляется доступ ко
всем входящим в состав набора утилитам.
Отправляясь от списка процессов, пользователь может записать дамп памяти процесса в файл или воспользоваться утилитой PE Sniffer, чтобы определить, какой компилятор создал
исполняемый файл и был ли файл обработан какой-нибудь из
известных утилит обфускации. Меню Tools предлагает аналогичные возможности для анализа файлов на диске. Пользователь может просматривать поля заголовка PE-файла с по­
мощью встроенной утилиты PE Editor, которая также позволяет
изменять значения полей. Модификация заголовка PE-файла
часто необходима для реконструкции оригинального файла из
его обфусцированной версии.

1

См. https://github.com/petoolse/petools/.
Обратная разработка...

47

Обфускация двоичного файла
Обфускацией называется любая попытка запутать истинный смысл
чего-либо. Применительно к исполняемым файлам обфускация –
это попытка скрыть истинное поведение программы. Программис­
ты используют обфускацию по ряду причин. Типичные примеры:
защита коммерческих алгоритмов и сокрытие злых намерений.
Почти все виды вредоносного ПО применяют обфускацию с целью
противостоять попыткам анализа. Существуют многочисленные инструменты, помогающие авторам создавать обфусцированные программы. Эти инструменты и методы, а также их влияние на процесс
обратного конструирования будут обсуждаться в главе 21.

Рис. 2.1. Утилита PE Tools

PEiD
PEiD – еще один инструмент для Windows, основная цель которого – определить, какой компилятор создал данный двоичный
PE-файл и какие инструменты применялись для его обфускации1. На рис. 2.2 показано применение PEiD для определения
инструмента (в данном случае ASPack), которым был обфусцирован вариант червя Gaobot2.
1
2

48

См. https://github.com/wolfram77web/app-peid/.
См. https://www.trendmicro.com/vinfo/us/threat-encyclopedia/malware/GAOBOT/.

ГЛАВА 2

Рис. 2.2. Утилита PEiD
Многие дополнительные возможности PEiD перекрываются
с возможностями PE Tools, в частности умение генерировать
сводку заголовков PE-файла, собирать информацию о работающих процессах и выполнять простое дизассемблирование.

ОБЗОРНЫЕ ИНСТРУМЕНТЫ
Поскольку наша цель – обратная разработка двоичных файлов программ, нам понадобятся более изощренные инструменты для извлечения подробной информации после начальной
классификации файла. Инструменты, обсуждаемые в этом разделе, по необходимости гораздо больше знают о форматах обрабатываемых файлов. В большинстве случаев инструменты
понимают один конкретный формат и используются для разбора файла и извлечения весьма специальной информации.

nm
Создавая объектный файл из исходного, компилятор должен
включить информацию о местоположении всех глобальных
(внешних) символов, чтобы компоновщик смог разрешить
ссылки на них в процессе объединения нескольких объектных
файлов с целью создания исполняемого. Если не было указаний удалить символы из окончательного исполняемого файла,
то компоновщик обычно переносит символы из объектных файлов в исполняемый. Согласно странице руководства, утилита
nm «печатает список символов в объектных файлах».
Обратная разработка...

49

По умолчанию nm, примененная к промежуточному объектному файлу (с расширением .o), выводит имена всех функций
и глобальных переменных, объявленных в файле. Ниже приведен пример работы nm.
ghidrabook# gcc -c ch2_nm_example.c
ghidrabook# nm ch2_nm_example.o
U exit
U fwrite
000000000000002e t get_max
U _GLOBAL_OFFSET_TABLE_
U __isoc99_scanf
00000000000000a6 T main
0000000000000000 D my_initialized_global
0000000000000004 C my_uninitialized_global
U printf
U puts
U rand
U srand
U __stack_chk_fail
U stderr
U time
0000000000000000 T usage
ghidrabook#

Мы видим, что nm выводит все символы и информацию о каждом из них. Буквенный код обозначает тип символа. В данном
примере встречаются следующие коды:
U неопределенный символ (обычно ссылка на внешний символ);
T символ, определенный в секции text (обычно имя функции);
t локальный символ, определенный в секции text. В программе на C это обычно статическая функция;
D инициализированные данные;
C неинициализированные данные.
ПРИМЕЧАНИЕ

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

Несколько больше информации выводится, когда nm используется для отображения символов в исполняемом файле.
50

ГЛАВА 2

В процессе компоновки символам сопоставляются виртуальные адреса (если возможно), поэтому nm доступно больше информации. Сокращенная распечатка, созданная при запуске
nm для исполняемого файла, приведена ниже.
ghidrabook# gcc -o ch2_nm_example ch2_nm_example.c
ghidrabook# nm ch2_nm_example
...
U fwrite@@GLIBC_2.2.5
0000000000000938 t get_max
0000000000201f78 d _GLOBAL_OFFSET_TABLE_
w __gmon_start__
0000000000000c5c r __GNU_EH_FRAME_HDR
0000000000000730 T _init
0000000000201d80 t __init_array_end
0000000000201d78 t __init_array_start
0000000000000b60 R _IO_stdin_used
U __isoc99_scanf@@GLIBC_2.7
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
0000000000000b50 T __libc_csu_fini
0000000000000ae0 T __libc_csu_init
U __libc_start_main@@GLIBC_2.2.5
00000000000009b0 T main
0000000000202010 D my_initialized_global
000000000020202c B my_uninitialized_global
U printf@@GLIBC_2.2.5
U puts@@GLIBC_2.2.5
U rand@@GLIBC_2.2.5
0000000000000870 t register_tm_clones
U srand@@GLIBC_2.2.5
U __stack_chk_fail@@GLIBC_2.4
0000000000000800 T _start
0000000000202020 B stderr@@GLIBC_2.2.5
U time@@GLIBC_2.2.5
0000000000202018 D __TMC_END__
000000000000090a T usage
ghidrabook#

Теперь некоторым символам (например, main) сопоставлены
виртуальные адреса. Как результат процесса компоновки появились новые символы (__libc_csu_init), для символов некоторых изменился тип (например, my_unitialized_global), тогда
как остальные остались неопределенными, поскольку продолОбратная разработка...

51

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

ldd

В процессе создания исполняемого файла должны быть разрешены ссылки на библиотечные функции. Существует два
метода разрешения таких ссылок: статическая компоновка
и динамическая компоновка. Какой из них выбрать, определяется аргументами командной строки компоновщика. Любой
исполняемый файл может быть скомпонован статически, динамически или обоими способами1.
Если запрашивается статическая компоновка, то компоновщик объединяет объектные файлы приложения с копией затребованной библиотеки и таким образом создает исполняемый
файл. Во время выполнения нет необходимости искать библиотечный код, потому что он уже содержится внутри исполняемого
файла. Статическая компоновка имеет следующие преимущест­
ва: (1) функции вызываются чуть быстрее и (2) распространение
двоичных файлов проще, потому что не делается никаких предположений о доступности библиотечного кода в системе пользователя. Но у нее есть и недостатки: (1) размер исполняемого
файла увеличивается и (2) сложнее переходить на новую версию
программы, когда изменяются библиотеки. Последнее связано
с тем, что программу нужно пересобирать всякий раз, как изменяется какая-нибудь библиотека. С точки зрения обратной разработки, статическая компоновка несколько усложняет задачу.
Анализируя статически скомпонованный файл, мы не можем
так просто ответить на вопросы типа «С какими библиотеками
скомпонован файл?» и «Какие из имеющихся функций взяты из
библиотеки?». В главе 13 обсуждаются проблемы обратной разработки статически скомпонованных файлов.
Динамическая компоновка отличается от статической тем,
что компоновщику не нужно копировать затребованные биб­
лиотеки. Вместо этого он вставляет только ссылки на эти биб­
лиотеки (часто файлы с расширением .so или .dll) в окончательный исполняемый файл, поэтому размер конечного файла
1

52

Дополнительные сведения о компоновке см. в книге John R. Levine
«Linkers and Loaders» (Morgan Kaufmann, 1999).

ГЛАВА 2

обычно оказывается гораздо меньше. Обновление библиотечного кода тоже существенно упрощается. Поскольку существует всего одна копия библиотеки, на которую ссылается много
двоичных файлов, простая замена старой версии библиотеки
новой приводит к тому, что все динамически скомпонованные
с ней файлы автоматически начинают пользоваться новой
версией. К недостаткам динамической компоновки можно
отнести более сложный процесс загрузки. Все необходимые
библиотеки должны быть найдены и загружены в память,
тогда как в случае статической компоновки нужно загрузить
только один файл, который уже содержит весь библиотечный
код. Еще один недостаток динамической компоновки – необходимость включать в дистрибутив не только собственный исполняемый файл, но и все библиотеки, от которых этот файл
зависит. Попытка выполнить программу в системе, где нет
необходимых библиотек, приведет к ошибке.
Ниже демонстрируется создание динамически и статически
скомпонованных версий программы, размер получающихся
двоичных файлов и то, как их идентифицирует утилита file:
ghidrabook# gcc -o ch2_example_dynamic ch2_example.c
ghidrabook# gcc -o ch2_example_static ch2_example.c -static
ghidrabook# ls -l ch2_example_*
-rwxrwxr-x 1 ghidrabook ghidrabook 12944 Nov 7 10:07 ch2_example_dynamic
-rwxrwxr-x 1 ghidrabook ghidrabook 963504 Nov 7 10:07 ch2_example_static
ghidrabook# file ch2_example_*
ch2_example_dynamic: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0,
BuildID[sha1]=e56ed40012accb3734bde7f8bca3cc2c368455c3, not stripped
ch2_example_static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux),
statically linked, for GNU/Linux 3.2.0,
BuildID[sha1]=430996c6db103e4fe76aea7d578e636712b2b4b0, not stripped
ghidrabook#

Для правильной работы динамически скомпонованный
файл должен содержать информацию о том, от каких библиотек он зависит, и о том, что нужно взять из каждой библиотеки. Поэтому, в отличие от статически скомпонованных файлов,
определить, от каких библиотек зависит динамически скомпонованный файл, очень просто. Утилита ldd (list dynamic
dependencies – перечислить динамические зависимости) выводит список динамических библиотек, необходимых данному
Обратная разработка...

53

исполняемому файлу. В примере ниже ldd используется, чтобы
определить, от каких библиотек зависит веб-сервер Apache:
ghidrabook# ldd /usr/sbin/apache2
linux-vdso.so.1 => (0x00007fffc1c8d000)
libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3(0x00007fbeb7410000)
libaprutil-1.so.0 => /usr/lib/x86_64-linux-gnu/libaprutil-1.so.0 (0x00007fbeb71e0000)
libapr-1.so.0 => /usr/lib/x86_64-linux-gnu/libapr-1.so.0 (0x00007fbeb6fa0000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fbeb6d70000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fbeb69a0000)
libcrypt.so.1 => /lib/x86_64-linux-gnu/libcrypt.so.1 (0x00007fbeb6760000)
libexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007fbeb6520000)
libuuid.so.1 => /lib/x86_64-linux-gnu/libuuid.so.1 (0x00007fbeb6310000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fbeb6100000)
/lib64/ld-linux-x86-64.so.2 (0x00007fbeb7a00000)
ghidrabook#

Утилита ldd входит в состав систем Linux и BSD. В системах
macOS аналогичную функциональность предлагает утилита
otool с флагом -L: otool -L filename. В Windows для вывода
списка зависимых библиотек служит утилита dumpbin, входящая в состав Visual Studio: dumpbin /dependents filename.

Остерегайтесь своих инструментов!
Может показаться, что ldd – простая программа, но страница
руководства для нее предупреждает: «никогда не следует запус­
кать ldd для исполняемого файла, полученного из ненадежного
источника, потому что это может привести к выполнению произвольного кода». В большинстве случаев это маловероятно, но
служит напоминанием, что запуск даже кажущихся простыми
инструментов обратного конструирования может иметь нежелательные последствия при исследовании не заслуживающих доверия входных файлов. Мы надеемся, никого не нужно убеждать
в том, что выполнение ненадежных двоичных программ вряд ли
безопасно, но призываем принимать меры предосторожности
даже при статическом анализе таких программ и предполагать,
что компьютер, на котором производится обратное конструирование, а также все данные на нем и на других соединенных с ним
компьютерах могут быть в результате скомпрометированы.

54

ГЛАВА 2

objdump
Если ldd – специализированная программа, то objdump – исключительно гибкий инструмент. Цель objdump – «вывести информацию, хранящуюся в объектных файлах»1. Это довольно
широко поставленная задача, и для ее решения objdump принимает более 30 аргументов командной строки, указывающих, что
именно нужно извлечь из объектных файлов. Утилита objdump
умеет показывать следующие данные (и еще много чего).
Заголовки секций. Сводная информация о каждой секции программного файла.
Частные заголовки. Информация о размещении программы в памяти и другие сведения, необходимые загрузчику, в т. ч. такой же список требуемых библиотек, какой
выводит ldd.
Отладочная информация. Вся отладочная информация, включенная в файл.
Информация о символах. Таблица символов в таком же
формате, как ее выводит утилита nm.
Листинг дизассемблера. Программа objdump применяет алгоритм линейной развертки для дизассемблирования
секций файла, помеченных как код. В процессе дизассемб­
лирования кода x86 objdump может генерировать ассемб­
лерный код в синтаксисе AT&T или Intel и записывать
результат в текстовый файл. Такие текстовые файлы называются мертвыми листингами, и хотя их, безусловно,
можно использовать для обратной разработки, они крайне
неудобны для навигации, а еще труднее вносить в них согласованные изменения, не наделав ошибок.
Программа objdump входит в состав комплекта GNU binutils,
который имеется в Linux, FreeBSD и Windows (если установлены WSL или Cygwin)2. Отметим, что objdump опирается на биб­
лиотеку Binary File Descriptor (libbfd), являющуюся составной
частью binutils, которая применяется для доступа к объектным
файлам, и потому умеет разбирать все форматы файлов, под1
2

См. http://www.sourceware.org/binutils/docs/binutils/objdump.html.
См. http://www.gnu.org/software/binutils/.
Обратная разработка...

55

держиваемые libbfd (в т. ч. ELF и PE). Специально для разбора
ELF-файлов имеется также утилита readelf, которая предлагает в основном те же возможности, что objdump, а отличается
от нее прежде всего тем, что не зависит от libbfd.

otool
Утилиту otool проще всего описать как аналог objdump для
macOS, она полезна для разбора файлов в формате Mach-O.
В листинге ниже показано, как otool отображает зависимости
двоичного файла в формате Mach-O от динамических библиотек, т. е. выполняет функцию ldd:
ghidrabook# file osx_example
osx_example: Mach-O 64-bit executable x86_64
ghidrabook# otool -L osx_example
osx_example:
/usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.4.0)
/usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.0.0)

Утилиту otool можно использовать для отображения информации из заголовков файла и таблицы символов, а также для
дизассемблирования секции кода. Дополнительные сведения
см. в странице руководства.

dumpbin
Командная утилита dumpbin входит в состав Microsoft Visual
Studio. Подобно otool и objdump, dumpbin умеет отображать разного рода информацию, хранящуюся в файлах в формате Windows
PE. Ниже показано, как dumpbin выводит динамические зависимости программы note­pad примерно так же, как это делает ldd.
$ dumpbin /dependents C:\Windows\System32\notepad.exe
Microsoft (R) COFF/PE Dumper
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file notepad.exe
File Type: EXECUTABLE IMAGE

56

ГЛАВА 2

Image has the following delay load dependencies:
ADVAPI32.dll
COMDLG32.dll
PROPSYS.dll
SHELL32.dll
WINSPOOL.DRV
urlmon.dll
Image has the following dependencies:
GDI32.dll
USER32.dll
msvcrt.dll
...

Дополнительные параметры dumpbin позволяют извлекать
информацию из различных секций PE-файла: символы, имена
импортированных функций, имена экспортированных функций, дизассемблированный код. Более подробные сведения
о программе dumpbin имеются на сайте Microsoft1.

c++filt
Языки, допускающие перегрузку функций, должны предоставлять механизм различения перегруженных вариантов функции, потому что все они имеют одно и то же имя. В следующем
примере, написанном на C++, показаны прототипы нескольких перегруженных вариантов функции demo:
void
void
void
void
void
void

demo(void);
demo(int x);
demo(double x);
demo(int x, double y);
demo(double x, int y);
demo(char* str);

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

См. https://docs.microsoft.com/en-us/cpp/build/reference/dumpbin-command-line/.
Обратная разработка...

57

ния уникальных имен функций с одинаковым именем называется декорированием имени (name mangling)1. Воспользовавшись
nm для вывода символов в откомпилированной версии показанного выше кода на C++, мы можем увидеть нечто подобное (вывод профильтрован, чтобы не отвлекаться ни на что, кроме demo):
ghidrabook# g++ -o ch2_cpp_example ch2_cpp_example.cc
ghidrabook# nm ch2_cpp_example | grep demo
000000000000060b T _Z4demod
0000000000000626 T _Z4demodi
0000000000000601 T _Z4demoi
0000000000000617 T _Z4demoid
0000000000000635 T _Z4demoPc
00000000000005fa T _Z4demov

Стандарт C++ не определяет схему декорирования имен,
оставляя это на усмотрение разработчиков компиляторов. Чтобы декодировать различные варианты demo, нужен инструмент,
который понимает схему декорирования имен, принятую в нашем компиляторе (в данном случае g++). Именно в этом и состоит назначение утилиты c++filt, которая рассматривает каждое
входное слово, как будто это декорированное имя, и пытается
определить, какой компилятор его сгенерировал. Если строка
выглядит как правильно декорированное имя, то на выходе печатается его декодированный вариант. Если же c++filt не может
интерпретировать строку, то просто выводит ее без изменения.
Если пропустить результаты nm из предыдущего примера
через c++filt, то будут восстановлены декорированные имена
функций:
ghidrabook# nm ch2_cpp_example | grep demo | c++filt
000000000000060b T demo(double)
0000000000000626 T demo(double, int)
0000000000000601 T demo(int)
0000000000000617 T demo(int, double)
0000000000000635 T demo(char*)
00000000000005fa T demo()

1

58

О декорировании имен см. статью http://en.wikipedia.org/wiki/Name_
mangling.

ГЛАВА 2

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

ИНСТРУМЕНТЫ ГЛУБОКОЙ
ИНСПЕКЦИИ
До сих пор мы обсуждали инструменты, выполняющие поверхностный анализ файлов на основе минимальных знаний об их
внутренней структуре. Мы также видели инструменты, способные
извлекать специфические данные из файлов, поскольку обладают очень детальными знаниями об их структуре. В этом разделе
мы поговорим об инструментах для извлечения специфической
информации независимо от типа анализируемого файла.

strings
Иногда полезно задавать более общие вопросы о содержимом
файла, для ответа на которые необязательно во всех подробностях знать структуру. Один такой вопрос: «Есть ли в файле
какие-нибудь строки?» Разумеется, сначала нужно решить,
что же такое строка. Определим строку неформально – как
непрерывную последовательность печатных символов. Часто
это определение дополняется условием на минимальную длину и конкретную кодировку. Таким образом, можно было бы
поискать последовательности, содержащие по меньшей мере
четыре печатных символа ASCII подряд, и вывести результаты
на консоль. Поиск таких строк, как правило, никак не связан
со структурой файла. Их можно искать как в двоичном ELFфайле, так и в документе Microsoft Word.
Утилита strings предназначена специально для выделения
строк из содержимого файла, часто безотносительно к формату
последнего. Вызов strings с параметрами по умолчанию (не
менее четырех символов в 7-битовой кодировке ASCII подряд)
может дать такой результат:
Обратная разработка...

59

ghidrabook# strings ch2_example
/lib64/ld-linux-x86-64.so.2
libc.so.6
exit
srand
__isoc99_scanf
puts
time
__stack_chk_fail
printf
stderr
fwrite
__libc_start_main
GLIBC_2.7
GLIBC_2.4
GLIBC_2.2.5
_ITM_deregisterTMCloneTable
__gmon_start__
_ITM_registerTMCloneTable
usage: ch4_example [max]
A simple guessing game!
Please guess a number between 1 and %d.
Invalid input, quitting!
Congratulations, you got it in %d attempt(s)!
Sorry too low, please try again
Sorry too high, please try again
GCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0
...

Почему утилита strings изменилась?
Исторически в применении к исполняемым файлам strings по
умолчанию искала последовательности символов только в загружаемых секциях, содержащих инициализированные данные. Поэтому
strings должна была уметь разбирать двоичный файл и искать такие секции, для чего использовались библиотеки типа libbfd. Но при
разборе ненадежных двоичных файлов уязвимости в библиотеках
могли привести к выполнению произвольного кода1. В итоге поведение strings по умолчанию было изменено, и она стала просматривать весь двоичный файл, а не только секции с инициализированными данными (это то же самое, что запуск с флагом -a). А прежнее
поведение эквивалентно заданию флага -d.
1

60

См. CVE-2014-8485 и https://lcamtuf.blogspot.com/2014/10/psadont-run-strings-on-untrusted-files.html.

ГЛАВА 2

К сожалению, хотя некоторые строки выглядят как вывод программы, есть и другие – похожие на имена функций и библиотек.
Так что не следует торопиться с выводами о поведении программы. Аналитики часто совершают ошибку, пытаясь что-то сказать
о поведении программы на основе распечатки strings. Не забывайте, что наличие строки в двоичном файле еще не озна­чает,
что эта строка как-то используется в процессе его работы.
В заключение сделаем несколько замечаний об использовании strings:
► по умолчанию strings ничего не говорит о том, в каком
месте файла находится строка. Задайте флаг -t, если хотите, чтобы strings печатала смещение каждой найденной строки от начала файла;
► во многих файлах используются альтернативные кодировки. Флаг -e означает, что strings должна искать широкие
символы, например в 16-разрядной кодировке 16 Юникод.

Дизассемблеры
Как уже было сказано, имеются инструменты, генерирущие
мертвые листинги дизассемблирования двоичных объектных
файлов. Файлы в форматах PE, ELF и Mach-O можно дизассемблировать программами dumpbin, objdump и otool соответственно. Но ни одна из них не умеет работать с произвольными блоками двоичных данных. Иногда можно встретить файл
в нестандартном формате, и в этом случае нужно использовать
инструменты, способные начать дизассемблирование с заданного пользователем смещения.
Два примера таких потоковых дизассемблеров для набора
команд x86: ndisasm и diStorm1. Утилита ndisasm входит в состав
NASM2. В примере ниже показано использование ndisasm для
дизассемблирования части шелл-кода, сгенерированного системой Metasploit3.

1
2
3

См. https://github.com/gdabah/distorm/.
См. http://www.nasm.us/.
См. https://metasploit.com/.
Обратная разработка...

61

ghidrabook# msfvenom -p linux/x64/shell_find_port -f raw > findport
ghidrabook# ndisasm –b 64 findport
00000000 4831FF xor rdi,rdi
00000003 4831DB xor rbx,rbx
00000006 B314 mov bl,0x14
00000008 4829DC sub rsp,rbx
0000000B 488D1424 lea rdx,[rsp]
0000000F 488D742404 lea rsi,[rsp+0x4]
00000014 6A34 push byte +0x34
00000016 58 pop rax
00000017 0F05 syscall
00000019 48FFC7 inc rdi
0000001C 66817E024A67 cmp word [rsi+0x2],0x674a
00000022 75F0 jnz 0x14
00000024 48FFCF dec rdi
00000027 6A02 push byte +0x2
00000029 5E pop rsi
0000002A 6A21 push byte +0x21
0000002C 58 pop rax
0000002D 0F05 syscall
0000002F 48FFCE dec rsi
00000032 79F6 jns 0x2a
00000034 4889F3 mov rbx,rsi
00000037 BB412F7368 mov ebx,0x68732f41
0000003C B82F62696E mov eax,0x6e69622f
00000041 48C1EB08 shr rbx,byte 0x8
00000045 48C1E320 shl rbx,byte 0x20
00000049 4809D8 or rax,rbx
0000004C 50 push rax
0000004D 4889E7 mov rdi,rsp
00000050 4831F6 xor rsi,rsi
00000053 4889F2 mov rdx,rsi
00000056 6A3B push byte +0x3b
00000058 58 pop rax
00000059 0F05 syscall
ghidrabook#

Гибкость потокового дизассемблирования часто оказывается полезной. Одна из таких ситуаций – анализ сетевых атак,
в которых сетевые пакеты могут содержать шелл-код. Для анализа поведения вредоносной полезной нагрузки в таком пакете можно воспользоваться потоковым дизассемблером. Другая ситуация – анализ образов ПЗУ, для которых недоступна
документация по размещению в памяти. В одних частях ПЗУ
62

ГЛАВА 2

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

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

3

ПЕРВОЕ ЗНАКОМСТВО
С GHIDRA

Ghidra – свободно доступный инструмент обратной разработки (software reverse engineering –
SRE) с открытым исходным кодом, разработанный Агентством национальной безопасности США
(АНБ). Платформенно независимая среда Ghidra
включает интерактивный дизассемблер и декомпилятор, а также множество других взаимосвязанных инструментов, помогающих анализировать код. Она поддерживает наборы команд
разной архитектуры, различные форматы исполняемых файлов и может работать как в автономном, так и в коллективном
режиме. Быть может, лучшей особенностью Ghidra является
возможность настраивать рабочую среду и разрабатывать плагины и скрипты SRE в собственных интересах, а затем делиться
своими находками со всем сообществом Ghidra.

ЛИЦЕНЗИОННАЯ ПОЛИТИКА GHIDRA
Ghidra распространяется бесплатно на условиях лицензии
Apache License, версия 2.0. Эта лицензия предоставляет физическим лицам широкие права по использованию Ghidra, но
накладывает некоторые ограничения. Любой человек, который скачивает, использует или редактирует Ghidra, должен
прочитать соглашение с пользователями (docs/UserAgreement.
html), а также файлы, описывающие условия лицензирования,
в каталогах GPL и licenses, и убедиться, что выполняет все
прописанные соглашения, поскольку сторонние компоненты,
входящие в Ghidra, имеют свои лицензии. На случай, если вы
забыли о чем-то, упомянутом в этом абзаце, Ghidra любезно
отображает сведения о лицензировании при каждом запуске
или выборе пункта About Ghidra из меню Help.

ВЕРСИИ GHIDRA
Ghidra доступна для Windows, Linux и macOS. Хотя Ghidra
конфигурируется в широких пределах, большинство новых
пользователей, скорее всего, скачают последнюю версию
Ghidra Core, которая включает традиционную функциональность обратной разработки. В этой книге описывается в основном функционал Ghidra Core для индивидуальных проектов.
Кроме того, мы уделим некоторое время обсуждению режимов совместной работы и работы в необслуживаемом режиме, а также конфигураций Developer (Для разработчиков),
Function ID (Идентификатор функции) и Experimental (Экспериментальная).

РЕСУРСЫ ПОДДЕРЖКИ GHIDRA
Работа с новой программой часто пугает, особенно когда
требуется решить трудную реальную проблему с помощью
обратной разработки. Как пользователю (или потенциальному разработчику) Ghidra вам наверняка интересно, куда
обратиться за помощью, если возникнут вопросы. Если мы
хорошо справились со своей работой, то во многих ситуациях
66

ГЛАВА 3

этой книги будет достаточно. Но на случай, если вам понадобится дополнительная помощь, подскажем, какие имеются ресурсы.
Официальная документация. Ghidra содержит подробную справочную систему, в которую можно попасть из
меню или нажатием клавиши F1. Система предлагает
иерар­хическое меню, а также средства поиска. Но в настоящее время не поддерживаются запросы в свободной форме типа «Как сделать x?».
Файлы readme. Иногда меню Help отсылает к дополнительному материалу по теме, например файлу
readme. В документацию включено много таких файлов,
которые сопровождают плагины, дополняют материал, представленный в меню Help (например, support /
analyzeHeadlessREADME.html), помогают выполнять установку (docs /InstallationGuide.html) и служат подспорьем
разработчику (например, Extensions/Eclipse/GhidraDev/
GhidraDev_README.html), если вы решите пойти по этому пути (и, чем черт не шутит, написать поддержку вопросов в свободной форме).
Сайт Ghidra. На домашней странице проекта Ghidra
(https://www.ghidra-sre.org/) имеются предложения для
потенциальных пользователей, текущих пользователей,
разработчиков и соавторов, помогающие каждой категории расширить свои знания о Ghidra. Помимо подробной
информации о скачивании каждого выпуска Ghidra, выложено полезное видео, описывающее процедуру установки по шагам.
Каталог docs. В состав установки Ghidra входит каталог, содержащий полезную документацию, в т. ч. допус­
кающее распечатку руководство по меню и горячим клавишам (docs/CheatSheet.html), которое здорово поможет
при первом знакомстве с Ghidra, а также много другое.
В каталоге docs/GhidraClass вы найдете пособия для
пользователей Ghidra начального, среднего и продвинутого уровней.

Первое знакомство с Ghidra

67

СКАЧИВАНИЕ GHIDRA
Для получения копии Ghidra нужно сделать три простых шага.
1. Перейдите на сайт https://ghidra-sre.org/.
2. Нажмите большую красную кнопку Download Ghidra.
3. Сохраните файл на своем компьютере.
Как часто бывает с простыми трехшаговыми процессами,
есть пара мест, где упрямый ослушник может предпочесть
немного отклониться от рекомендованного пути. Следующие
пункты адресованы тем из вас, кто хочет чего-нибудь, отличного от традиционного меню.
► Если вы хотите установить другую версию, просто нажмите кнопку Releases – и вам будет предложено скачать
другие выпускные версии. В каких-то моментах функцио­
нальность может отличаться, но в основе своей это все та
же Ghidra.
► Если вы хотите установить сервер для коллективной работы, подождите до главы 11, где описано, как внести это
важное изменение в процедуру установки (или на свой
страх и риск попробуйте воспользоваться информацией
в каталоге server). В худшем случае будет несложно откатиться назад, начать с первого шага и установить локальный экземпляр Ghidra.
► Сильные духом могут попробовать собрать Ghidra из исходников. Исходный код Ghidra имеется на GitHub по
адресу https://github.com/National SecurityAgency/ghidra/.
А мы продолжим рассказ о традиционном процессе установки.

УСТАНОВКА GHIDRA
Итак, что произошло, когда вы нажали волшебную красную
кнопку и выбрали место на своем компьютере? Если все пройдет по плану, то в выбранном каталоге должен появиться
zip-файл. Для самой первой версии Ghidra файл назывался
ghidra_9.0_PUBLIC_20190228.zip.
Zip-файл представляет собой архив, содержащий больше
3400 файлов, составляющих каркас Ghidra. Если вас устраива68

ГЛАВА 3

ет место, куда был сохранен файл, распакуйте его (например,
в Windows щелкните по нему правой кнопкой мыши и выберите из контекстного меню пункт Извлечь все), и вы получите доступ к иерархии каталогов Ghidra. Отметим, что Ghidra
должна откомпилировать кое-какие внутренние файлы данных, поэтому пользователю понадобится доступ к подкаталогам с правом записи.

Структура каталогов Ghidra
Близкое знакомство с содержимым установочного каталога
Ghidra необязательно – начать работать можно и так. Но уж
коль скоро мы заговорили о распаковке дистрибутива, кинем
взгляд на структуру каталогов. Эти знания пригодятся позже,
когда мы перейдем к продвинутым средствам Ghidra. Ниже
приведено краткое описание всех подкаталогов, а на рис. 3.1
показан их перечень.

Рис. 3.1. Структура каталогов Ghidra
docs. Содержит общую документацию по Ghidra и работе
с ней. Внутри этого каталога есть два достойных упоминания подкаталога. Во-первых, в подкаталоге GhidraClass
находятся дополнительные учебные материалы, которые
помогут вам больше узнать о Ghidra. Во-вторых, в подкаталоге languages описывается язык спецификации процессоров SLEIGH. Мы будем подробно обсуждать его в главе 18.
Первое знакомство с Ghidra

69

Extensions. Содержит полезные уже собранные расширения, а также данные и информацию, важную для написания расширений Ghidra. Этот каталог подробно рассмат­
ривается в главах 15, 17 и 18.
Ghidra. Содержит код Ghidra. О том, что здесь находится,
мы подробно расскажем, когда начнем настраивать Ghidra
в главе 12 и расширять ее возможности в главах 13–18.
GPL. Некоторые компоненты были разработаны не командой Ghidra, но включают код, распространяемый по
лицензии GNU General Public License (GPL). В каталоге
GPL находятся относящиеся к таким компонентам файлы,
а также информация о лицензии.
licenses. Содержит файлы, описывающие юридические
условия использования сторонних компонент Ghidra.
server. Поддержка установки сервера Ghidra, необходимо
для коллективной работы над SRE. Этот каталог подробно
обсуждается в главе 11.
support. Сборная солянка – различные специальные возможности и средства Ghidra. В качестве бонуса прилагается значок Ghidra (ghidra.ico), если вы захотите продолжить
настройку своей среды (например, создать ярлык, указывающий на скрипт запуска Ghidra). Этот каталог будет обсуждаться по мере необходимости в разных частях книги.

Запуск Ghidra
Наряду с каталогами, в корневом каталоге есть файлы, которые помогут начать путешествие в мир Ghidra и SRE. Здесь
имеется еще один файл лицензии (LICENSE.txt), но главное –
скрипты запуска Ghidra. После первого двойного щелчка по
файлу ghidraRun.bat (или его эквивалента ghidraRun в Linux
или macOS) нужно будет принять лицензионное соглашение
с конечным пользователем (EULA), показанное на рис. 3.2,
подтвердив тем самым, что планируемое использование Ghidra
не противоречит соглашению с пользователем (Ghidra User
Agreement). При последующих запусках это окно уже не будет
показываться, но его всегда можно вызвать из меню Help.
70

ГЛАВА 3

Кроме того, может быть задан вопрос о пути к установке Java.
(Если на вашем компьютере Java не установлен, см. «Руководство по установке» в подкаталоге docs, где имеется необходимая
документация в разделе «Java Notes».) Для Ghidra необходима
версия Java Development Kit (JDK) 11 или старше.

Рис. 3.2. Соглашение с пользователем Ghidra

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

Часть II

Основы
использования
Ghidra

4

НАЧАЛО РАБОТЫ С GHIDRA

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

ЗАПУСК GHIDRA
При каждом запуске Ghidra вы в течение короткого времени будете видеть заставку, содержащую логотип Ghidra, информацию о сборке, номера версий Ghidra и Java и сведения
о лицензии. Если вам захочется внимательно прочитать, что
написано на заставке, и, в частности, больше узнать об используемых версиях, то ее можно будет в любой момент вывести
на экран, выбрав пункт HelpAbout Ghidra в окне проекта

Ghidra. Когда заставка исчезнет, на экране появится окно проекта Ghidra, а поверх него диалоговое окно Tip of the Day (Совет дня), как показано на рис. 4.1. Можете полистать советы,
нажимая кнопку Next Tip (Следующий совет). Когда будете
готовы начать работу, закройте диалоговое окно.
Если вы не хотите видеть советы, сбросьте флажок Show
Tips on Startup? (Показывать советы при запуске?) в нижней
части диалогового окна. Если вы соскучитесь по советам, это
окно легко восстановить, воспользовавшись меню Help.

Рис. 4.1. Запуск Ghidra

76

ГЛАВА 4

Если вы закроете диалоговое окно Tip of the Day или сбросите флажок, а потом перезапустите Ghidra, то появится окно
проекта Ghidra. В Ghidra проекты служат для организации
и управления инструментами и данными, связанными с файлом или группой файлов, с которыми вы работаете. Для начала
мы будем рассматривать один файл в составе неразделяемого
проекта. Более сложные средства обсуждаются в главе 11.

СОЗДАНИЕ НОВОГО ПРОЕКТА
При первом запуске Ghidra нужно будет создать проект.
Если вы уже запускали Ghidra ранее, то активным будет проект, с которым вы работали в последний раз. Команда меню
FileNew Project (ФайлСоздать проект) позволяет задать
характеристики среды, ассоциированной с проектом. Первый
шаг создания нового проекта – выбор между разделяемым
и неразделяемым проектами. В этой главе рассматриваются
только неразделяемые проекты. Затем откроется диалоговое
окно, показанное на рис. 4.2. Для неразделяемого проекта
нужно задать каталог и имя проекта.

Рис. 4.2. Создание проекта Ghidra
Заполнив поля с информацией о месте нахождения проекта,
нажмите кнопку Finish, чтобы завершить создание проекта.
Вы вернетесь в окно проекта, где будет присутствовать только
что созданный проект, как показано на рис. 4.3.

Начало работы с Ghidra

77

Рис. 4.3. Окно проекта Ghidra

Загрузка файла в Ghidra
Чтобы можно было сделать что-то полезное, нужно добавить
в проект хотя бы один файл. Вы можете открыть файл, либо
выбрав команду FileImport File (ФайлИмпорт файла)
и отыскав в системе нужный файл, либо перетащив файл прямо в папку в окне проекта. После того как файл выбран, появится диалоговое окно импорта, показанное на рис. 4.4.
Ghidra строит список потенциальных типов файлов и предлагает его в раскрывающемся списке Format в первой строке
диалогового окна. Нажав кнопку со значком i справа от поля
Format, вы получите перечень поддерживаемых форматов,
которые описаны в главе 17. В списке Format представлено подмножество загрузчиков Ghidra, подходящих для работы с выбранным файлом. В этом примере список содержит
два элемента: Portable Executable (PE) и Raw Binary. Формат
Raw Binary присутствует всегда, потому что он подразумевается по умолчанию в случае, когда Ghidra не распознает тип
файла; это самый низкий уровень, пригодный для загрузки
любого файла. Если в списке имеется несколько загрузчиков,
78

ГЛАВА 4

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

Рис. 4.4. Диалоговое окно импорта
В поле Language (Язык) можно указать, какой процессорный
модуль следует использовать для дизассемблирования. Специ­
фикация языка и компилятора может включать тип процессора, порядок байтов (LE/BE – прямой/обратный)), разрядность
(16/32/64), вариант процессора и идентификатор компилятора
(например, ARM:LE:32:v7:default). Подробнее смотрите во врезке «Спецификации языка и компилятора» в главе 13, а также
в разделе «Файлы определения языка» главы 17. В большинстве
случаев Ghidra выбирает правильный процессор, опираясь на
информацию, прочитанную из заголовков исполняемого файла.
В поле Destination Folder (Конечная папка) задается папка
проекта, в которой будет отображаться вновь импортированный
файл. По умолчанию он отображается в папке верхнего уровня,
но при желании можно добавить подпапки, чтобы организовать
импортированные программы внутри проекта. Кнопки с многоточием справа от полей Language и Destination Folder позволяют посмотреть дополнительные варианты. Можете также
изменить текст в поле Program Name (Имя программы). Пусть
вас не смущает изменение терминологии: Program Name – это
имя, которое Ghidra использует для ссылки на импортированНачало работы с Ghidra

79

ный двоичный файл внутри проекта, в т. ч. для отображения
в заголовке окна проекта. По умолчанию оно совпадает с именем импортированного файла, но может быть изменено на чтонибудь более содержательное, например «Вредонос от Starship
Enterprise».
Помимо четырех полей, показанных на рис. 4.4, можно задать другие параметры процесса загрузки – для этого предназначена кнопка Options (Дополнительно). Параметры зависят от выбранного формата и процессора. Для PE-файла
ch4_example.exe для процессора x86 параметры показаны на
рис. 4.5, и предлагаемые по умолчанию уже выбраны. Вообще
говоря, продолжить с параметрами по умолчанию – разумный
подход, но с обретением опыта вы, возможно, захотите что-то
изменить. Например, флажок Load External Libraries (Загрузить внешние библиотеки) позволяет импортировать в проект библиотеки, от которых зависит файл.

Рис. 4.5. Параметры загрузки PE-файла
Параметры импорта нужны для точного контроля над процессом загрузки файла. Не все параметры применимы ко всем
типам файлов, и в большинстве случаев можно согласиться
с предложенными по умолчанию. Дополнительные сведения
о параметрах имеются в справке по Ghidra Help. А о процессе
импорта и загрузчиках будет подробно рассказано в главе 17.
Задав параметры загрузки и нажав кнопку OK для закрытия всех диалоговых окон, вы увидите окно Import Results
80

ГЛАВА 4

Summary (Результаты импорта), показанное на рис. 4.6. Здесь
можно узнать о выбранных параметрах импорта, а также увидеть основную информацию, которую загрузчик извлек из
выбранного файла. В разделе «Импорт файлов» главы 13 мы
обсудим, как можно модифицировать некоторые результаты
импорта, перед тем как приступать к анализу, если вы располагаете дополнительной информацией, не отраженной в окне
Import Results Summary.

Рис. 4.6. Окно результатов импорта
Начало работы с Ghidra

81

Использование простого двоичного
загрузчика
Иногда Raw Binary (Простой двоичный) – единственный элемент в списке Format. Так Ghidra сообщает, что ни один загрузчик не распознал выбранный файл. Простой двоичный
загрузчик может пригодиться, например, для анализа нестандартных прошивок и полезных нагрузок эксплойтов, извлеченных из сетевых пакетов или журнальных файлов. В таких
случаях Ghidra не может получить из заголовка файла информацию, подсказывающую, как его загружать, поэтому вам придется вмешаться и самостоятельно сделать то, что загрузчик
делает автоматически, например указать процессор, разрядность и в некоторых случаях конкретный компилятор.
Допустим, если вы знаете, что двоичный файл содержит код
для процессора x86, то в диалоговом окне Language, показанном на рис. 4.7, предлагается много вариантов. Зачастую требуется исследование или даже подбор методом проб и ошибок,
чтобы сузить диапазон возможностей и выбрать вариант, пригодный для данного файла. Полезна будет любая информация
об устройстве, для которого предназначался файл. Если вы
уверены, что файл не предназначен для Windows, то попробуйте выбрать в качестве компилятора gcc или default (если
присутствует в списке).
Если двоичный файл не содержит в заголовке информацию, с которой Ghidra может работать, то Ghidra не распознает
и способ размещения файла в памяти. Если вы знаете базовый
адрес, смещение от начала файла или длину файла, то можете
ввести эти значения в соответствующие поля окна параметров
на рис. 4.8 или продолжить загрузку, не вводя дополнительную
информацию. (Эту информацию можно ввести или скорректировать в любой момент до или после начала анализа в окне
Memory Map, которое обсуждается в разделе «Окно карты памяти» главы 5.)

82

ГЛАВА 4

Рис. 4.7. Задание языка и компилятора

Рис. 4.8. Параметры простого двоичного загрузчика

Начало работы с Ghidra

83

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

АНАЛИЗ ФАЙЛОВ В GHIDRA
По сути своей Ghidra – это приложение базы данных, управляемое библиотекой плагинов, каждый из которых обладает своей функциональностью. Все данные проекта хранятся
в специализированной базе данных, которая растет и развивается по мере того, как пользователь добавляет информацию.
Различные окна, предоставляемые Ghidra, – это просто представления базы данных, показывающие информацию в виде,
полезном инженеру, занимающемуся обратной разработкой.
Все модификации, произведенные пользователем, отражаются в представлениях и сохраняются в базе данных, но никоим образом не влияют на исходный исполняемый файл. Мощь
Ghidra кроется в ее инструментах для анализа и манипулирования данными, хранящимися в базе.
Браузер кода интегрирует многие инструменты, имеющиеся
в Ghidra, и позволяет организовывать окна, добавлять и удалять инструменты, реорганизовывать содержимое и документировать весь процесс. По умолчанию браузер кода открывает
окна дерева программы, дерева символов, диспетчера типов
данных, листинга, декомпилятора и консоли. Эти и другие
окна описываются в главе 5.
Только что описанный процесс можно использовать для создания проекта и загрузки в них файлов, но к настоящей работе – анализу файлов – мы еще не приступали. Двойной щелчок
по файлу в окне проекта открывает окно браузера кода, показанное на рис. 4.9. Если вы выбрали импортированный файл
в первый раз, то Ghidra предложит проанализировать файл
автоматически. Пример автоматического анализа с помощью
диалогового окна Analysis Options (Параметры анализа) показан на рис. 4.10. В большинстве случаев, когда двоичный
файл взят с широко распространенной платформы и собран одним из широко используемых компиляторов, автоматический
анализ, вероятно, является правильным выбором, по крайней
мере в начале работы. Процесс анализа в любой момент мож84

ГЛАВА 4

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

Рис. 4.9. Окно браузера кода

Рис. 4.10. Диалоговое окно параметров анализа

Начало работы с Ghidra

85

Помните, что если вы недовольны результатами автоматического анализа, то всегда можете отбросить их, закрыв браузер
кода и отказавшись от сохранения изменений, после чего нужно
будет заново открыть файл и попробовать другую комбинацию
параметров. Типичные причины такого развития ситуации –
необычно структурированные файлы, например обфусцированные, или двоичные файлы, собранные неизвестным Ghidra
компилятором или для неизвестной операционной системы.
Заметим, что при открытии очень большого двоичного файла
(10 МБ и выше) Ghidra может потребоваться от нескольких минут до нескольких часов для завершения автоматического анализа. В таких случаях имеет смысл отключить некоторые особенно требовательные анализаторы (например, Decompiler Switch
Analysis, Decompiler Parameter ID и Stack) или задать для них
тайм-аут. Как показано на рис. 4.10, при выборе анализатора отображается его описание, где могут присутствовать предупреж­
дения о том, сколько времени может занять анализ. Кроме того,
будет показан раздел Options, позволяющий управлять некоторыми аспектами отдельных анализаторов. Любой анализ, который был отключен или прерван по тайм-ауту, можно запустить
позже, задав параметры с помощью меню Analysis.

Предупреждения автоматического
анализа
Начав анализ файла, загрузчик может столкнуться с проблемами, о которых считает нужным сообщить. Например, такое случается, когда PE-файл собран без ассоциированного файла базы
данных программы (PDB). В подобных случаях по завершении
анализа вы увидите диалоговое окно Auto Analysis Summary
(Итоги автоматического анализа) с сообщением обо всех встретившихся проблемах (рис. 4.11).

Рис. 4.11. Диалоговое окно итогов автоматического
анализа
86

ГЛАВА 4

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

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

Рис. 4.12. Представление важной сводной информации из меню
HelpAbout ch4_example.exe
Начало работы с Ghidra

87

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

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

Идентификация аргументов функции
и локальных переменных
Для каждой идентифицированной функции (это делается на
основе таблицы символов и адресов в командах вызова) Ghidra
выполняет детальный анализ поведения регистра указателя
стека, чтобы, во-первых, распознать доступ к переменным в стеке, а во-вторых, понять структуру кадра стека данной функции. Имена таких переменных генерируются автоматически
с учетом их использования: в качестве локальных переменных
или размещенных в стеке аргументов, переданных функции
при вызове. Кадры стека рассматриваются в главе 6.

Информация о типах данных
Ghidra использует знание о хорошо известных библиотечных
функциях и их параметрах для идентификации функций, ти88

ГЛАВА 4

пов данных и структур данных, используемых внутри каждой
функции. Эта информация добавляется в окна дерева символов, диспетчера типов данных и листинга. Данный процесс
экономит вам кучу времени, сообщая информацию, которую
иначе пришлось бы собирать вручную из различных справочников по интерфейсам прикладного программирования (API).
О том, как Ghidra обращается с библиотечными функциями
и их типами данных, мы подробно поговорим в главе 8.

ПОВЕДЕНИЕ РАБОЧЕГО СТОЛА
ВО ВРЕМЯ НАЧАЛЬНОГО АНАЛИЗА
В процессе начального анализа вновь открытого файла на рабочем столе в браузере кода происходит много интересного.
Понять смысл происходящего можно, наблюдая за обновлениями в правом нижнем углу окна браузера. Здесь же сообщается о том, какая часть анализа уже выполнена. Если вы
не умеете быстро читать, то можете открыть соответствующий журнал Ghidra и следить за ее действиями в более комфортном темпе. Для открытия журнала выберите команду
HelpShow Log в окне проекта. (Заметим, что пункт меню
Show Log присутствует только в меню ProjectHelp, в меню
CodeBrowserHelp его нет.)
Ниже приведена выдержка из журнала, сгенерированного Ghidra в процессе автоматического анализа файла ch4_
example.exe, она дает представление о формируемых сообщениях. Мы видим повествование о процессе анализа – всю
последовательность операций Ghidra и время, затраченное на
каждую операцию.
2019-09-23 15:38:26 INFO (AutoAnalysisManager) ------------------------ASCII Strings 0.016 secs
Apply Data Archives 1.105 secs
Call Convention Identification 0.018 secs
Call-Fixup Installer 0.000 secs
Create Address Tables 0.012 secs
Create Function 0.000 secs
Data Reference 0.014 secs
Decompiler Parameter ID 2.866 secs
Начало работы с Ghidra

89

Decompiler Switch Analysis 2.693 secs
Demangler 0.004 secs
Disassemble Entry Points 0.016 secs
Embedded Media 0.031 secs
External Entry References 0.000 secs
Function ID 0.312 secs
Function Start Search 0.051 secs
Function Start Search After Code 0.006 secs
Function Start Search After Data 0.005 secs
Non-Returning Functions - Discovered 0.062 secs
Non-Returning Functions - Known 0.000 secs
PDB 0.000 secs
Reference 0.025 secs
Scalar Operand References 0.074 secs
Shared Return Calls 0.000 secs
Stack 0.063 secs
Subroutine References 0.016 secs
Windows x86 PE Exception Handling 0.000 secs
Windows x86 PE RTTI Analyzer 0.000 secs
WindowsResourceReference 0.100 secs
X86 Function Callee Purge 0.001 secs
x86 Constant Reference Analyzer 0.509 secs
----------------------------------------------------Total Time 7 secs
----------------------------------------------------2019-09-23 15:38:26 DEBUG (ToolTaskManager) task finish (8.128 secs)
2019-09-23 15:38:26 DEBUG (ToolTaskManager) Queue - Auto Analysis
2019-09-23 15:38:26 DEBUG (ToolTaskManager) (0.0 secs)
2019-09-23 15:38:26 DEBUG (ToolTaskManager) task Complete (8.253 secs)

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

Сохранение работы и выход
Когда захотите отвлечься от анализа, хорошо бы сохранить
плоды своих трудов. В окне браузера кода это можно сделать
любым из следующих способов:
► воспользоваться любой командой Save (Сохранить) в меню
File;
► щелкнуть по значку Save на панели инструментов;
► закрыть окно браузера кода;
90

ГЛАВА 4

► сохранить проект в окне Ghidra;
► выйти из Ghidra с помощью команды в меню File.
В любом случае будет предложено сохранить все модифицированные файлы. Подробнее изменение внешнего вида
и функциональность браузера кода и других инструментов
Ghidra обсуждаются в главе 12.

СОВЕТЫ ПО ОРГАНИЗАЦИИ
РАБОЧЕГО СТОЛА GHIDRA
Ghidra отображает огромный объем информации, так что
ее рабочий стол со временем загромождается. Ниже приведено несколько советов, как лучше пользоваться своим
рабочим столом.
► Чем больше места на экране отведено Ghidra, тем удобнее
с ней работать. Это может служить оправданием для покупки гигантского монитора (или сразу четырех)!
► Не забывайте использовать меню Window (Окно) в браузере кода для открытия новых представлений или восстановления случайно закрытых окон данных. Многие окна
можно также открыть с помощью значков на панели инструментов браузера кода.
► Вновь открытое окно может заслонить какое-то из сущест­
вующих. В таком случае поищите вкладки в нижнем или
верхнем окне, позволяющие переключаться между ними.
► Можно закрыть любое окно, азатем снова открыть, когда оно понадобится, и перетащить в другое место рабочего стола.
► Внешним видом окон можно управлять с помощью параметров отображения в окне, открываемом командой
EditTool Options (Редактирование Параметры инструментов).
Эти советы – не более чем верхушка айсберга, но и они пригодятся, когда вы начнете разбираться в рабочем столе браузера
кода. Дополнительные рекомендации и хитрости, включая горячие клавиши и панель инструментов, обсуждаются в главе 5.
Начало работы с Ghidra

91

РЕЗЮМЕ
Близкое знакомство с рабочим столом браузера кода резко повысит комфорт работы с Ghidra. Обратная разработка двоичного кода – достаточно сложное дело и без того, чтобы сражаться с инструментами. Задание параметров на этапе начальной
загрузки и ассоциированного с ним анализа готовит почву
для всех последующих видов анализа. Сейчас вы, наверное,
довольны работой, выполненной за вас Ghidra, и для простых
двоичных файлов ничего больше, возможно, и не потребуется.
С другой стороны, если вы хотите знать, как дополнительно
контролировать процесс обратной разработки, то самое время
углубиться в изучение возможностей многочисленных окон
данных Ghidra. В последующих главах будут представлены
все основные окна и условия, при которых они могут быть полезны. Мы также расскажем, как овладеть всеми тонкостями
инструментов и окон, чтобы оптимизировать свой технологический процесс.

5

ОТОБРАЖЕНИЕ ДАННЫХ
В  GHIDRA

Сейчас вы уже знаете, как создать проект, загрузить в него двоичные файлы и запустить автоматический анализ. После того как Ghidra завершит
этап начального анализа, настанет ваша очередь.
Как было сказано в главе 4, сразу после запуска
Ghidra вы оказываетесь в окне проекта. После открытия файла
в одном из проектов открывается второе окно. Это браузер кода,
именно здесь берут начало многие действия, связанные с SRE.
Мы уже воспользовались браузером кода для автоматического
анализа файла, теперь более подробно рассмотрим его меню,
окна и основные параметры. Это поможет лучше познакомиться с возможностями Ghidra и создать среду SRE, отвечающую
вашим личным представлениям о технологическом процессе.
Начнем с главных окон Ghidra.

БРАУЗЕР КОДА
Открыть окно браузера кода можно, выполнив команду
ToolsRunToolCodeBrowser в окне проекта Ghidra.
Обычно браузер кода открывается путем выбора файла для
анализа, но сейчас мы откроем пустой экземпляр, чтобы продемонстрировать функциональность и конфигурационные параметры, не отвлекаясь на вещи, относящиеся к конкретному
файлу. См. рис. 5.1. По умолчанию в браузере кода имеется
шесть подокон. Прежде чем переходить к их рассмотрению, потратим некоторое время на знакомство с меню браузера кода
и связанную с ним функциональность.

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

ГЛАВА 5

ны с загруженным файлом. Остальные пункты и их применение
в процессе SRE будут описаны в соответствующем контексте.
File. Включает функциональность, которую мы ожидаем
от меню операций с файлами, в т. ч. Открыть/Закрыть,
Импорт/Экспорт, Сохранить и Печать. Помимо этого,
в меню есть специфические команды Ghidra, в частности
Tool options (Параметры инструментов), которые позволяют сохранить содержимое браузера кода и выполнять
операции с ним, и Parse C Source (Разобрать исходный
код на C), которая извлекает информацию из заголовочных файлов С и тем самым помогает процессу декомпиляции (см. раздел «Разбор исходного кода на C» в главе 13).
Edit. Включает одну команду, не связанную с конкретными подокнами: EditTool Options, которая открывает
окно, где задаются различные параметры многочисленных
инструментов, доступных из браузера кода. На рис. 5.2 показаны параметры, относящиеся к консоли. В правом нижнем углу всегда присутствует кнопка Restore Defaults
(Восстановить значения по умолчанию).
Analysis. Позволяет заново проанализировать двоичный
файл или избирательно выполнить отдельные этапы анализа. Базовые параметры анализа были описаны в разделе «Анализ файлов с помощью Ghidra» главы 4.
Navigation. Средства навигации по файлу. Это меню
предоставляет базовую функциональность клавиатуры,
поддерживаемую многими приложениями, и добавляет
специальные возможности навигации для двоичных файлов. Хотя меню предлагает единый метод перемещения
по файлу, накопив опыт работы с различными средствами
навигации, вы, скорее всего, будете пользоваться панелью
инструментов или горячими клавишами (указанными
справа от названия пункта меню).
Search. Предлагает средства поиска по памяти, тексту
программы, строкам, таблицам адресов, прямым ссылкам,
командным паттернам и многое другое. Базовые средства
поиска описаны в разделе «Поиск» главы 6, а более специаль­
ные – в различных примерах в последующих главах.
Отображение данных в  Ghidra

95

Рис. 5.2. Редактирование параметров консоли браузера кода
Select (Выбор). Предоставляет возможность задать часть
файла для выполнения конкретной задачи. Можно выбирать подпрограммы, функции, потоки управления или
просто выделить нужную часть файла.
Tools (Инструменты). Включает ряд интересных возможностей, позволяющих поместить на рабочий стол дополнительные ресурсы SRE. Один из самых полезных – параметр Processor Manual, который выводит руководство
по процессору, ассоциированному с текущим файлом. При
попытке открыть отсутствующее руководство будет предложен метод включения руководства, как показано на
рис. 5.3.
96

ГЛАВА 5

Рис. 5.3. Сообщение об отсутствующем руководстве по
процессору
Window. Позволяет настроить рабочую среду Ghidra под
ваш технологический процесс. Большая часть этой главы
посвящена рассмотрению окон Ghidra, открываемых по
умолчанию, а также некоторых других, которые могут оказаться полезны.
Help. Предлагает разнообразные хорошо организованные
и очень подробные сведения. Окно справки поддерживает
поиск, различные представления, переход к более детальной информации и возврат, а также печать и настройку
печати.

ОКНА БРАУЗЕРА КОДА
На рис. 5.4 показано раскрытое меню Window. По умолчанию
после запуска браузера кода открыто шесть окон: Program
Trees (Деревья программы), Symbol Tree (Дерево символов),
Data Type Manager (Диспетчер типов данных), Listing (Лис­
тинг), Console (Консоль) и Decompiler (Декомпилятор). Название каждого окна отображается в его левом верхнем углу.
Все эти окна присутствуют в меню Window, а некоторым также
соответствуют значки на панели инструментов. Так, на рис. 5.4
стрелками обозначены команда меню и значок на панели инструментов, соответствующие открытию окна декомпилятора.

Отображение данных в  Ghidra

97

Горячие клавиши, кнопки, панели,
о Боже!
Почти с каждым часто используемым действием в Ghidra ассоциирован пункт меню, горячая клавиша и кнопка на панели инструментов. А если нет, то вы можете создать их самостоятельно!
Панель инструментов Ghidra прекрасно настраивается, равно как
и назначение горячих клавиш пунктам меню. (Выберите команду CodeBrowser EditTool OptionsKey Bindings или просто
наведите мышь на пункт меню и нажмите клавишу F4.) Но если
и этого недостаточно, то Ghidra еще предлагает контекстное меню
команд в ответ на нажатие правой кнопки мыши. Хотя контекстные меню и не содержат полный список действий, допустимых
в данном месте и в данный момент, они служат хорошим напоминанием о большинстве типичных действий. Благодаря такой
гибкости вы можете выполнять действия, пользуясь теми средствами, которые вам удобнее, и при этом настраивать среду по
мере того, как узнаете больше о возможностях Ghidra.

Рис. 5.4. Окно браузера кода, в котором выделены элементы
для открытия окна декомпилятора

98

ГЛАВА 5

Теперь перейдем к шести окнам, открытым по умолчанию,
и их роли в процессе SRE.

Окна родные и чужие
Начав исследовать различные окна Ghidra, вы скоро заметите,
что по умолчанию одни окна открываются внутри рабочего стола
браузера кода, а другие плавают снаружи. Отвлечемся на минутку и поговорим об этих «родных» и «чужих» окнах в контексте
среды Ghidra.
Чужие окна плавают вне среды браузера кода и могут быть связанными или независимыми. Эти окна можно расположить рядом с браузером. Примерами могут служить окна графов функций, комментариев и карты памяти.
Кроме них, выделяются три класса «родных» окон:


окна, открытые по умолчанию в браузере кода (например,
дерево символов и листинг);



окна, образующие стопку, включающую основное окно
брау­зера кода (например, окно байтов);



окна, которые разделяют место с другими окнами браузера
кода (например, окна уравнивания и внешних программ).

Когда вы открываете окно, разделяющее место с другим открытым окном, новое окно оказывается на переднем плане внутри
существующего окна. Все окна, разделяющие одно место, являются вкладками, что позволяет быстро переходить от одного
к другому. Если вы хотите одновременно видеть два окна, разделяющих одно место, щелкните по полосе заголовка окна и отбуксируйте его за пределы окна браузера кода.
Но будьте осторожны! Вернуть окно обратно в окно браузера
не так просто, как выдвинуть его наружу (подробнее см. раздел
«Реорганизация окон» главы 12).

Отображение данных в  Ghidra

99

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

Окно листинга
Окно листинга, или, как его еще называют, окно дизассемблера, будет вашим основным инструментом при просмотре, манипулировании и анализе результатов дизассемблирования
Ghidra. В этом текстовом окне представлен весь дизассемблированный листинг программы, позволяющий просмотреть области данных в двоичном файле.
На рис. 5.5 показан вид браузера кода, в который загружен
файл ch5_example1.exe, когда конфигурация окон, предлагаемая по умолчанию, не изменялась. В левом поле окна листинга
показана важная информация о файле и о месте в файле, где
вы сейчас находитесь. Вертикальная полоса прокрутки отражает текущее положение внутри файла и тоже может использоваться для навигации. Справа от полосы прокрутки имеется
дополнительная область, в которой располагаются закладки –
дополнительное средство навигации.

100

ГЛАВА 5

Рис. 5.5. Окно браузера кода по умолчанию с загруженным
файлом ch5_example1.exe

Ваши любимые полосы
После того как автоматический анализ файла завершен, вы можете использовать информационные полосы на полях для навигации и дальнейшего анализа. По умолчанию отображается
только навигационная полоса. Можно добавить (или скрыть) полосу Overview (Обзор) и полосу Entropy (Энтропия), воспользовавшись кнопкой Toggle Overview Margin (Переключить полосу
обзора) в правом верхнем углу окна листинга (рис. 5.6). Какие бы
полосы ни были включены, навигационный маркер слева от всех
полос напоминает, в каком месте файла вы находитесь. Щелчок
левой кнопкой мыши в любой полосе вызывает переход в это место
файла и обновляет содержимое окна листинга.
Разобравшись с тем, как управлять появлением (и исчезновением)
полос, рассмотрим, что показывается в каждой полосе и как ими
можно воспользоваться в процессе обратного конструирования.
Отображение данных в  Ghidra

101

Область навигационных маркеров. Позволяет перемещаться по файлу и обладает еще одной важной функцией: если
щелкнуть правой кнопкой мыши внутри этой области, будут показаны классы маркеров и закладок, которые можно ассоциировать с данным файлом. Включая и выключая типы маркеров,
вы можете управлять тем, что отображается в полосе навигации. Это позволяет легко перемещаться по маркерам определенного типа (например, по подсвеченным выделениям).
Полоса обзора. Предоставляет важную визуальную информацию о содержимом файла. Горизонтальные пояса в полосе
обзора представляют цветокодированные области программы. Ghidra предлагает цвета по умолчанию для общеупотребительных категорий, например функций, внешних ссылок,
данных и команд, но вы и сами можете управлять цветовой
схемой с помощью меню EditTool Options. По умолчанию,
если задержать мышь над областью, будет показана детальная информация об этой области, включая ее тип и, возможно, ассоциированный с ней адрес.
Полоса энтропии. Это уникальная особенность Ghidra: классификация содержимого файла на основе его окружения. Если
изменения внутри области очень малы, ей назначается низкая
энтропия. Если же степень случайности велика, то энтропия будет большой. Задержав мышь над горизонтальным поясом в полосе энтропии, вы увидите значение энтропии (от 0.0 до 8.0),
тип (например, .text), а также ассоциированный адрес в файле.
Конфигурируемую в широких пределах полосу энтропии можно
использовать для оценки наиболее вероятного содержания полосы. Дополнительные сведения об этой возможности и стоящей за ней математике можно найти в справке по Ghidra.

На рис. 5.6 описано назначение кнопок на панели инструментов, относящихся к окну листинга. А на рис. 5.7 мы раскрыли
окно листинга на весь экран, чтобы было понятно, что в нем отображается. Результат дизассемблирования расположен линейно,
в левом столбце по умолчанию показаны виртуальные адреса.
В окне листинга есть несколько элементов, заслуживающих
внимания. В серой полосе слева размещаются маркеры. Она
служит для обозначения текущего положения в файле и включает точечные и площадные маркеры, описанные в справке по
Ghidra. В данном случае текущее положение в файле (004011b6)
обозначено черной стрелкой.
102

ГЛАВА 5

Копировать
Вставить

Эта функциональность присутствует во многих окнах Ghidra, но
результат зависит от конкретного окна и содержимого, которое
было выбрано в момент операции. Иногда копируемое
содержимое оказывается несовместимо с тем, куда вставляется,
и тогда выдается сообщение об ошибке

Переключить режим
задержки мыши

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

Форматер полей
браузера

Позволяет задать формат окна листинга (см. главу 12)

Открыть окно
разности

Позволяет сравнить два файла (см. главу 23)

Снимок

Создает и открывает несвязанную копию окна листинга

Переключить
отображение обзора
на полях

Включает или выключает отображение полос энтропии и обзора

Рис. 5.6. Кнопки на панели инструментов, относящиеся к окну
листинга
Справа от поля маркеров находится область, в которой графически изображается нелинейный поток управления внутри
функции1. Если исходный или целевой адрес команды потока
управления виден в окне листинга, то появляется ассоциированная стрелка потока. Сплошными стрелками обозначаются
безусловные переходы, а штриховыми – условные. Если задержать мышь над линией потока, то откроется всплывающая подсказка, в которой показан начальный и конечный адреса потока, а также его тип. Когда поток (условный или безусловный)
передает управление предшествующему адресу в программе,
это часто является признаком цикла. Такая ситуация показана
на рис. 5.7 стрелкой, идущей из адреса 004011cf в 004011c5. Чтобы проследовать в начало или конец любого перехода, достаточно дважды щелкнуть по соответствующей ему стрелке потока.

1

В Ghidra термином поток описывается возможное продолжение выполнения после данной команды. Нормальным (или обыкновенным) потоком
называется последовательное выполнение команд. Поток перехода означает, что текущая команда выполняет (или может выполнить) переход
в другое место программы. Поток вызова означает, что текущая команда
вызывает подпрограмму.
Отображение данных в  Ghidra

103

Объявления
Поле маркеров

Стрелки потока
Перекрестная
ссылка

Рис. 5.7. Аннотированное окно листинга
Секция объявлений в верхней части рис. 5.7 показывает,
как Ghidra оценивает структуру кадра стека функции1. Ghidra
вычисляет структуру кадра стека (локальные переменные),
производя детальный анализ использования указателя стека и указателя кадра стека внутри функции. Мы вернемся
к отобра­жению стека в главе 6.
В листингах обычно присутствуют многочисленные перекрестные ссылки на данные и код. Они обозначены словом
XREF в правой части рис. 5.7. Перекрестная ссылка создается
всякий раз, как одно место листинга ссылается на другое. Например, если команда по адресу A выполняет переход на команду по адресу B, то будет создана перекрестная ссылка из
A в B. Если задержать мышь над адресом ссылки, то появится
вплывающее окно, содержащее адрес источника ссылки. Это
окно имеет такую же структуру, как окно листинга, но на желтом фоне (таком же, как фон всплывающих подсказок). Оно
1

104

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

ГЛАВА 5

позволяет просматривать содержимое, но не дает возможности
следовать по ссылкам. Перекрестные ссылки – тема главы 9.

СОЗДАНИЕ ДОПОЛНИТЕЛЬНЫХ
ОКОН ДИЗАССЕМБЛЕРА
Если вам понадобится одновременно видеть листинги двух
функций, то нужно лишь открыть другое окно дизассемблера, щелкнув по значку снимка на панели инструментов (см.
рис. 5.6). В первом открытом окне дизассемблера имени файла предшествует префикс Listing:. А все последующие окна
озаглавлены [Listing: ], это показывает, что они
отсоединены от главного окна. Снимки отсоединяются, чтобы
перемещения внутри них не затрагивали других окон.

Конфигурирование окон листинга
В листинге дизассемблера можно выделить несколько компонентов,
включая поле мнемоники, поле адреса и поле комментария. До сих
пор мы видели листинги, содержащие набор полей по умолчанию,
которые сообщают важную информацию о файле. Но иногда представление по умолчанию не содержит нужной вам информации. Тогда на помощь приходит форматер полей браузера.
Форматер позволяет настроить более 30 полей и тем самым полностью контролировать внешний вид окон листинга. Для активации
форматера нажмите кнопку на панели инструментов (см. рис. 5.6).
В результате в верхней части окна листинга появится большое подменю и редактор структуры, показанные на рис. 5.8. Форматер полей дает возможность определить, как должны выглядеть адреса,
функции, переменные, команды, данные, структуры и массивы.
В каждой категории имеются поля, которые можно настраивать, добиваясь подходящего внешнего вида листинга. Мы в основном будем придерживаться форматов по умолчанию, но вам рекомендуем
поэкспериментировать с форматером, чтобы понять, при каких параметрах вам легче воспринимать содержимое окна.

Отображение данных в  Ghidra

105

Рис. 5.8. Окно листинга с активным форматером полей
браузера

ПРЕДСТАВЛЕНИЕ ГРАФА ФУНКЦИИ
В GHIDRA
Листинги дизассемблера, конечно, интересны и информативны, но поток управления в программе проще понять, глядя на
графическое представление. Чтобы открыть окно графа функции, выберите из меню команду WindowFunction Graph
(ОкноГраф функции) или щелкните по соответствующему
значку на панели инструментов. На рис. 5.9 показано окно
графа функции, соответствующее функции на рис. 5.7. Графические представления напоминают блок-схемы программ тем,
что функция расчленена на простые блоки, чтобы наглядно показать поток управления от одного блока к другому1.
1

106

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

ГЛАВА 5

Рис. 5.9. Графическое представление листинга на рис. 5.7
На экране для различения типов потоков между блоками
внутри функции используются стрелки разных цветов. Кроме
того, потоки анимируются при наведении на них мыши, чтобы показать направление. Простые блоки, завершающиеся командой условного перехода, порождают два возможных потока:
со стрелкой Да (проверяемое условие выполнено), по умолчанию зеленого цвета, и со стрелкой Нет (проверяемое условие
не выполнено), по умолчанию красного цвета. Простые блоки,
у которых имеется только один блок-преемник, оканчиваются
нормальной стрелкой, по умолчанию синего цвета, которая указывает на следующий блок. Щелчок по любой стрелке показывает ассоциированный с ней переход из одного блока в другой.
Поскольку граф и листинг по умолчанию синхронизированы,
текущее положение в файле будет оставаться согласованным
блока имеется единственная точка входа (первая команда блока) и единственная точка выхода (последняя команда блока). Первая команда прос­
того блока часто является конечной целью какой-то команды перехода,
а последняя зачастую сама является командой перехода.
Отображение данных в  Ghidra

107

при навигации в любом представлении. Исключения из этого
правила обсуждаются в главе 10 и в справке по Ghidra.
В режиме графа отображается только одна функция. Ghidra
реализует навигацию по графу, применяя традиционные методы взаимодействия с изображениями, в частности панорамирование и изменение масштаба. Для большой и сложной
функции граф может оказаться очень громоздким, так что по
нему будет трудно перемещаться. В этом случае поможет вид
со спутника. По умолчанию представление «вид со спутника»
находится в правом нижнем углу окна графа и бывает очень
полезно, когда нужно оценить ситуацию в целом (см. рис. 5.9).

Спутниковая навигация
На виде со спутника всегда отображается полная блочная структура графа, а подсвечен блок, соответствующий области графа,
которая сейчас просматривается в окне дизассемблера. Если
щелкнуть по любому блоку на виде со спутника, то граф будет
перерисован так, чтобы этот блок оказался в его центре. Подсвеченный блок играет роль объектива, который можно буксировать
по окну, чтобы быстро переместиться в любое место графа. Помимо средства для навигации по графу функции, это магическое
окно обладает и другими возможностями, которые иногда помогают, а иногда мешают исследованию файла.
Это окно занимает ценное место в окне графа функции и может
загораживать важные вещи именно тогда, когда вы хотите на них
взглянуть. Разрешить проблему можно двумя способами. Можно
щелкнуть по виду со спутника и сбросить флажок Dock Satellite
View (Пристыковать вид со спутника). Тогда вид со спутника со
всей его функциональностью окажется за пределами окна графа
функции. Если впоследствии снова установить флажок, то вид
со спутника вернется на свое обычное место.
Второй способ – скрыть вид со спутника, если вы не хотите пользоваться им для навигации. Для этого служит другой флажок
в контекстном меню. Если вид со спутника скрыт, то в правом
нижнем углу окна графа функции появится небольшой значок,
щелчок по которому восстанавливает вид со спутника.
Если вид со спутника присутствует на экране, то основное представление может работать медленнее, чем хотелось бы. Скрыв
этот вид, мы сделаем его более отзывчивым.

108

ГЛАВА 5

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

Помимо навигации с помощью вида со спутника, представлением в окне графа функции можно манипулировать и другими способами.
Панорамирование. Положение графа можно изменить,
щелкнув мышью в любой точке фона окна и отбуксировав
его в любую сторону.
Изменение масштаба. Граф можно отдалять и приближать традиционными способами, например: ctrl/команда, прокрутка мышью или назначенные горячие клавиши. Если уменьшить масштаб слишком сильно, то можно
перейти порог рисования, за которым содержимое блоков
не отображается. При этом блоки становятся просто цветными прямоугольниками. В некоторых случаях, особенно
когда окно графа расположено бок о бок с окном листинга,
это даже имеет преимущества, поскольку в таком режиме
граф функции перерисовывается быстрее.
Реорганизация блоков. Отдельные блоки в графе можно
перетаскивать в другие места, потянув за полосу заголовка
блока. При перемещении все связи между блоками сохраняются. Если впоследствии вы захотите вернуться к размещению графа по умолчанию, щелкните по значку Refresh
(Обновить) на панели инструментов окна графа функции.
Группировка и сворачивание блоков. Блоки можно
группировать, по отдельности или вместе с другими блоОтображение данных в  Ghidra

109

ками, и сворачивать, чтобы «разгрести» окно. Группировка
приводит к сворачиванию блока. Сворачивание – простой
способ отслеживать уже проанализированные блоки. Свернуть можно любой блок, щелкнув по значку группировки –
последнему справа на панели инструментов блока. Если
сделать это, когда выбрано несколько блоков, то будут свернуты все, и в окне появится список блоков в группе. Некоторые нюансы формирования и расформирования группы,
а также действия с группами описаны в справке по Ghidra.

Настройка отображения графа
Чтобы помочь вам в анализе, Ghidra располагает в верхней части
каждого узла графа функции полосу меню, которое позволяет
управлять отображением этого узла. Можно изменить цвет фона
или текста в узле, перейти по перекрестной ссылке, просмотреть
листинг узла в полном окне, использовать механизм группировки
для объединения и сворачивания узлов. (Заметим, что при изменении цвета фона блока в окне графа функции меняется также
цвет фона в окне листинга.) Некоторые из этих средств могут показаться лишними, если вы активно используете окно листинга
в сочетании с окном графа функции, но возможности настройки,
безусловно, стоит изучить. Мы еще вернемся к ним в главе 10.

Когда представление в виде графа открывается в окне,
внешнем по отношению к браузеру кода, оба окна можно расположить бок о бок. Поскольку между окнами имеется связь,
изменение положения в одном окне приводит к перемещению
маркера места в другом. Многие пользователи предпочитают
какой-то один способ визуализации потока управления в программе, но совершенно необязательно так ограничивать себя.
Кроме того, имейте в виду, что способы управления представлениями в виде графа и текста далеко не исчерпываются приведенными выше примерами. Дополнительные графические
возможности Ghidra рассматриваются в главе 10, а подробнее
прочитать о параметрах просмотра можно в справке.
В следующих пяти главах мы преимущественно будем иллюст­
рировать примеры с помощью листингов, привлекая графы,
если это делает изложение понятнее. В главе 6 речь пойдет об
интерпретации результата дизассемблирования, а в главе 7 мы
110

ГЛАВА 5

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

Перемещение по листингу
В дополнение к традиционным средствам навигации по файлу (на строку
вверх и вниз, на страницу вверх и вниз и т. д.) Ghidra предлагает инструменты, специфические для процесса SRE. Значки на панели инструментов навигации (рис. 5.10) позволяют легко перемещаться по программе.

Рис. 5.10. Панель инструментов навигации в браузере кода
Крайним слева является значок направления. Он может иметь вид
стрелки, направленной вниз или вверх, и определяет направление для
всех остальных значков навигации. Следующие восемь значков позволяют перебирать различные элементы, описанные на рис. 5.11.
Команда
Данные
Неопределенные
Метка
Функция
Команда вне функции
Другое значение байта
Закладка (любого типа)

Рис. 5.11. Значки на панели инструментов навигации
Значок Данные перемещает курсор не просто к следующему элементу
данных в листинге, а пропускает соседние данные и останавливается
в начале следующего, не смежного с текущим, блока данных. Значки
Команда и Неопределенные ведут себя точно так же.
Крайний справа значок стрелки вниз отображает список, позволяющий
выбрать конкретный тип закладки для быстрой навигации. Хотя преимущественно эти средства используются в окне листинга, они доступны
и во всех связанных с ним окнах. Навигация в любом из таких окон вызывает синхронную навигацию во всех связанных окнах.

Отображение данных в  Ghidra

111

ОКНО ДЕРЕВЬЕВ ПРОГРАММЫ
Вернемся к обсуждению окон браузера кода, открытых по
умолчанию, и бегло рассмотрим окно деревьев программы, показанное на рис. 5.12.

Рис. 5.12. Окно деревьев программы
В этом окне программа представлена в виде папок и фрагментов, что позволяет уточнить организацию программы
в процессе анализа. Фрагментом в Ghidra называется непрерывный диапазон адресов. Фрагменты не могут перекрываться. Традиционно вместо термина «фрагмент» чаще
употребляется термин секция программы (например, .text,
.data, .bss). К деревьям программы применимы следующие
операции:
► создать папку или фрагмент;
► раскрыть, свернуть или объединить папки;
► добавить или удалить папку или фрагмент;
► идентифицировать часть кода в окне листинга и помес­
тить ее во фрагмент;
► отсортировать по имени или адресу;
► выбрать адреса;
► копировать, вырезать, вставить фрагменты или папки;
► изменить порядок папок.
112

ГЛАВА 5

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

ОКНО ДЕРЕВА СИМВОЛОВ
Когда вы импортируете файл в проект, Ghidra выбирает модуль загрузчика, который будет загружать файл. Если файл
двоичный, то загрузчик может извлечь из таблицы символов
(см. обсуждение в главе 2) информацию, которая будет отобра­
жаться в окне дерева символов, показанном на рис. 5.13. В этом
окне представлены импортированные и экспортированные
объекты, функции, метки, классы и пространства имен, встречающиеся в программе. Все эти категории и соответствующие
им типы символов обсуждаются ниже.

Рис. 5.13. Окно дерева символов в браузере кода

Отображение данных в  Ghidra

113

Отображение всех шести папок в дереве символов можно
контролировать с помощью фильтра в нижней части окна. Эта
возможность обретает смысл, если вы что-то знаете об анализируемом файле. Кроме того, окно дерева символов предлагает функциональность, аналогичную таким командным инструментам, как objdump (с флагом -T), readelf (с флагом -s)
и dumpbin (с флагом /EXPORTS).

Импортируемые объекты
В папке Imports перечислены все функции, импортируемые
в анализируемый двоичный файл. Она представляет интерес,
только когда в двоичном файле используются разделяемые биб­
лиотеки, т. к. у статически скомпонованных файлов нет внешних зависимостей, а значит, ничего и не импортируется. В папке Imports присутствуют импортируемые библиотеки и объекты
(функции или данные), импортируемые из каждой библиотеки.
При щелчке по любому символу в дереве представления во всех
связанных окнах изменяются так, чтобы был виден выбранный
символ. В примере показан исполняемый файл Windows, щелчок по символу GetModuleHandleA в папке Imports заставит окна
дизассемблера показать запись в таблице адресов, соответствующую импортируемой функции GetModuleHandleA; на рис. 5.14
показано, что она размещена по адресу 0040e108.

Рис. 5.14. Запись в таблице импортируемых объектов
и соответствующий ей адрес в окне листинга
Важно помнить, что в папке Imports отображаются только символы, поименованные в таблице импортируемых объектов двоич114

ГЛАВА 5

ного файла. Символы, которые программа загружает по собственной инициативе, пользуясь механизмом типа dlopen/dlsym или
LoadLibrary/GetProcAddress не показаны в окне дерева символов.

Экспортируемые объекты
В папке Exports перечислены точки входа в файл. К ним относится точка входа в программу, заданная в секции заголовка, а также все функции и переменные, которые файл экспортирует для нужд других файлов. Экспортируемые функции
обычно встречаются в разделяемых библиотеках, например
в DLL-файлах в Windows. Экспортируемые объекты представлены именами, а соответствующие виртуальные адреса
подсвечиваются в окне листинга при выборе объекта. Для исполняемых файлов папка Exports всегда содержит по меньшей мере один элемент: точку входа в программу. Ghidra может называть этот символ entry или _start в зависимости от
типа двоичного файла.

Функции
Папка Functions содержит перечень всех функций, которые
Ghidra сумела найти в двоичном файле. Если задержать мышь
над именем функции в окне дерева символов, то появится
всплывающая подсказка с подробной информацией о функции
(рис. 5.15). В процессе обработки файла загрузчик применяет различные алгоритмы, например анализ структуры файла
и сопоставление последовательности байтов, чтобы определить,
каким компилятором был создан файл. На этапе анализа анализатор идентификаторов функций использует полученную
информацию о компиляторе для сравнения хешей тел функций в попытке найти библиотечные функции, скомпонованные с исполняемым файлом. Если подходящий хеш найден,
то Ghidra извлекает имя соответствующей функции из базы
данных хешей (она хранится в файлах типа .fidbf) и добавляет
это имя как символ функции. Сравнение хешей особенно полезно при анализе двоичных файлов с удаленными символами, потому что позволяет восстановить символы, не опираясь
на таблицу символов. Эта функциональность более подробно
обсуждается в разделе «Идентификаторы функций» главы 13.
Отображение данных в  Ghidra

115

Рис. 5.15. Всплывающее окно для папки функций в окне дерева
символов

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

Классы
Папка Classes содержит элементы, соответствующие классам,
найденным Ghidra на этапе анализа. Для каждого класса перечисляются данные-члены и методы, что может быть полезно для
понимания поведения класса. Классы и структуры C++, которыми
Ghidra заполняет папку Classes, подробно обсуждаются в главе 8.
116

ГЛАВА 5

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

ОКНО ДИСПЕТЧЕРА ТИПОВ ДАННЫХ
Окно диспетчера типов данных позволяет находить, организовывать и применять типы данных к файлу, используя систему архивов типов данных. Архивы представляют накопленные
Ghidra знания о предопределенных типах данных, добытые из
заголовочных файлов, входящих в состав наиболее популярных компиляторов. В процессе обработки заголовочных файлов
Ghidra узнает о типах данных, ожидаемых часто используемыми библиотечными функциями, и может соответственно аннотировать листинги дизассемблера и декомпилятора. Из тех же
заголовков Ghidra получает информацию о размерах и размещении в памяти сложных структур данных. Вся эта информация аккумулируется в архивных файлах и применяется при
каждом анализе двоичного файла.
Возвращаясь к рис. 5.4, мы видим, что корень дерева
BuiltInTypes, содержащего примитивные типы вроде int, которые нельзя изменять, переименовывать или перемещать внутри
архива типа данных, отображается в окне диспетчера типов
данных (в левом нижнем углу окна браузера кода), даже когда
никакая программа не загружена. Помимо встроенных типов,
Ghidra поддерживает создание определенных пользователем
типов данных, включая структуры, объединения, перечисления
и псевдонимы типов. Также поддерживаются массивы и указатели, которые считаются производными типами данных.
С каждым открытым файлом ассоциирована запись в окне
диспетчера типов данных, как было показано на рис. 5.5. СоотОтображение данных в  Ghidra

117

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

ОКНО КОНСОЛИ
Окно консоли, находящееся в нижней части окна браузера
кода, используется как область вывода для плагинов и скриптов, в т. ч. разработанных пользователями. Именно здесь нужно искать информацию о задачах, которые Ghidra выполняет,
когда вы работаете с файлом. Разработке плагинов и скриптов
посвящены главы 14 и 15.

ОКНО ДЕКОМПИЛЯТОРА
Окно декомпилятора позволяет одновременно просматривать
и манипулировать представлениями кода на языке ассемблера и на C с помощью связанных окон. Представление на C,
генерируемое декомпилятором Ghidra, не всегда идеально, но
оно может оказаться очень полезным для понимания двоичного файла. Базовая функциональность, предлагаемая декомпилятором, включает восстановление выражений, переменных,
параметров функций и полей структур. Также декомпилятор
во многих случаях может восстановить блочную структуру
функции, пропадающую в языке ассемблера, который не имеет блочной структуры и широко использует предложения goto
(или их эквиваленты), чтобы такую структуру смоделировать.
В окне декомпилятора отображается представление на языке C функции, выбранной в окне листинга, как показано на
рис. 5.16. Если вы не очень подкованы в языке ассемблера, то
разобраться в декомпилированном коде будет гораздо проще.
Даже начинающий программист сможет опознать бесконечный
цикл в декомпилированной функции (условие в цикле while зависит от значения param_3, которое не изменяется внутри цикла).
118

ГЛАВА 5

Рис. 5.16. Окна листинга и декомпилятора
На рис. 5.17 показаны значки в окне декомпилятора. Значок
снимка служит для открытия дополнительных (несвязанных)
окон декомпилятора на случай, если потребуется сравнить декомпилированные версии нескольких функций или продолжить просмотр интересующей функции, уйдя в другое место
в окне листинга. Значок экспорта позволяет сохранить декомпилированную функцию в C-файле.
При щелчке правой кнопкой мыши в окне декомпилятора
открывается контекстное меню, содержащее действия, которые
можно выполнить для выделенного элемента. На рис. 5.18 показаны операции, доступные для одного из параметров функции, param_1.

Отображение данных в  Ghidra

119

Повторно декомпилировать

Производит повторную декомпиляцию кода в листинге

Копировать

Копирует выбранный в окне декомпилятора текст в
буфер обмена Ghidra

Экспорт

Экспортирует декомпилированную функцию в указанный файл

Снимок

Создает и открывает копию в несвязанном окне декомпилятора

Отладить декомпиляцию функции

Запускает декомпилятор и сохраняет всю относящуюся к делу
информацию в XML-файле

Рис. 5.17. Контекстное меню параметра функции в окне
декомпилятора

Рис. 5.18. Параметры функции в окне декомпилятора
120

ГЛАВА 5

Декомпиляция – очень сложный процесс, ее теория до сих
пор является областью активных исследований. В отличие от
дизассемблирования, верность которого можно проверить по
справочным руководствам производителей, не существует никаких руководств, где описывалась бы каноническая трансляция языка ассемблера на C (или C на ассемблер, если на то пошло). Хотя декомпилятор Ghidra всегда генерирует исходный
код на C, может статься, что исходный код был написан на другом языке, и тогда многие ориентированные на C допущения
декомпилятора будут неверны.
Как и у большинства сложных плагинов, у декомпилятора
есть свои странности, и качество выхода во многом зависит от
качества входа. Многие проблемы и нерегулярности в окне
декомпилятора берут начало в проблемах дизассемблирования, так что если декомпилированный код не имеет смысла,
то имеет смысл потратить время на улучшение качества диз­
ассемблированного кода. В большинстве случаев это сводится
к аннотированию листинга более точной информацией о типах
данных; эту тему мы будем обсуждать в главах 8 и 13. В последующих главах мы продолжим изучение возможностей декомпилятора и особенно глубоко обсудим их в главе 19.

ДРУГИЕ ОКНА GHIDRA
Помимо шести окон по умолчанию, можно открыть и другие
окна, предлагающие альтернативные или специализированные представления файла. Список доступных окон отображается в меню Window, показанном на рис. 5.4. Полезность этих
представлений зависит от особенностей анализируемого двоичного файла и вашего опыта работы с Ghidra. Некоторыеиз
них настолько специальны, что требуют отдельного рассмотрения в последующих главах, но наиболее употребительные мы
опишем здесь же.

Окно байтов
В окне байтов (Bytes) содержимое файла представлено на уровне байтов. По умолчанию это окно открывается в правом верхнем углу браузера кода и содержит стандартное шестнадцатеОтображение данных в  Ghidra

121

ричное представление программы по 16 байт в строке. Окно
играет также роль шестнадцатеричного редактора, формат
отображения данных можно настроить с помощью инструмента
Settings (Настройки) на его панели инструментов. Часто бывает полезно добавить в окно байтов представление в коде ASCII,
как показано на рис. 5.19. На рисунке представлено также диалоговое окно параметров средства просмотра байтов и значки,
предназначенные для редактирования и получения снимка
байтового представления.

Рис. 5.19. Синхронизированные представления в виде листинга
дизассемблера и шестнадцатеричных байтов, а также значки
снимка и переключения в режим редактирования
Как и в случае окна листинга, можно одновременно открыть несколько окон байтов, воспользовавшись значком снимка (рис. 5.19)
на панели инструментов. По умолчанию первое окно байтов связано с окном листинга, поэтому прокрутка и щелчок по элементу
в одном окне приводят к прокрутке до того же места (того же виртуального адреса) в другом окне. Последующие окна байтов не связаны, т. е. их можно прокручивать независимо. Имя несвязанного
окна в его полосе заголовка заключено в квадратные скобки.
Чтобы превратить окно байтов в шестнадцатеричный (или
ASCII) редактор, просто щелкните по значку карандаша, по122

ГЛАВА 5

казанному на рис. 5.19. Курсор станет красным, показывая, что
можно редактировать содержимое, однако редактировать области, содержащие существующий код, например команды, запрещено. Закончив редактирование, снова щелкните по значку карандаша, и окно вернется в режим просмотра. (Отметим,
что изменения не отражаются в несвязанных окнах байтов.)
Если в столбце Hex отображаются вопросительные знаки,
а не шестнадцатеричные значения, значит, Ghidra не уверена,
какие значения находятся в данном диапазоне виртуальных
адресов. Такое бывает, когда в программе есть секция bss1, которая обычно не занимает места в файле, а создается загрузчиком, который выделяет память необходимого размера.

ОКНО ОПРЕДЕЛЕННЫХ ДАННЫХ
В окне определенных данных (Defined Data) отображается строковое представление данных, определенных в текущей программе, представлении или выбранном участке. Наряду с данными
отображаются адрес, тип и размер, как показано на рис. 5.20.
Как и в большинстве табличных окон, можно производить сортировку по любому столбцу в порядке возрастания или убывания,
для чего достаточно щелкнуть по заголовку столбца. Двойной
щелчок по любой строке в окне определенных данных вызывает
переход к адресу выбранного элемента в окне листинга.
В сочетании с перекрестными ссылками (обсуждаются в главе 9) окно определенных данных позволяет быстро находить
интересующий элемент и все места в программе, откуда имеются ссылки на него. Например, вы видите строку "SOFTWARE\
Microsoft \Windows\Current Version\Run" и хотите узнать, почему приложение ссылается на этот раздел реестра Windows;
в результате выясняется, что программа создает его, чтобы автоматически запускаться на этапе загрузки Windows.

1

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

123

Рис. 5.20. Окно определенных данных, значок Filter увеличен
Окно определенных данных располагает развитыми возможностями фильтрации. Помимо поля Filter в нижней час­
ти, имеется значок Filter в правом верхнем углу (увеличен на
рис. 5.20), который позволяет дополнительно задавать фильтр
по типу, как показано на рис. 5.21.
Когда вы закроете диалоговое окно фильтрации данных по
типу, нажав OK, Ghidra перерисует содержимое окна определенных данных в соответствии с новыми параметрами.

124

ГЛАВА 5

Рис. 5.21. Фильтрация определенных данных по типу

ОКНО ОПРЕДЕЛЕННЫХ СТРОК
В окне определенных строк (Defined Strings) отображаются
строки, определенные в двоичном файле. Пример приведен на
рис. 5.22. Помимо столбцов, отображаемых по умолчанию, можно добавить дополнительные, щелкнув правой кнопкой мыши
по строке заголовков столбцов. Пожалуй, одним из самых интересных является столбец Has Encoding Error (Содержит
ошибку кодировки), который говорит о возможной проблеме
с кодировкой символов или о неправильной идентификации
строки. Кроме этого окна, Ghidra располагает развитой функциональностью поиска строк, обсуждаемой в главе 6.

Отображение данных в  Ghidra

125

Рис. 5.22. Окно определенных строк

ОКНА ТАБЛИЦЫ СИМВОЛОВ
И ССЫЛОК НА СИМВОЛЫ
В окне таблицы символов отображается сводный перечень всех
глобальных имен, найденных в двоичном файле. По умолчанию отображается восемь столбцов, показанных на рис. 5.23.
Окно настраивается в широких пределах: можно добавлять
и удалять столбцы, а также сортировать по любому столбцу
в порядке возрастания или убывания. По умолчанию первые
два столбца: Name (Имя) и Location (Адрес). Под именем понимается просто символическое описание символа, расположенного по указанному адресу.
Окно таблицы символов связано с окном листинга, но предлагает средства для управления своей связью с этим окном.
126

ГЛАВА 5

Увеличенный значок справа на рис. 5.23 является переключателем – он определяет, должен ли одиночный щелчок по адресу в окне таблицы символов приводить к соответствующему
перемещению в окне листинга. Независимо от установленного
режима двойной щелчок по любой строке таблицы символов
заставляет окно листинга немедленно перейти к выбранному
элементу. Это полезный инструмент для быстрой навигации
к известным адресам в листинге программы.

Рис. 5.23. Окно таблицы символов, значки Показать ссылки на
символы и Режим навигации увеличены
Окно таблицы символов располагает развитыми возможностями фильтрации и предлагает несколько способов доступа к параметрам фильтрации. Значок шестеренки на панели
инструментов открывает диалоговое окно фильтра таблицы
символов. Это окно (в котором флажок Use Advanced Filters
Отображение данных в  Ghidra

127

[Использовать дополнительные фильтры] поднят) показано
на рис. 5.24. Помимо этого диалогового окна, можно воспользоваться полем Filter в нижней части окна. Подробное обсуждение параметров фильтрации таблицы символов см. в справке
по Ghidra.
На рис. 5.23 увеличено два значка, левый из них – «Показать ссылки на символы». Щелчок по нему добавляет в окно
таблицы символов окно ссылок на символы. По умолчанию обе
таблицы располагаются бок о бок. Для удобства можете перетащить окно ссылок на символы под окно таблицы символов, как
показано на рис. 5.25. Связь между этими двумя таблицами
односторонняя: таблица ссылок на символы обновляется, когда
в таблице символов что-то выбирается.

Рис. 5.24. Диалоговое окно фильтрации таблицы символов

128

ГЛАВА 5

Рис. 5.25. Таблица символов вместе со ссылками на символы
В окне ссылок на символы точно такие же средства организации столбцов, как в окне таблицы символов. Дополнительно
для управления содержанием окна ссылок на символы имеются три значка (S, I и D) в правом верхнем углу панели инструментов. Эти режимы взаимно исключают друг друга, т. е.
в каждый момент времени можно выбрать только один.
Значок S. В этом режиме в окне ссылок на символы отобра­
жаются все ссылки на символ, выбранный в таблице символов. На рис. 5.25 показано, как выглядит окно ссылок,
когда выбран этот режим.
Значок I. В этом режиме в окне отображаются все ссылки
в командах из функции, выбранной в таблице символов
(список будет пуст, если выбрана не точка входа в функцию).
Значок D. В этом режиме в окне отображаются все ссылки
на данные из функции, выбранной в таблице символов.
Список будет пуст, если выбрана не точка входа в функцию или выбранная функция не ссылается на символы,
относящиеся к данным.

Отображение данных в  Ghidra

129

ОКНО КАРТЫ ПАМЯТИ
В окне карты памяти отображается список всех блоков памяти,
присутствующих в программе (рис. 5.26). Отметим, что блоки
памяти в терминологии Ghidra часто называются секциями
при обсуждении структуры двоичных файлов. В окне представлены имя блока памяти (секции), начальный и конечный
адреса, длина, флаги разрешений, тип блока, флаг инициализации, а также оставлено место для имени исходного файла
и комментариев. Начальный и конечный адреса описывают
диапазоны виртуальных адресов, на которые будут отображены секции программы во время выполнения.

Рис. 5.26. Окно карты памяти
Двойной щелчок по начальному или конечному адресу приводит к переходу на указанный адрес в окне листинга (и во
всех остальных связанных окнах). На панели инструментов
имеются значки для удаления и добавления блоков, перемещения блоков, разделения и слияния блоков, редактирования
адресов и задания нового базового адреса образа. Эти средства
особенно полезны, когда реконструируемые двоичные файлы
имеют нестандартный формат, поскольку загрузчик Ghidra может не распознать структуру сегмента.
Окну карты памяти соответствуют командные утилиты
objdump (с флагом -h), readelf (с флагом -S) и dumpbin (с флагом /HEADERS).
130

ГЛАВА 5

ОКНО ГРАФА ВЫЗОВОВ ФУНКЦИИ
В любой программе функция может вызывать другие функции
и сама быть вызвана из другой функции. В окне графа вызовов
функции показаны непосредственные соседи данной функции.
Мы называем Y соседом X, если Y непосредственно вызывает
X или X непосредственно вызывает Y. В момент открытия окна
Ghidra определяет соседей функции, внутри которой расположен курсор, и строит соответствующий граф. На экране показана функция в контексте ее использования в программе, но это
лишь часть полной картины.
На рис. 5.27 показано, что функция FUN_0040198c вызывается
из функции FUN_00401edc, а та, в свою очередь, вызывает шесть
других функций. Двойной щелчок по любой функции в окне
вызывает переход к данной функции в окне листинга и в других связанных окнах. Для построения графа вызовов функции
Ghidra использует механизм перекрестных ссылок (XREF), который подробно рассматривается в главе 9.

Рис. 5.27. Окно графа вызовов функции
Отображение данных в  Ghidra

131

Кто кого вызывает?
Окно графа вызовов функции, конечно, полезно, но иногда требуется исчерпывающая или хотя бы более полная картина. Окно
деревьев вызова функций (WindowFunction Call Trees) позволяет увидеть все вызовы выбранной функции и из выбранной
функции. Окно состоит из двух частей (рис. 5.28): входящие и исходящие вызовы. Те и другие можно раскрывать и сворачивать.

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

РЕЗЮМЕ
Поначалу количество окон в Ghidra ошеломляет. Проще будет ограничиться окнами, открытыми по умолчанию, пока
вы не накопите достаточно опыта, чтобы приступить к исследованию дополнительных возможностей. Как бы то ни было,
никто не заставляет использовать все, что может предложить
Ghidra. Не каждое окно полезно в любом сценарии обратной
разработки.
132

ГЛАВА 5

Один из лучших способов познакомиться с окнами Ghidra –
пошарить по различным вкладкам, которые Ghidra заполняет
данными о двоичном файле, и пооткрывать другие доступные
окна. Эффективность и производительность вашего труда будут возрастать по мере обретения комфорта в работе с Ghidra.
Ghidra – очень сложный инструмент. Совершенствуя свое
мастерство, вы можете столкнуться и с другими окнами, помимо рассмотренных в этой главе. Мы будем рассказывать о важных диалоговых окнах, когда в этом возникнет необходимость.
Сейчас вы уже более-менее свободно ориентируетесь в интерфейсе Ghidra и в рабочем столе браузера кода. В следующей
главе мы начнем разговор о многочисленных способах работы
с листингом дизассемблера – так вы лучше поймете его поведение, и жить с Ghidra станет легче.

6

ДИЗАССЕМБЛИРОВАНИЕ
В GHIDRA

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

лагаемые Ghidra, и как они могут помочь в расшифровке лис­
тинга дизассемблера.

НАВИГАЦИЯ ПО ЛИСТИНГУ
ДИЗАССЕМБЛЕРА
В главах 4 и 5 мы показали, что на базовом уровне Ghidra объединяет функции многих распространенных инструментов обратной разработки в своем интегрированном браузере кода.
Навигация по результирующему листингу – одно из основных
умений, необходимых для овладения Ghidra. Такие инструменты статического дизассемблирования, как objdump, не предлагают никаких встроенных средств навигации, кроме прокрутки
листинга вверх и вниз. Даже при использовании самых лучших
текстовых редакторов, включающих поиск в стиле grep, навигация по таким мертвым листингам затруднительна. С другой
стороны, в Ghidra встроены исключительно удобные средства
навигации. Помимо вполне стандартных возможностей поиска,
к которым привыкли пользователи текстовых редакторов или
процессоров, Ghidra строит и отображает полный список перекрестных ссылок, которые ведут себя как гиперссылки на вебстраницах. В итоге переход к месту, представляющему интерес,
в большинстве случаев сводится к двойному щелчку.

Имена и метки
В процессе дизассемблирования файла каждой точке в программе сопоставляется виртуальный адрес. Поэтому мы можем перемещаться по программе, задавая виртуальный адрес
того места, куда хотим попасть. К сожалению, держать список
адресов в голове – задача не тривиальная. Поэтому программисты уже давно придумали назначать символические имена
тем адресам, на которые нужно сослаться, тем самым сильно
упростив себе жизнь. Назначение символических имен адресам можно уподобить назначению мнемонических названий
команд кодам операций; программы гораздо проще читать
и писать, когда идентификаторы легко запоминаются. Ghidra
продолжает эту традицию, создавая метки для виртуальных
адресов и давая пользователю возможность изменять и расши136

ГЛАВА 6

рять набор меток. Мы уже видели, как имена используются
в окне дерева символов. Напомним, что двойной щелчок по
имени приводит к переходу на соответствующий адрес в окне
листинга (и в окне ссылок на символы). Хотя между терминами
имя и метка есть различие (например, функции имеют имена
и находятся не в той ветви дерева символов Ghidra, что метки),
в контексте навигации они являются синонимами, потому что
тот и другой описывают место, в которое нужно попасть.
Ghidra генерирует символические имена на этапе автоматического анализа, используя существующее имя в двоичном
файле (если оно доступно) или создавая его самостоятельно на
основе того, как программа ссылается на адрес. Помимо удобства запоминания, все метки, отображаемые в окне дизассемб­
лера, являются потенциальными целями навигации, подобно
гиперссылке на веб-странице. Между этими метками и стандартными гиперссылками есть два важных различия: метки
никак визуально не выделяются, и для перехода по метке
обычно требуется двойной, а не одинарный щелчок.

Познакомьтесь с соглашением
об именовании!
Ghidra предлагает пользователю большую гибкость в назначении меток, но некоторые комбинации имеют специальный смысл.
К ним относятся следующие префиксы, за которыми следует
знак подчеркивания и адрес: EXT, FUN, SUB, LAB, DAT, OFF и UNK.
При создании меток избегайте таких комбинаций. Кроме того,
в метках запрещены пробелы и непечатаемые символы. Зато
длина метки может достигать аж 2000 знаков. Считайте внимательнее, если опасаетесь превысить этот предел!

НАВИГАЦИЯ В GHIDRA
В листинге на рис. 6.1 все символы, на которые указывают
сплошные стрелки, представляют именованную цель навигации. Двойной щелчок по любому из них в окне листинга заставит Ghidra перерисовать окно листинга (и все связанные окна),
так чтобы выбранный адрес был виден.
Дизассемблирование в Ghidra

137

LAB_0040132b

XREF[1]:

0040132b

81 fa cd

CMP

EDX,0xcd

00401331

7f 1b

JG

LAB_0040134eu

00401333

0f 84 86

JZ

LAB_004013bfu

00401339

83 ea 0a

SUB

EDX,0xa

0040133c

74 69

JZ

LAB_004013a7u

0040133e

81 ea c1

SUB

EDX,0xc1

00401344

74 69

JZ

LAB_004013afu

00401346

4a

DEC

EDX

00401347

74 6e

JZ

LAB_004013b7u

LAB_0040134

XREF[1]:

004012ef(j)

Цели
навигации

00401331(j)

Рис. 6.1. Листинг, на котором показаны цели навигации
Для навигации Ghidra трактует еще два объекта как цели.
Во-первых, это перекрестные ссылки (показаны штриховыми
стрелками на рис. 6.1). Двойной щелчок по нижней перекрестной ссылке приведет к переходу по адресу 00401331. Перекрестные ссылки подробно рассматриваются в главе 9. Если задержать мышь над любым из этих объектов навигации, появится
всплывающая подсказка, содержащая код по этому адресу.
Во-вторых, есть еще один тип отображаемых объектов, заслуживающий специального обращения в контексте навигации, – записанный шестнадцатеричными цифрами. Если последовательность шестнадцатеричных значений представляет
действительный виртуальный адрес в двоичном файле, то этот
адрес будет показан справа, как на рис. 6.2.
00409013

04

??

04h

00409014

b0

??

B0h

?

00409015

37

??

37h

7

00409016

40

??

40h

@

00409017

00

??

00h

00409018

00

??

00h

00409019

0a

??

0Ah

0040901a

90

??

90h

?

0040901b

45

??

45h

E

0040901c

40

??

40h

@

0040901d

00

??

00h

0040901e

00

??

00h

0040901f

0a

??

0Ah

00409020

a8

??

A8h

–> 004037b0

Цели
навигации

?

–> 00404590

–> 00404da8

Рис. 6.2. Листинг, в котором присутствуют
шестнадцатеричные цели навигации
Двойной щелчок по этому адресу вызовет переход к нему
в окне дизассемблера. На рис. 6.2 двойной щелчок по любому
138

ГЛАВА 6

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

Перейти к
Если известен адрес или имя места, куда нужно попасть (например, в начало функции main в двоичном ELF-файле, откуда начинается анализ), то можно прокрутить листинг до этого
адреса, поискать нужное имя в папке Functions в окне дерева
или воспользоваться средствами поиска Ghidra, обсуждаемыми в следующей главе. Но самый простой способ перейти по
известному имени или адресу дает диалоговое окно Go To (Перейти к), показанное на рис. 6.3. Для доступа к нему служит
команда меню NavigationGo To или горячая клавиша G,
действующая, когда открыто окно дизассемблера.

Рис. 6.3. Диалоговое окно Go To
Для перехода в любое место двоичного файла нужно лишь
задать действительный адрес (чувствительное к регистру имя
символа или шестнадцатеричное значение) и нажать кнопку
OK. Введенные значения сохраняются в выпадающем списке
истории, что упрощает возврат в ранее посещенные места.

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

139

ется в историю. По этой истории можно перемещаться из окна
Go To или с помощью значков со стрелками вперед и назад на
панели инструментов браузера кода.
В диалоговом окне Go To, показанном на рис. 6.3, стрелка
справа от текстового поля открывает выпадающий список, из
которого можно выбрать предыдущий адрес, введенный в этом
окне Go To. Две крайние левые кнопки на панели инструментов браузера кода (рис. 6.4) реализуют привычное по работе с веббраузером поведение перехода вперед и назад. С каждой кнопкой
ассоциирован подробный выпадающий список истории, который
дает мгновенный доступ к любому ранее посещенному адресу без
необходимости проходить по всему списку. Пример выпадающего
списка, ассоциированного со стрелкой назад, показан на рис. 6.4.

Рис. 6.4. Стрелки навигации вперед и назад и список адресов
Комбинация alt-стрелка влево (option-стрелка влево
на Mac) для обратной навигации – одна из самых полезных
горячих клавиш, которые стоит запомнить. Обратная навигация чрезвычайно удобна, когда вы прошли по цепочке вызовов функций на несколько уровней вглубь и хотите вернуться
в исходное место в листинге. Комбинация alt-стрелка вправо
(option-стрелка вправо на Mac) перемещается по окну дизассемблера в прямом направлении списка истории.
Теперь у нас сложилась гораздо более четкая картина навигации по листингу дизассемблера в Ghidra, но мы все еще
140

ГЛАВА 6

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

КАДРЫ СТЕКА
Поскольку Ghidra – инструмент низкоуровневого анализа,
предполагается, что пользователь худо-бедно знаком с низкоуровневыми деталями компилируемых языков, а конкретно со спецификой генерирования команд машинного языка
и управления памятью в программе на языке высокого уровня.
Ghidra уделяет особое внимание тому, как компиляторы обрабатывают объявления локальных переменных и доступ к ним.
Вы, наверное, заметили, что в начале листингов большинства
функций много строк посвящено локальным переменным. Эти
строки – результат детального анализа стека, выполняемого
Ghidra для каждой функции с применением анализатора стека. Такой анализ необходим, потому что компиляторы размещают локальные переменные функции (а иногда и ее аргументы) в блоках памяти, выделенных в стеке. В этом разделе мы
объясним, как компиляторы обращаются с локальными переменными и аргументами функций, чтобы лучше понимать, как
устроен листинг дизассемблера в Ghidra.

Механизмы вызова функций
При вызове функции может понадобиться память для размещения информации, передаваемой функции в виде параметров
(аргументов), и для временного хранения на время выполнения
функции. Значения параметров или их адреса нужно сохранить в таком месте, где функция сможет их найти. Временная
память часто выделяется программистом путем объявления локальных переменных, которые могут быть использованы внут­ри
функции, но недоступны после ее завершения. Кадры стека
(или записи активации) – это блоки памяти, выделяемые в стеке программы на время одного выполнения функции.
Компиляторы прозрачно для программиста используют кад­
ры стека для выделения и освобождения памяти под параметры
Дизассемблирование в Ghidra

141

функции и локальные переменные. Следуя соглашениям о вызове, компилятор вставляет код для размещения параметров
в стеке, перед тем как передать управление самой функции,
а внутрь функции вставляет код, выделяющий достаточно памяти для хранения ее локальных переменных. В некоторых случаях в кадре стека хранится также адрес, по которому функция
должна вернуть управление. Кадры стека допускают рекурсию1,
потому что для каждого вызова функции создается отдельный
кадр, так что следующий вызов не конфликтует с предыдущим.
При вызове функции имеют место следующие операции.
1. Вызывающая сторона помещает необходимые функции
параметры в места, определяемые соглашением о вызове.
Указатель стека программы может измениться, если параметры передаются в стеке.
2. Вызывающая сторона передает управление вызываемой
функции с помощью таких команд, как CALL в x86, BL в ARM
или JAL в MIPS. Адрес возврата сохраняется в стеке или
в регистре процессора.
3. При необходимости вызванная функция изменяет указатель кадра и сохраняет в стеке значения регистров, которые
вызывающая сторона хочет видеть неизменными2.
4. Вызванная функция выделяет место для всех своих локальных переменных. Часто это делается путем изменения указателя стека, так чтобы зарезервировать память в стеке.
5. Вызванная функция делает свое дело, возможно, обращаясь
к переданным ей параметрам, и вычисляет результат. Если
функция что-то возвращает, то результат часто помещается
в специальный регистр или несколько регистров, так чтобы
вызывающая сторона могла получить к нему доступ после
возврата из функции.
1

2

142

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

ГЛАВА 6

6. Когда функция завершит свою операцию, память, выделенная в стеке для локальных переменных, освобождается.
Для этого часто производятся действия, обратные произведенным на шаге 4.
7. Восстанавливаются значения регистров, сохраненные на
шаге 3 в интересах вызывающей стороны.
8. Вызванная функция возвращает управление вызывающей стороне. Для этого имеются специальные команды: RET
в x86, POP в ARM и JR в MIPS. В зависимости от соглашения
о вызове при этом могут также выталкиваться из стека один
или несколько параметров.
9. Получив управление, вызывающая сторона может удалить
параметры из стека, восстановив значение указателя стека,
которое имело место до шага 1.
Шаги 3 и 4 обычно выполняются после входа в функцию
и составляют так называемый пролог функции. Аналогично шаги 6–8 составляют эпилог функции. Все операции, кроме шага 5, – часть накладных расходов, связанных с вызовом
функции. На языке высокого уровня они не видны, но на языке
ассемблера очень даже заметны.

Они и вправду ушли?
Когда говорят об «удалении» или «выталкивании» элементов
из стека, а также об удалении целых кадров стека, имеется
в виду, что указатель стека изменяется таким образом, что
указывает на данные, находящиеся ниже, поэтому удаленное
содержимое больше недоступно с помощью операции POP.
Но пока это содержимое не затерто последующей операцией
PUSH, оно все еще находится в стеке. С точки зрения программирования такая ситуация квалифицируется как удаление. Однако с точки зрения компьютерно-технической экспертизы это
означает, что неинициализированные локальные переменные
в кадре стека могут содержать значения, оставшиеся в памяти с момента последнего использования некоторого диапазона
байтов в стеке.

Дизассемблирование в Ghidra

143

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

Передача аргументов в регистрах и в стеке
Аргументы функции можно передавать в регистрах процессора, в стеке программы или там и там. Если аргументы помещаются в стек, то вызывающая сторона выполняет команду записи
в память (обычно PUSH), а вызываемая должна прочитать аргументы из памяти. Чтобы ускорить процесс вызова функции,
иногда соглашения о вызове допускают передачу аргументов
в регистрах. Если аргумент передается в регистре, то отпадает
необходимость в операциях чтения и записи в память, поскольку аргумент доступен вызываемой функции непосредственно.
Недостатком такой схемы является тот факт, что количество
регистров процессора ограничено, тогда как списки аргументов функции могут быть сколь угодно велики, поэтому соглашение должно учитывать случай, когда аргументов больше,
чем доступных регистров. «Лишние» аргументы, для которых
не хватило регистров, обычно размещаются в стеке.

Соглашение о вызове C
Соглашение о вызове C применяется по умолчанию в большинстве
компиляторов C при генерировании кода вызова функций. Чтобы
явно описать это соглашение в программе на C/C++, можно включить в прототип функции ключевое слово _cdecl. Соглашение
cdecl требует, чтобы вызывающая сторона помещала аргументы
144

ГЛАВА 6

в стек в порядке справа налево и чтобы она же (а не вызываемая
сторона) удаляла их из стека после завершения вызванной функции. На 32-разрядной платформе x86 соглашение cdecl требует,
чтобы все аргументы передавались в стеке. На 64-разрядной платформе x86 cdecl зависит от операционной системы; в Linux первые
шесть (или менее) аргументов помещаются в регистры RDI, RSI,
RDX, RCX, R8 и R9, именно в таком порядке, а остальные передаются
в стеке. На платформе ARM первые четыре аргумента передаются
в регистрах R0–R3, а начиная с пятого – в стеке.
Если передаваемые в стеке аргументы помещаются в стек в порядке справа налево, то самый левый аргумент всегда находится
на вершине стека. Это позволяет легко найти первый аргумент
вне зависимости от общего числа ожидаемых функцией аргументов, поэтому соглашение о вызове cdecl идеально подходит для
функций с переменным числом аргументов (например, printf).
Требование о том, чтобы вызывающая функция удаляла параметры из стека, означает, что вы часто будете видеть команды, которые корректируют указатель стека сразу после возврата из вызванной функции. В случае функций, принимающих
переменное число аргументов, именно вызывающая сторона
точно знает, сколько аргументов было передано, и может правильно выполнить корректировку, тогда как вызванная функция заранее не знает, сколько аргументов получит.
В следующих примерах рассматриваются вызовы функций
на 32-разрядной платформе x86 с разными соглашениями
о вызове. Первая функция имеет такой прототип:
void demo_cdecl(int w, int x, int y, int z);

По умолчанию в этой функции используется соглашение
о вызове cdecl, т. е. она ожидает, что все четыре параметра будут переданы в стеке в порядке справа налево и вызывающая
сторона очистит стек после возврата. Для следующего вызова
функции на C:
demo_cdecl(1, 2, 3, 4); // вызов demo_cdecl (на C)

компилятор может сгенерировать такой код:
Дизассемблирование в Ghidra

145








PUSH 4 ; поместить в стек параметр z
PUSH 3 ; поместить в стек параметр y
PUSH 2 ; поместить в стек параметр x
PUSH 1 ; поместить в стек параметр w
CALL demo_cdecl ; вызвать функцию
ADD ESP, 16 ; восстановить прежнее значение ESP

Четыре операции PUSH  увеличивают указатель стека (ESP)
на 16 байт (4 * sizeof(int) в 32-разрядной архитектуре), и это
изменение аннулируется сразу после возврата из demo_cdecl .
Показанная ниже техника, которая использовалась в некоторых версиях компиляторов GNU (gcc и g++), также согласуется с соглашением о вызове cdecl, но устраняет необходимость
явно удалять параметры из стека после каждого обращения
к demo_cdecl:
MOV
MOV
MOV
MOV
CALL

[ESP+12], 4
[ESP+8], 3
[ESP+4], 2
[ESP], 1
demo_cdecl

;
;
;
;
;

поместить параметр
поместить параметр
поместить параметр
поместить параметр
вызвать функцию

z
y
x
w

в четвертую позицию в стеке
в третью позицию в стеке
во вторую позицию в стеке
в первую позицию в стеке

В этом примере помещение параметров demo_cdecl в стек
не изменяет значения указателя стека. Заметим также, что
при вызове функции таким способом указатель стека указывает на самый левый аргумент в стеке.

Стандартное соглашение о вызове
В 32-разрядных DLL для Windows компания Microsoft часто
использует так называемое стандартное соглашение о вызове. В исходном коде оно описывается модификатором _stdcall
в объявлении функции:
void _stdcall demo_stdcall(int w, int x, int y);

Чтобы избежать путаницы, связанной с употреблением слова стандартный, мы далее будем называть это соглашение
просто stdcall.
146

ГЛАВА 6

Соглашение stdcall также требует, чтобы все размещаемые в стеке параметры функций помещались туда в порядке
справа налево, но за удаление параметров из стека отвечает
вызванная функция. Это возможно только для функций, принимающих фиксированное количество параметров; функции
с переменным числом аргументов, например printf, не могут
пользоваться соглашением о вызове stdcall.
Функция demo_stdcall принимает три целых аргумента, занимающих 12 байт в стеке (3 * sizeof(int) в 32-разрядной архитектуре). Компилятор на платформе x86 может воспользоваться специальной формой команды RET, которая одновременно
извлекает из стека адрес возврата и увеличивает указатель
стека, чтобы очистить стек от аргументов функции. В случае
demo_stdcall мы могли бы увидеть такую команду возврата:
RET 12 ; вернуться и удалить из стека 12 байт

Использование соглашения stdcall избавляет от необходимости очищать стек после каждого вызова функции, что делает
программы чуть меньше и чуть быстрее. Microsoft применяет
это соглашение для всех функций с фиксированным числом
аргументов, экспортируемых из 32-разрядных разделяемых
библиотек (DLL-файлов). Об этом важно помнить при попытке
сгенерировать прототипы функций или двоично совместимые
замены разделяемых библиотек.

Соглашение fastcall на платформе x86
Компиляторы Microsoft C/C++ и GNU gcc/g++ (начиная с версии 3.4) понимают соглашение fastcall – вариант соглашения
stdcall, при котором первые два аргумента помещаются в регистры ECX и EDX соответственно. Все остальные аргументы помещаются в стек в порядке справа налево, и за удаление их
из стека при возврате отвечает вызванная функция. В следующем объявлении демонстрируется использование модификатора fastcall:
void fastcall demo_fastcall(int w, int x, int y, int z);

Дизассемблирование в Ghidra

147

Для следующей функции, написанной на C:
demo_fastcall(1, 2, 3, 4); // вызов demo_fastcall (на C)

компилятор мог бы сгенерировать такой код:
PUSH 4
PUSH 3
MOV EDX, 2 ;
MOV ECX, 1 ;
Call demo_fastcall ;

; поместить параметр
; поместить параметр
поместить параметр x
поместить параметр w
вызвать функцию

z
y
в
в

во вторую позицию в стеке
на вершину стека
EDX
ECX

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

Соглашения о вызове C++
Нестатические функции-члены классов C++ должны получать
указатель на объект, от имени которого была вызвана функция
(указатель this)1. Адрес этого объекта должен быть передан
вызывающей стороной в качестве параметра, но в стандарте
языка C++ не описано, как именно должен передаваться этот
указатель this, поэтому неудивительно, что в разных компиляторах это делается по-разному.
На платформе x86 компилятор Microsoft C++ применяет соглашение о вызове thiscall, согласно которому this передается в регистре ECX/RCX, а нестатическая функция-член должна
чистить стек от параметров, как в соглашении stdcall. Компилятор GNU g++ считает this неявным первым параметром нестатической функции-члена и ведет себя точно так же, как при
использовании соглашения cdecl. Таким образом, в 32-раз1

148

В классе C++ могут быть определены функции-члены двух видов: статические и нестатические. Нестатические функции-члены используются
для манипулирования атрибутами конкретных объектов и потому должны точно знать, с каким объектом работают (указатель this). Статические
функции-члены принадлежат классу в целом и используются для манипулирования атрибутами, общими для всех экземпляров класса. Они
не нуждаются в указателе this (и не получают его).

ГЛАВА 6

рядном коде, откомпилированном g++, this помещается на
вершину стека перед вызовом нестатической функции-члена,
а вызывающая функция отвечает за удаление параметров (которых всегда не меньше одного) из стека после возврата из вызываемой. Дополнительные особенности откомпилированных
программ на C++ обсуждаются в главах 8 и 20.

Другие соглашения о вызове
Полное описание всех соглашений о вызове заняло бы целую
книгу. Они часто зависят от операционной системы, языка,
компилятора и процессора, и если вы столкнетесь с кодом, сгенерированным редким компилятором, то, возможно, придется
проделать небольшую исследовательскую работу. Однако есть
несколько ситуаций, заслуживающих специального внимания:
оптимизированный код, нестандартный код на языке ассемб­
лера и системные вызовы.
Если функции экспортируются для других программистов
(например, библиотечные), то важно, чтобы они следовали хорошо известным соглашениям о вызове, иначе с ними будет трудно организовать интерфейс. С другой стороны, если функция
предназначена только для внутреннего использования в программе, то соглашение о ее вызове должно быть известно лишь
самой программе. В таких случаях оптимизирующие компиляторы могут выбирать альтернативные соглашения о вызове
для генерирования более быстрого кода. Например, флаг /GL
компилятора Microsoft C/C++ означает, что нужно произвести
«оптимизацию всей программы», что может повлечь сквозную
оптимизацию использования регистров, не ограниченную отдельными функциями. А ключевое слово regparm в компиляторе GNU gcc/g++ позволяет программисту указать, что до трех
аргументов следует передавать в регистрах.
Решая писать на языке ассемблера, программист получает
полный контроль над передачей параметров своим функциям.
Если только он не ставит целью сделать свои функции доступными другим программистам, то может передавать параметры,
как ему заблагорассудится. Поэтому при анализе нестандартного ассемблерного кода, например подпрограмм обфускации
и шелл-кода, нужно проявлять особую осторожность.
Дизассемблирование в Ghidra

149

Системный вызов – это специальный вид вызова функции,
используемый для запроса обслуживания со стороны операционной системы. Обычно системные вызовы влекут за собой
переход из режима пользователя в режим ядра, чтобы ядро
операционной системы могло обслужить запрос пользователя.
То, каким образом инициируется системный вызов, зависит
от процессора и операционной системы. Например, в 32-разрядной ОС Linux на платформе x86 системные вызовы могут
инициироваться командой INT 0x80 или sysenter, а в других
операционных системах для x86 может использоваться только команда sysenter или иной номер прерывания, тогда как
в 64-разрядном коде для x86 используется команда syscall.
Во многих системах на платформе x86 (Linux составляет исключение) параметры системного вызова помещаются в стек,
а его номер – в регистр EAX непосредственно перед инициированием системного вызова. В Linux системные вызовы принимают параметры в конкретных регистрах, а иногда в памяти,
если параметров больше, чем доступных регистров.

Дополнительные сведения о кадре стека
В любом процессоре регистры – это ограниченный ресурс, который должен совместно использоваться всеми функциями
программы. Когда некоторая функция (func1) выполняется,
она считает, что все регистры процессора находятся в ее распоряжении. Когда func1 вызывает другую функцию (func2), та,
естественно, считает себя ничем не хуже и тоже хочет использовать все доступные регистры процессора, как ей удобно, но
если func2 будет произвольно изменять регистры, то может затереть значения, от которых зависит поведение func1.
Чтобы решить эту проблему, все компиляторы следуют четко определенным правилам распределения и использования
регистров. Обычно эти правила называют платформенным
двоичным интерфейсом прикладных программ (application
binary interface – ABI). ABI разделяет регистры на две категории: сохраняемые вызывающей стороной и сохраняемые вызываемой стороной. Когда одна функция вызывает другую,
вызывающая сторона обязана сохранять только регистры, принадлежащие первой категории, чтобы предотвратить потерю
150

ГЛАВА 6

их значений. Все регистры, принадлежащие второй категории,
сохраняются вызываемой функцией, перед тем как она начнет
использовать их для своих целей. Обычно это является частью
пролога функции, причем регистры, сохраненные вызванной
стороной, восстанавливаются в эпилоге функции прямо перед
возвратом. Регистры, сохраняемые вызывающей стороной,
называются мусорными (clobber) регистрами, потому что вызванная функция вправе модифицировать их, предварительно
не сохранив. Регистры же, сохраняемые вызванной стороной,
называются немусорными (no-clobber).
System V ABI для 32-разрядных процессоров Intel определяет
регистры EAX, ECX и EDX как сохраняемые вызывающей стороной,
а регистры EBX, EDI, ESI, EBPи ESP как сохраняемые вызываемой
стороной1. Глядя на откомпилированный код, можно заметить,
что компиляторы часто предпочитают использовать внутри
функции регистры, сохраняемые вызывающей стороной, потому что тем самым снимают с себя обязанность сохранять и восстанавливать их содержимое при входе и выходе из функции.

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

См. https://wiki.osdev.org/System_V_ABI.
Дизассемблирование в Ghidra

151

Примеры кадров стека
Решая любую сложную задачу, например задачу обратной
разработки двоичного файла, всегда следует заботиться об
эффективном использовании своего времени. Если требуется
разобраться в поведении дизассемблированной функции, то
чем меньше времени мы потратим на изучение типичных последовательностей команд, тем больше останется на анализ
трудных случаев. Прологи и эпилоги функций – отличные
примеры типичных последовательностей, поэтому вы должны
уметь распознавать их, понимать и быстро переходить к более
интересному коду, требующему размышлений.
Ghidra сводит свои знания о прологах функций к списку
локальных переменных в начале листинга каждой функции, но хотя дизассемблированный код от этого становится
более понятным, его объем ничуть не уменьшается. В следующих примерах мы обсудим два часто встречающихся типа
кадров стека и код их создания, так чтобы, встретив такой
код на практике, вы быстро проскочили мимо и перешли
к сути функции.
Рассмотрим следующую функцию, откомпилированную на
компьютере с 32-разрядным процессором x86:
void helper(int j, int k); // прототип функции
void demo_stackframe(int a, int b, int c) {
int x;
char buffer[64];
int y;
int z;
// тело функции не важно, лишь бы она
// вызывала другую функцию
helper(z, y);
}

Локальные переменные в demo_stackframe занимают 76 байт
(три 4-байтовых целых и 64-байтовый буфер). Какое бы соглашение о вызове – stdcall или cdecl – ни использовалось, кадр
стека будет выглядеть одинаково.

152

ГЛАВА 6

Пример 1: доступ к локальным переменным
по указателю стека
На рис. 6.5 показан возможный кадр стека при вызове demo_
stackframe. В этом примере компилятор решил использовать
указатель стека при любом доступе к переменным в кадре
стека, оставив прочие регистры для других целей. Если какая-нибудь команда изменит значение указателя стека, то
компилятор должен будет учесть это изменение при любом последующем обращении к локальным переменным.
Переменнная
ESP

Смещение

z

[ESP]

y

[ESP+4]

buffer

[ESP+8]

x

[ESP+72]

Saved EIP

[ESP+76]

a

[ESP+80]

b

[ESP+84]

c

[ESP+88]

Локальные
переменные

Параметры

Рис. 6.5. Пример кадра стека функции, откомпилированной
на компьютере с 32-разрядным процессором x86
Место для этого кадра отводится при входе в demo_stackframe
с помощью однострочного пролога:
SUB ESP, 76 ; выделить достаточно места для всех локальных переменных

В столбце «Смещение» на рис. 6.5 мы видим режим адресации x86 (в данном случае – база + смещение), необходимый для
доступа к любой локальной переменной и любому параметру
в кадре стека. В нашем примере ESP используется как базовый
регистр, а смещение – это расстояние от ESP до начала переменной в кадре стека. Однако смещения на рис. 6.5 правильны
только при условии, что значение, хранящееся в ESP, не изменяется. К сожалению, указатель стека изменяется довольно часто, и компилятор должен постоянно компенсировать эти
изменения, чтобы смещения переменных в кадре стека были
Дизассемблирование в Ghidra

153

правильны. Рассмотрим ассемблерный код вызова функции
helper из demo_stackframe:
 PUSH dword [ESP+4] ; поместить y в стек
 PUSH dword [ESP+4] ; поместить z в стек
CALL helper
ADD ESP, 8 ; cdecl требует, чтобы вызывающая сторона очистила
стек

Первая команда PUSH  правильно помещает локальную
переменную y по смещению, показанному на рис. 6.5. На первый взгляд может показаться, что вторая команда PUSH  снова ссылается на локальную переменную y, что неправильно.
Однако поскольку все переменные в кадре стека адресуются
относительно ESP, а первая команда PUSH  модифицирует ESP,
все смещения на рис. 6.5 необходимо временно скорректировать. Поэтому после первой PUSH  новое смещение локальной
переменной z становится равным [ESP+4]. Изучая функции, обращающиеся к переменным в кадре стека по указателю стека,
внимательно следите за изменениями указателя стека и соответственно корректируйте все последующие смещения.
По завершении функция demo_stackframe должна вернуть
управление вызывающей стороне. К сожалению, команда RET снимает требуемый ей адрес возврата с вершины стека и помещает
его в регистр счетчика команд (в данном случае – EIP). Но преж­де
чем выталкивать адрес возврата, необходимо удалить из стека
локальные переменные, чтобы указатель стека правильно указывал на сохраненный ранее адрес возврата. В данной конкретной функции (в предположении, что используется соглашение
о вызове cdecl) эпилог выглядит следующим образом:
ADD ESP, 76 ; скорректировать ESP, так чтобы он указывал на сохраненный
; адрес возврата
RET ; вернуть управление вызывающей стороне

Пример 2: дать указателю стека передохнуть
Ценой выделения второго регистра для адресации переменных в кадре стека мы можем разрешить указателю стека про154

ГЛАВА 6

извольно изменяться, не пересчитывая смещений всех переменных в кадре. Конечно, компилятор должен взять на себя
обязательство не изменять этот второй регистр, иначе он
столк­нется с теми же проблемами, что в предыдущем примере.
В этой ситуации компилятор должен сначала выбрать подходящий регистр, а затем сгенерировать код его инициализации
при входе в функцию.
Любой регистр, выбранный для этой цели, называется указателем кадра. В примере выше указателем кадра был регистр
ESP, поэтому можно говорить о кадре стека по базе ESP. Для
большинства архитектур ABI рекомендует, какой регистр использовать в качестве указателя кадра. Указатель кадра всегда считается немусорным регистром, поскольку вызывающая
функция может использовать его для тех же целей. В программах для x86 обычно указателем кадра назначается регистр EBP/
RBP (расширенный указатель базы). По умолчанию большинство компиляторов генерируют код, в котором указателем кад­
ра является какой-то регистр, отличный от указателя стека,
хотя есть возможность задать режим, при котором в этой роли
будет выступать указатель стека (например, компилятор GNU
gcc/g++ предлагает флаг -fomit-frame-pointer, при задании которого сгенерированный код функций не использует другой
регистр в качестве указателя кадра).
Чтобы понять, как будет выглядеть кадр стека функции
demo_stackframe при использовании выделенного указателя
кадра, рассмотрим новый пролог:
 PUSH EBP

 MOV EBP, ESP
 SUB ESP, 76

;
;
;
;

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

Команда PUSH  сохраняет текущее значение EBP вызывающей программы, потому что EBP – немусорный регистр. Это
значение EBP нужно будет восстановить перед возвратом. Если
нужно сохранить еще какие-то регистры, нужные вызывающей
стороне (к примеру, ESI или EDI), то компиляторы могут сделать
это одновременно с сохранением EBP или отложить сохранение
до момента выделения места для локальных переменных. ТаДизассемблирование в Ghidra

155

ким образом, не существует никакого стандарта на расположение сохраненных регистров в кадре стека.
После того как EBP сохранен, в него можно записать текущее
положение в стеке с помощью команды MOV , которая копирует текущее значение указателя стека (единственного регистра,
который гарантированно указывает на стек в настоящий момент) в EBP. Наконец, как и в случае кадра стека по базе ESP,
выделяется место для локальных переменных . Получившая­
ся структура стека показана на рис. 6.6.

ESP

EBP

Переменная

Смещение

z

[EBP-76]

y

[EBP-72]

buffer

[EBP-68]

x

[EBP-4]

Saved EBP

[EBP]

Saved EIP

[EBP+4]

a

[EBP+8]

b

[EBP+12]

c

[EBP+16]

Локальные
переменные

Параметры

Рис. 6.6. Кадр стека по базе EBP
При использовании выделенного указателя кадра смещения
всех переменных можно вычислить относительно этого регист­
ра, как показано на рис. 6.6. Чаще всего (хотя это необязательно) положительные смещения соответствуют размещенным
в стеке аргументам функции, а отрицательные – локальным
переменным. Коль скоро указатель кадра выделенный, указатель стека можно изменять как угодно, не опасаясь, что смещения переменных в кадре изменятся. Теперь вызов функции
helper можно реализовать следующим образом:
 PUSH dword [ebp-72]
PUSH dword [ebp-76]
CALL helper
ADD ESP, 8


156

ГЛАВА 6

; поместить y в стек
; поместить z в стек
; cdecl требует, чтобы вызывающая сторона
; очистила стек

Тот факт, что указатель стека изменился после первой команды PUSH , не оказывает никакого влияния на доступ к локальной переменной z в следующей команде PUSH.
Если в функции используется указатель кадра, то в ее эпилоге необходимо восстановить указатель кадра вызывающей
стороны. Если указатель кадра предполагается восстанавливать командой POP, то предварительно нужно удалить из стека локальные переменные, но это легко, потому что текущий
указатель кадра указывает на место в стеке, где хранится сохраненный указатель кадра. В программах для 32-разрядного
процессора x86, где в качестве указателя кадра используется
регистр EBP, типичный эпилог выглядит так:
MOV ESP, EBP
POP EBP
RET


;
;
;
;

очистить локальные переменные, переустановив ESP
восстановить EBP вызывающей стороны
снять со стека адрес возврата для возврата
управления вызывающей стороне

Эта последовательность операций встречается настолько часто,
что в системе команд x86 для нее имеется специальная команда LEAVE:
LEAVE ; копировать EBP в ESP и извлечь значение с вершины стека в EBP
RET ; снять со стека адрес возврата для возврата управления
; вызывающей стороне

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

ПРЕДСТАВЛЕНИЯ СТЕКА В GHIDRA
Кадры стека – понятие времени выполнения; кадр не может
существовать без стека и без работающей программы. Но это
не значит, что мы должны игнорировать концепцию кадра
стека, выполняя статический анализ кода с помощью инструДизассемблирование в Ghidra

157

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

Анализ кадров стека в Ghidra
В процессе первоначального анализа Ghidra делает все возможное, чтобы проследить поведение указателя стека на протяжении работы функции, замечая все операции PUSH и POP, а также
арифметические операции, которые могут изменить указатель
стека, например прибавление или вычитание констант. Цель
этого анализа – определить точный размер области локальных
переменных, отведенной под кадр стека, понять, используется
ли в функции выделенный указатель кадра (обращая внимание,
например, на последовательность команд PUSH EBP/MOV EBP, ESP),
и найти все ссылки на переменные, размещенные в кадре стека.
Например, увидев команду
MOV EAX, [EBP+8]

в теле demo_stackframe, Ghidra поймет, что первый аргумент функции (в данном случае – a) загружается в регистр EAX (см. рис. 6.6).
Ghidra может отличить ссылки на аргументы функции (расположенные ниже сохраненного адреса возврата) от ссылок на локальные переменные (расположенные выше адреса возврата).
Ghidra также смотрит, на какие адреса в памяти, занятой
кадром стека, имеются прямые ссылки. Например, хотя стек на
рис. 6.6 занимает 96 байт, мы, скорее всего, увидим ссылки всего на семь переменных (четыре локальные переменные и три
параметра). Поэтому можно сосредоточить внимание на семи
объектах, которые Ghidra идентифицировала как важные,
и не тратить времени на размышления о тех байтах, которые
Ghidra оставила непоименованными. В процессе идентификации и присваивания имен элементам в кадре стека Ghidra рас158

ГЛАВА 6

познает также, как переменные расположены друг относительно друга. В некоторых случаях это может быть очень полезно,
например при разработке эксплойта, поскольку Ghidra позволяет легко определить, какие переменные могут быть затерты в результате переполнения буфера. Декомпилятор Ghidra
(обсуждается в главе 19) также полагается на анализ кадра
стека и использует его результаты, чтобы понять, сколько аргументов получает функция и какие объявления переменных
должны быть в декомпилированном коде.

Кадры стека в листинге дизассемблера
Чтобы понять поведение функции, зачастую достаточно понять типы данных, которыми функция манипулирует. Во время чтения листинга дизассемблера первое, на что нужно обратить внимание, чтобы разобраться в этом вопросе, – разбиение
кадра стека на части. Ghidra предлагает два представления
кадра стека любой функции: сводное и детальное. Мы проиллюстрируем их на примере следующей версии функции demo_
stackframe, откомпилированной с помощью gcc:
void demo_stackframe(int i, int j, int k) {
int x = k;
char buffer[64];
int y = j;
int z = 10;
buffer[0] = 'A';
helper(z, y);
}

Поскольку локальные переменные существуют лишь во время выполнения функции, любая локальная переменная, которая не используется в функции сколько-нибудь разумным
способом, по существу бесполезна. Фактически приведенный
выше код функционально эквивалентен следующему (можно
сказать, оптимизированному):
void demo_stackframe_2(int b) {
helper(10, b);
}
Дизассемблирование в Ghidra

159

То есть хотя, на первый взгляд, функция очень занята, в действительности она симулирует бурную деятельность, чтобы
произвести впечатление на босса.
В оригинальной версии demo_stackframe локальные переменные x и y инициализируются значениями параметров k и j соответственно. Локальная переменная z инициализируется литералом 10, а первый символ 64-байтового локального массива
buffer, – значением 'A'. Результат дизассемблирования этой
функции с применением автоматического анализа по умолчанию показан на рис. 6.7.

Рис. 6.7. Результат дизассемблирования функции demo_
stackframe
160

ГЛАВА 6

В плане знакомства с нотацией дизассемблера Ghidra в этом
листинге много моментов, на которые стоит обратить внимание.
В этом обсуждении мы сосредоточимся на двух секциях, содержащих особенно полезную информацию. Для начала увеличим
сводку стека, как показано в следующем листинге (вы всегда
сможете вернуться к рис. 6.7, чтобы взглянуть на это сводное
представление кадра стека в контексте). Чтобы упростить обсуждение, будем употреблять термины локальная переменная
и аргумент для различения двух видов переменных. Если вид
переменной не играет роли, будем говорить просто переменная.
undefined
undefined
undefined4
undefined4
undefined4
undefined4
undefined4
undefined1

AL:1
Stack[0x4]:1 param_1
Stack[0x8]:4 param_2
Stack[0xc]:4 param_3
Stack[-0x10]:4 local_10
Stack[-0x14]:4 local_14
Stack[-0x18]:4 local_18
Stack[-0x58]:1 local_58

Ghidra предлагает сводное представление стека, в котором перечислены все переменные, на которые имеются прямые ссылки, и с каждой ассоциируется важная информация.
Осмыс­ленные имена (в третьем столбце), которые Ghidra присваивает каждой переменной, сообщают, что она собой представляет: именам аргументов предшествует префикс param_,
а именам локальных переменных – префикс local_. Таким образом, легко отличить одно от другого.
В дополнение к префиксам имя переменной содержит информацию о ее позиционном номере или положении в стеке.
В случае имен аргументов, например param_3, число равно позиционному номеру аргумента в списке параметров функции.
В случае имен локальных переменных, например local_10,
число – это шестнадцатеричное смещение переменной от начала кадра стека. Смещение показано также в среднем столбце
листинга, слева от имени. В этом столбце мы видим два компонента, разделенных двоеточием: вычисленная Ghidra оценка
размера переменной в байтах и положение переменной в кад­
ре стека, представленное в виде смещения от начального значения указателя стека при входе в функцию.
Дизассемблирование в Ghidra

161

Табличное представление этого кадра стека показано на
рис. 6.8. Как уже было сказано, параметры находятся ниже
сохраненного адреса возврата, поэтому их смещение положительно. А локальные переменные находятся выше адреса возврата, так что их смещение отрицательно. Порядок размещения локальных переменных в стеке не совпадает с порядком
их объявления в исходном коде, потому что компилятор вправе располагать переменные в стеке, как ему угодно, учитывая
различные факторы, например выравнивание и размещение
массивов после остальных локальных переменных.
Адрес
-0x68
-0x64

Описание

Имя

параметры helper

-0x58

buffer

local_58

-0x18

z

local_18

-0x14

y

local_14

-0x10

x

local_10

-0x04

Сохраненный EBP

0x00

Сохраненный адрес
возврата

0x04

i

param_1

0x08

j

param_2

0x0с

k

param_3

Рис. 6.8. Кадр стека в нашем примере

Анализ кадра стека с помощью
декомпилятора
Помните, мы говорили о функциональном эквиваленте кода?
void demo_stackframe_2(int j) {
helper(10, j);
}

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

ГЛАВА 6

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

Рис. 6.9. Окно декомпилятора для функции demo_stackframe
(создано анализатором ИД параметров)
Вы, вероятно, заметили, что функция demo_stackframe принимала три целых параметра, но лишь два из них (param_1 и param_2)
включены в листинг декомпилятора. Какой пропущен, и почему?
Дело в том, что дизассемблер и декомпилятор Ghidra по-разному
подходят к именам. Хотя оба именуют все параметры вплоть до последнего, на который есть ссылка, декомпилятор присваивает имена лишь параметрам до того, который используется осмысленным
образом, включительно. Среди имеющихся в Ghidra анализаторов
есть и анализатор идентификаторов параметров (Decompiler
Parameter ID). В большинстве случаев этот анализатор по умолчанию не задействован (он активируется только для файлов Windows
PE, размер которых меньше 2 МБ). Если активирован анализатор
идентификаторов параметров, то Ghidra использует выведенную
декомпилятором информацию о параметрах для именования параметров функции в листинге дизассемблера. В листинге ниже
показаны переменные в листинге дизассемблера demo_stackframe,
когда активирован анализатор ИД параметров:
undefined
undefined
undefined4
undefined4
undefined4
undefined4
undefined1

AL:1
Stack[0x4]:4 param_1
Stack[0x8]:4 param_2
Stack[-0x10]:4 local_10
Stack[-0x14]:4 local_14
Stack[-0x18]:4 local_18
Stack[-0x58]:1 local_58

Дизассемблирование в Ghidra

163

Заметим, что param_3 отсутствует в списке аргументов функции, потому что декомпилятор определил, что он не используется. Этот конкретный кадр стека обсуждается далее в главе 8.
Если вы когда-нибудь захотите, чтобы Ghidra выполнила анализ идентификаторов параметров после того, как двоичный
файл был открыт с деактивированным анализатором, то можете
выбрать из меню команду AnalysisOne ShotDecompiler
Parameter ID (АнализОднократныйИдентификаторов
параметров декомпилятором).

Локальные переменные как операнды
Теперь перейдем к секции, содержащей собственно дизассемб­
лированный код.
08048473
08048474
08048476
08048479
0804847c
0804847f
08048482
08048485
0804848c
08048490
08048493
08048496

55 PUSH EBP
89 e5 MOV EBP,ESP
83 ec 58 SUB ESP,0x58
8b 45 10 MOV EAX,dword ptr [EBP + param_3]
89 45 f4 MOV dword ptr [EBP + local_10],EAX
8b 45 0c MOV EAX,dword ptr [EBP + param_2]
89 45 f0 MOV dword ptr [EBP + local_14],EAX
c7 45 ec MOV dword ptr [EBP + local_18],0xa
0a 00 00 00
c6 45 ac 41 MOV byte ptr [EBP + local_58],0x41
83 ec 08 SUB ESP,0x8
ff 75 f0 PUSH dword ptr [EBP + local_14]
ff 75 ec PUSH dword ptr [EBP + local_18]

Мы видим в функции обычный пролог , где определяется
кадр стека по базе EBP. Компилятор выделяет в кадре стека
88 байт (0x58 равно 88) под локальные переменные . Это несколько больше, чем наша оценка – 76 байт, и показывает, что
иногда компиляторы дополняют область для локальных переменных лишними байтами ради выравнивания на некоторую
границу в памяти.
Важное отличие между листингом дизассемблера Ghidra
и выполненным нами ранее анализом кадра стека заключается в том, в листинге вы не увидите ссылок типа [EBP-12]
(которые формирует, например, objdump). Вместо этого все постоянные смещения заменены символическими именами, при164

ГЛАВА 6

сутствующими в представлении стека, где им были присвоены
смещения относительного начального указателя стека. С символическими именами иметь дело проще, чем с константами.
Заодно это дает нам имя, которое можно будет изменить, когда
мы поймем, для чего нужна переменная. Для справки Ghidra
показывает и исходный вид текущей команды, без всяких меток, в правом нижнем углу окна браузера кода.
В этом примере, поскольку у нас имеется исходный код для
сравнения, мы можем сопоставить сгенерированным Ghidra
именам переменных те имена, которые фигурировали в исходном коде, воспользовавшись различными ключами, рассыпанными по листингу дизассемблера.
1. Во-первых, demo_stackframe принимает три параметра, i, j и k,
которым соответствуют переменные param_1, param_2 и param_3.
2. Локальная переменная x (local_10) инициализируется значением параметра k (param_3) .
3. Аналогично локальная переменная y (local_14) инициализируется значением параметра j (param_2) .
4. Локальная переменная z (local_18) инициализируется значением 10 .
5. Первый символ 64-байтового массива символов buffer[0]
(local_58) инициализируется значением A (ASCII 0x41) .
6. Оба аргумента вызываемой функции helper помещаются в стек . Вместе с корректировкой указателя стека на
8 байт, предшествующей этим командам PUSH, указатель
стека изменяется на 16 байт. Тем самым сохраняется выравнивание на границу 16 байт, произведенное в программе ранее.

Редактор кадра стека в Ghidra
Помимо сводного представления стека, Ghidra предлагает детальный редактор кадра стека, в котором учтен каждый байт
в кадре. Чтобы открыть окно редактора кадра стека, щелкните
правой кнопкой мыши по функции или переменной в сводном
представлении стека и выберите из контекстного меню команду FunctionEdit Stack Frame (ФункцияРедактировать
кадр стека). На рис. 6.10 показано это окно для функции demo_
stackframe.
Дизассемблирование в Ghidra

165

Рис. 6.10. Пример детального представления стека
Поскольку в детальном представлении отражены все байты
в кадре стека, оно занимает гораздо больше места на экране,
чем сводное. Часть кадра стека, показанная на рис. 6.10, охва­
тывает 29 байт, это лишь малая толика всего кадра стека. Кроме того, на переменные local_10 , local_14  и local_18 
есть прямые ссылки в листинге дизассемблера, и видно, что
они инициализированы записью 4-байтового двойного слова
(dword). Зная, что было скопировано 32 бита данных, Ghidra
166

ГЛАВА 6

может заключить, что все эти переменные – 4-байтовые величины, поэтому им соответствуют метки undefined4 (4-байтовая
переменная неизвестного типа).
Поскольку это окно редактора, мы можем редактировать в нем
поля, изменять формат отображения и добавлять информацию,
если считаем, что это поможет. Например, можно было бы добавить имя для сохраненного адреса возврата со смещением 0x0.

Регистровые параметры
В соглашении о вызове для ARM до четырех параметров разрешается передавать без использования стека. В некоторых соглашениях на платформе x86-64 для этой цели разрешено задействовать целых шесть регистров, а в соглашениях для MIPS – аж
восемь. Регистровые параметры немного труднее идентифицировать, чем стековые.
Рассмотрим следующие два фрагмента кода на языке ассемблера:
stackargs: ; пример функции на 32-разрядной платформе x86
PUSH EBP ; сохранить немусорный регистр ebp
MOV EBP, ESP ; инициализировать указатель кадра
 MOV EAX, [EBP + 8] ; извлечь аргумент, помещенный в стек
MOV CL, byte [EAX] ; разыменовать извлеченный аргумент,
являющийся указателем
...
RET
regargs: ; пример функции на 64-разрядной платформе x86
PUSH RBP ; сохранить немусорный регистр rbp
MOV RBP, RSP ; инициализировать указатель кадра
 MOV CL, byte [RDI] ; разыменовать извлеченный аргумент,
являющийся указателем
...
RET
В первой функции производится доступ к части стека ниже сохраненного адреса возврата , и мы заключаем, что функция ожидает,
по меньшей мере, один аргумент. Ghidra, как и большинство хороших дизассемблеров, анализирует указатели стека и кадра, чтобы
выявить команды, которые обращаются к кадру стека функции.

Дизассемблирование в Ghidra

167

Во второй функции регистр RDI используется  раньше, чем был
инициализирован. Это может означать только, что RDI инициализирован где-то в вызывающей программе и, стало быть, используется для передачи информации в функцию regargs (т. е. это параметр). В терминах анализа программы RDI – активный регистр
при входе в regargs. Чтобы определить, сколько регистровых параметров ожидает функция, нужно идентифицировать все активные регистры, содержимое которых читается и используется до
того, как регистр был записан (инициализирован) внутри функции.
К сожалению, такой анализ потока данных обычно оказывается за пределами возможностей дизассемблеров, в т. ч. Ghidra.
С другой стороны, декомпиляторы должны выполнять такого рода анализ и обычно хорошо справляются с идентификацией регистровых параметров. Анализатор идентификаторов
парамет­ров (EditOptions for PropertiesAnalyzers)
может дополнить листинг дизассемблера, опираясь на анализ
параметров, выполненный декомпилятором.

Редактор стека дает детальное представление о внутренних
механизмах компиляторов. Из рис. 6.10 ясно, что компилятор
вставил лишние 8 байт между сохраненным указателем кадра
-0x4 и локальной переменной x (local_10). Эти байты занимают позиции кадра стека со смещениями от -0x5 до -0xc. Если
вы сами никогда не писали компиляторы и не горите желанием копаться в исходном коде GNU gcc, то можно лишь строить
гипотезы насчет того, зачем эти байты нужны. В большинстве
случаев мы можем отнести дополнительные байты на счет выравнивания, и обычно их присутствие никак не влияет на поведение программы. В главе 8 мы вернемся к редактору стека
и его применению к более сложным типам данных: массивам
и структурам.

ПОИСК
Как было показано в начале этой главы, Ghidra упрощает навигацию по листингу дизассемблера, чтобы можно было находить объекты, о которых мы уже знаем, и обнаруживать новые
объекты. Кроме того, многие окна спроектированы так, чтобы
168

ГЛАВА 6

свести воедино информацию конкретных типов (имена, строки,
импортированные объекты и т. д.) и упростить поиск по ним.
Однако для эффективного анализа листинга дизассемблера
часто бывает необходима возможность поиска новых ключей,
дающих пищу для размышлений. По счастью, в Ghidra имеется меню Search (Поиск), которое позволяет искать интересующие нас объекты. На рис. 6.11 показано содержимое этого меню
по умолчанию. В этом разделе мы поговорим о методах поиска
по листингу дизассемблера в текстовом и байтовом режимах.

Рис. 6.11. Меню поиска в Ghidra

Поиск по тексту программы
Поиск по тексту в Ghidra сводится к поиску подстрок в лис­
тинге дизассемблера. Для поиска по тексту служит команда
SearchProgram Text (ПоискВ тексте программы), которая открывает диалоговое окно, показанное на рис. 6.12. Доступно два вида поиска: по всей базе данных программы, а не
только по тексту, видимому в окне браузера кода, и по листингу в браузере кода. Помимо вида поиска, можно задать еще несколько не требующих объяснения параметров, описывающих,
как и что искать.

Дизассемблирование в Ghidra

169

Для навигации по результатам поиска предназначены кнопки Next (Следующий) и Previous (Предыдущий) в нижней
час­ти диалогового окна, а также кнопка Search All (Искать
все), которая открывает результаты поиска в новом окне, где
легко перейти к любому найденному.

Рис. 6.12. Диалоговое окно поиска по тексту программы

170

ГЛАВА 6

Нарекаю тебя…
Окна поиска – один из типов окон в Ghidra, которые можно переименовать по собственному желанию, что помогает отслеживать
окна в процессе экспериментов. Чтобы переименовать окно,
прос­то щелкните правой кнопкой мыши по полосе заголовка
и задайте желаемое имя. Полезный прием – включать в название окна строку поиска и мнемоническое обозначение параметров, чтобы было легче понять, что вы искали.

Поиск в памяти
Если требуется найти конкретное двоичное содержимое, например известную последовательность байтов, то поиск по тексту
не годится. Вместо этого нужно использовать предоставляемую
Ghidra возможность поиска в памяти. Для этого служит команда SearchMemory (ПоискВ памяти) или горячая клавиша S. На рис. 6.13 показано диалоговое окно поиска в памяти.
Для поиска последовательности шестнадцатеричных байтов
задайте строку, состоящую из списка разделенных пробелами
двузначных шестнадцатеричных значений (регистр неважен),
например c9 c3, как показано на рис. 6.13. Если вы не знаете
точно шестнадцатеричное значение, можно использовать метасимволы (* или ?).

Дизассемблирование в Ghidra

171

Рис. 6.13. Диалоговое окно поиска в памяти
Результаты поиска байтов c9 c3 в памяти в режиме Search
All (Искать все) показаны на рис. 6.14. Можно отсортировать по
любому столбцу, переименовать окно или применить фильтр.
Предоставляется также контекстное меню, позволяющее,
в частности, удалять строки и манипулировать выбранными
строками.
Искомые значения можно задавать также в виде строки, в десятичном или двоичном виде либо в виде регулярного выражения.
Для строкового, десятичного и двоичного форматов определены контекстно-зависимые параметры. Регулярные выражения
позволяют искать заданный образец, но только в прямом направлении – это ограничение связано с порядком их обработки.
В Ghidra используется грамматика регулярных выражений,
встроенная в Java, она очень подробно описана в справке.
172

ГЛАВА 6

Рис. 6.14. Результаты поиска в памяти

РЕЗЮМЕ
В этой главе мы хотели познакомить вас с минимальными навыками, необходимыми для эффективной интерпретации лис­
тингов дизассемблера и навигации по ним. В подавляющем
большинстве взаимодействий с Ghidra используются описанные выше операции. Однако умение выполнять простую навигацию, понимать базовые конструкции типа стека и искать по
листингу – лишь верхушка айсберга, для обратной разработки
нужно знать гораздо больше.
Располагая этими знаниями, мы можем перейти к следующему логическому шагу – как использовать Ghidra для решения
своих задач. В следующей главе мы начнем вносить простые
изменения в листинг дизассемблера, отражающие знания, полученные по мере того, как улучшается наше понимание содержимого и поведения двоичного файла.

7

УПРАВЛЕНИЕ
ДИЗАССЕМБЛИРОВАНИЕМ

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

Лучше бы я этого не делал
Хороший специалист по обратному конструированию не должен бояться исследовать, экспериментировать, а если необходимо, то откатываться назад и выбирать другой путь. Мощные
средства отмены в Ghidra дают возможность гибко отменять
(и повторять) действия, произведенные в процессе SRE. К этим
волшебным возможностям есть несколько способов доступа:
значки со стрелками на панели инструментов браузера кода
, показанные на рис. 7.1; команда EditUndo (Редактирование Отмена) в меню браузера и горячие клавиши отмены
Ctrl-Z и повтора Ctrl-Shift-Z.
1

2

Рис. 7.1. Значки отмены и повтора на панели
инструментов браузера кода

МАНИПУЛИРОВАНИЕ ИМЕНАМИ
И МЕТКАМИ
На данный момент нам известны две категории идентификаторов в листингах дизассемблера: метки (идентификаторы,
ассоциированные с адресами) и имена (идентификаторы, ассоциированные с переменными в кадре стека). Чаще всего мы
называем те и другие просто именами, поскольку и в Ghidra
различие между ними размыто. (Если хотите абсолютной точности, то с метками на самом деле ассоциированы имена, адреса, истории и т. д. Но обращаемся к метке мы обычно по имени.)
Более узкие термины мы будем использовать, когда различие
действительно важно.

176

ГЛАВА 7

Напомним, что имена переменных в стеке могут иметь
один из двух префиксов в зависимости от того, являются
ли они параметрами (param_) или локальными переменными (local_), а в процессе автоматического анализа адресам
присваиваются имена или метки с полезными префиксами
(например, LAB_, DAT_, FUN_, EXT_, OFF_, UNK_). Чаще всего
Ghidra автоматически генерирует имена и метки на основе
гипотез об использовании соответствующей переменной или
адреса, но вы все равно должны проанализировать программу самостоятельно, чтобы уяснить назначение каждой метки
или переменной.
Начинать анализ любой программы имеет смысл с замены
имен, выбранных по умолчанию, более осмысленными. По счастью, Ghidra позволяет легко изменить любое имя и распространяет это изменение по всей программе. Чтобы открыть диалоговое окно изменения имени, щелкните по имени и нажмите
клавишу L или выберите из контекстного имени команду Edit
Label (Редактировать метку). Начиная с этого момента про­
цедуры для переменных в стеке (имен) и именованных адресов
(меток) различаются, и эти различия описываются в следующих разделах.

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

Управление дизассемблированием

177

Рис. 7.2. Дерево символов, в котором показано повторное
использование имен параметров (param_1)
При попытке переименовать переменную в окне листинга открывается диалоговое окно, показанное на рис. 7.3. Тип
изменяемой сущности (переменная, функция и т. д.) показан
в полосе заголовка окна, а текущее имя (подлежащее изменению) – в редактируемом текстовом поле и в полосе заголовка.

Рис. 7.3. Переименование локальной переменной (local_14 в y)
178

ГЛАВА 7

После задания нового имени Ghidra заменяет все вхождения старого имени в текущей функции. Результат переименования local_14 в y в функции demo_stackframe показан в лис­
тинге ниже.
*******************************************************************
*
FUNCTION
*
*******************************************************************
undefined demo_stackframe(undefined param_1, undefined4)
undefined AL:1
undefined Stack[0x4]:1 param_1
undefined4 Stack[0x8]:4 param_2
undefined4 Stack[0xc]:4 param_3
undefined4 Stack[-0x10]:4 local_10
undefined4 Stack[-0x14]:4 y
undefined4 Stack[-0x18]:4 local_18
undefined1 Stack[-0x58]:1 local_58
demo_stackframe
08048473 55 PUSH EBP
08048474 89 e5 MOV EBP,ESP
08048476 83 ec 58 SUB ESP,0x58
08048479 8b 45 10 MOV EAX,dword ptr [EBP + param_3]
0804847c 89 45 f4 MOV dword ptr [EBP + local_10],EAX
0804847f 8b 45 0c MOV EAX,dword ptr [EBP + param_2]
08048482 89 45 f0 MOV dword ptr [EBP + y],EAX
08048485 c7 45 ec MOV dword ptr [EBP + local_18],0xa
0a 00 00 00
0804848c c6 45 ac 41 MOV byte ptr [EBP + local_58],0x41
08048490 83 ec 08 SUB ESP,0x8
08048493 ff 75 f0 PUSH dword ptr [EBP + y]
08048496 ff 75 ec PUSH dword ptr [EBP + local_18]
08048499 e8 88 ff CALL helper
ff ff
0804849e 83 c4 10 ADD ESP,0x10
080484a1 90 NOP
080484a2 c9 LEAVE
080484a3 c3 RET

Изменения  отражаются также в дереве символов, как
показано на рис. 7.4.

Управление дизассемблированием

179

Рис. 7.4. Переменованная локальная переменная y в дереве
символов

Запрещенные имена
Существует ряд ограничений на имена переменных в функции.
Приведем самые важные правила именования параметров.

180



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



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



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



Можно восстановить исходное присвоенное Ghidra имя
параметра, введя param_ и затем целое число. Если использовано то же число, что было изначально, то Ghidra
восстановит имя без возражений. Если же указано любое
другое число, то Ghidra выведет предупреждение «Rename
failed – default names may not be used» (Ошибка переименования – нельзя использовать имена по умолчанию). Нажатие в этот момент кнопки Cancel в диалоговом окне переименования параметров восстановит оригинальное имя.



Можно иметь два параметра с именами param_1 (присвоено
Ghidra) и Param_1 (присвоено вами). Имена чувствительны
к регистру, но такое повторное использование не рекомендуется.

ГЛАВА 7

Локальные переменные также чувствительны к регистру, и префикс local_ можно использовать, если суффикс содержит
не только цифры.
Ни для каких переменных нельзя использовать имя переменной,
уже встречающееся в текущей области видимости (например,
в одной функции). Ghidra откажется это делать и объяснит причину в диалоговом окне.
Наконец, если вы запутались с метками, то можете просмотреть
историю изменения метки переменной, нажав клавишу H или
выбрав из контекстного меню команду Show All History (Показать всю историю) и введя текущее (или прошлое) имя переменной в текстовом поле. То же самое можно сделать с помощью
команды SearchLabel History (ПоискИстория метки) в главном меню.

Где лучше изменять имя?
Имена переменных можно изменять в окнах листинга, дерева
символов и декомпилятора. Результат всегда одинаков, но диалоговое окно, открывающееся при переименовании из окна лис­
тинга, содержит больше информации. Все правила именования
переменных действуют при использовании любого метода.
Многие примеры имен параметров, встречающиеся в этой книге,
были изменены из окна листинга с помощью диалогового окна,
показанного на рис. 7.5 слева. Чтобы изменить имя из окна дерева символов, щелкните правой кнопкой мыши по имени и выберите команду Rename из контекстного меню. В окне декомпилятора воспользуйтесь горячей клавишей L или командой Rename
Variable в контекстном меню; соответствующее диалоговое
окно показано на рис. 7.5 справа. Функционально оба окна эквивалентны, но в правом нет информации о пространстве имен
и о свойствах параметра.

Управление дизассемблированием

181

Рис 7.5. Переименование переменной из окна
листинга или окна дерева символов (слева) и из окна
декомпилятора (справа)
В Ghidra пространством имен называется просто именованная
область видимости. Внутри пространства имен все символы уникальны. Глобальное пространство имен содержит все символы,
определенные в двоичном файле. Пространства имен функций
вложены в глобальное пространство имен. В пространстве имен
функции все имена переменных и метки уникальны. Функции
и сами могут содержать вложенные пространства имен, например ассоциированное с предложением switch (это позволяет
использовать одинаковые метки case в разных пространствах
имен; например в одной функции может быть два предложения
switch, в каждом из которых есть метка case 10).

Переименование меток
Метка – это имя, ассоциированное с адресом; она может присваиваться автоматически или пользователем. Как и в случае локальных переменных, диалоговое окно переименования открывается клавишей L или командой Edit Label из
контекстного меню. В процессе изменения имени метки можно изменить также ее пространство имен и свойства, как показано на рис. 7.6.

182

ГЛАВА 7

Рис. 7.6. Переименование функции
В полосе заголовка этого расширенного окна показаны тип
и виртуальный адрес сущности. В разделе Properties можно
указать, что это адрес точки входа, или закрепить адрес (см. раздел «Редактирование меток» ниже). Как отмечалось в главе 6,
максимальная длина имени в Ghidra равна 2000 знаков, так что
не стесняйтесь задавать осмысленные имена и даже включать
целое повествование об адресе (только без пробелов). Если имя
слишком длинное, то в окне листинга показывается только его
часть, но в окне декомпилятора оно будет видно целиком.

Добавление новой метки
Хотя Ghidra генерирует много меток по умолчанию, вы и сами
можете создать новые метки и связать их с адресами в листинге. Это может быть полезно для аннотирования листинга диз­
ассемблера, хотя в большинстве случаев лучше использовать
для этой цели комментарии (обсуждаются ниже в данной
главе). Для добавления новой метки откройте диалоговое окно
Add Label (клавиша L), показанное на рис. 7.7, подведя курсор к интересующему вас адресу. В выпадающем списке Enter
Label (Введите метку) будутприсутствовать недавно использованные имена, а выпадающий список Namespace (Пространство имен) позволяет выбрать подходящую область видимости.

Управление дизассемблированием

183

Рис. 7.7. Диалоговое окно добавления метки

О префиксах хороших и разных
Когда в процессе анализа Ghidra создает метки, она использует мнемонические префиксы, за которыми следует адрес, чтобы было понятно, что предположительно находится по адресу. Ниже перечислены эти префиксы с краткими описаниями. Дополнительные сведения о семантике каждого префикса имеются в справке по Ghidra.
LAB_адрес Код – автоматически сгенерированная метка (обычно адрес перехода внутри функции).
DAT_адрес Данные – автоматически сгенерированное имя глобальной переменной.
FUN_адрес Функция – автоматически сгенерированное имя
функции.
SUB_адрес Объект вызова (или эквивалента) – возможно,
не функция.
EXT_адрес Внешняя точка входа – вероятно, функция, написанная кем-то еще.
OFF_адрес Обрезок (внутри существующих данных или кода) –
вероятно, ошибка дизассемблирования.
UNK_адрес Неизвестно – назначение данных не определено.
Для меток функций действуют следующие специальные правила:


184

ГЛАВА 7

если удалить метку функции по умолчанию (например,
FUN_08048473) в окне листинга, то префикс FUN_ будет
заменен префиксом SUB_ (в данном случае получится
SUB_08048473);



добавление метки для адреса, который уже имеет метку
FUN_ по умолчанию, изменяет имя функции, а не создает
новую метку;



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

При попытке использовать один из зарезервированных Ghidra
префиксов для нового имени можно нарваться на конфликт.
Если вы упорны в своем намерении использовать зарезервированный префикс, то Ghidra отвергнет предложенную вами
метку, если полагает, что конфликт возможен. Это случается,
когда Ghidra думает, что суффикс похож на адрес (наш опыт показывает, что это четыре или более шестнадцатеричных цифр).
Например, Ghidra разрешает метки FUN_zone и FUN_123, но отвергает FUN_12345. Кроме того, если вы попытаетесь связать еще
одну метку с тем же адресом, что и метка функции по умолчанию (например, FUN_08048473), то Ghidra переименует функцию,
но не станет добавлять вторую метку для одного адреса.

Редактирование меток
Для редактирования метки служит горячая клавиша L или команда Edit Label из контекстного меню. При редактировании
метки открывается то же диалоговое окно, что при добавлении,
только поля в нем будут уже инициализированы текущими значениями. Отметим, что редактирование метки может повлиять
на другие метки, связанные с тем же адресом, независимо от
того, разделяют ли они общее пространство имен. Например,
если вы идентифицируете метку как точку входа, то Ghidra
опишет все метки, связанные с этим адресом, как точки входа.

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

Управление дизассемблированием

185

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

Флажок Primary (Основная) на рис. 7.7 показывает, что
именно эта метка будет отображаться вместо адреса. По умолчанию этот флажок неактивен для основной метки, так что отменить назначение метки основной невозможно. Это необходимо для того, чтобы всегда существовало какое-то отображаемое
имя. Если назначить основной другую метку, то ее флажок станет неактивным, зато флажки других меток, связанных с тем
же адресом, будут активны.
Хотя до сих пор мы ассоциировали метки с адресами, на
практике метки чаще всего ассоциируются с содержимым, которое по стечению обстоятельств является адресом. Например,
метка main чаще всего обозначает начало блока кода, являющегося главной функцией программы. Ghidra берет адрес этого
места из информации в заголовке файла. Если бы мы захотели
переместить все содержимое двоичного файла в новый диапазон адресов, то ожидали бы, что метка main по-прежнему правильно ассоциируется с новым адресом главной функции и соответствующим ему неизменившимся байтовым содержимым.
Если метка закреплена (pinned), то ее связь с содержимым по
помеченному адресу не разрывается. Если бы мы переместили содержимое двоичного файла в другой диапазон адресов,
то закрепленные метки остались бы связанными с прежними
адресами. Чаще всего закрепленные метки используются для
именования вектора сброса и адресов ввода-вывода, отображенных на память, которые фиксируются при проектировании
процессора и операционной системы.
186

ГЛАВА 7

Удаление метки
Для удаления метки под курсором можно использовать команду из контекстного меню (или клавишу DEL). Но имейте в виду, что не каждую метку можно удалить. Во-первых,
не удаляются метки, сгенерированные Ghidra по умолчанию.
Во-вторых, если вы переименовали метку по умолчанию, а затем решили удалить ее, то Ghidra заменит удаляемое имя первоначально присвоенным (это прямое следствие предыдущего
утверждения). Тонкие детали удаления меток обсуждаются
в справке по Ghidra.

Навигация по меткам
Метки ассоциируются с местами, доступными для навигации,
поэтому двойной щелчок по ссылке на метку вызывает переход на эту метку. Подробнее мы будем обсуждать эту тему
в главе 9, но уже сейчас запомните, что метку можно связать
с любым адресом в листинге, на который вы хотите переходить. Та же самая функциональность описывается в разделе «Аннотации» ниже, но иногда метка (особенно если иметь
в виду, что ее длина может достигать 2000 знаков) – кратчайший способ добиться цели.

КОММЕНТАРИИ
Включение комментариев в листинги дизассемблера и декомпилятора – особенно полезный способ оставить для себя заметки об открытиях, сделанных в процессе анализа программы.
Ghidra предлагает пять категорий комментариев, каждый для
своей цели. Начнем с комментариев, которые можно добавлять
прямо в окне листинга.
Открыть окно задания комментария (рис. 7.8) можно с помощью контекстного меню, но быстрее использовать для этой
цели горячую клавишу – точку с запятой (;). (Это логичный выбор, потому что точка с запятой начинает комментарий во многих языках ассемблера.)

Управление дизассемблированием

187

Рис. 7.8. Окно задания комментария
Окно задания комментария открывается для конкретного
адреса, который показан в полосе заголовка: на рис. 7.8 он равен 08048479. Комментарий, введенный на любой из пяти вкладок (EOL, Pre, Post, Plate и Repeatable), связывается с этим
адресом.
По умолчанию комментарий вводится в текстовое поле, при
этом многострочные комментарии можно разбивать знаками
новой строки. По завершении ввода нажмите кнопку Apply
(Применить) или OK. (Кнопка Apply позволяет посмотреть
на комментарий в контексте, не закрывая окно, а затем продолжить редактирование.) Чтобы сэкономить время на вводе коротких комментариев, отметьте флажок Enter accepts
comment (Enter завершает комментарий) в левом нижнем
углу окна (его всегда можно временно сбросить, если вы пишете особенно длинный вводный комментарий).

Три кнопки, три кнопки, три кнопки
Из трех кнопок в нижней части диалогового окна задания комментария (рис. 7.8) OK и Apply ведут себя в соответствии с ожиданиями. Нажатие OK закрывает окно и сохраняет изменения. При
нажатии Apply листинг обновляется, поэтому можно посмотреть
на результат изменений и одобрить их или продолжить редактирование комментария.
Но вот кнопка Dismiss (Отклонить) – не то же самое, что Cancel,
которая просто закрыла бы окно без внесения каких-либо изменений в листинг! Специальному термину соответствует специ-

188

ГЛАВА 7

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

Чтобы удалить комментарий, сотрите его текст в окне задания
диалога или нажмите клавишу Del, когда курсор находится на
комментарии в окне листинга. Чтобы восстановить комментарий, когда-то ассоциированный с данным адресом, выберите
из контекстного меню команду CommentsShow History for
Comment (КомментарииПоказать историю комментария).

Концевые комментарии
Пожалуй, чаще всего используется концевой комментарий
(EOL comment), который ставится в конце строки в окне лис­
тинга. Чтобы добавить такой комментарий, откройте окно задания комментария клавишей ; и перейдите на вкладку EOL
Comment. По умолчанию концевые комментарии отображаются синим цветом и могут занимать несколько строк, если
вы ввели в текстовом поле многострочный комментарий. Все
строки будут выровнены по левому краю и помещены справа на странице, а существующее содержимое будет сдвинуто
вниз. Комментарий можно в любой момент изменить, заново
открыв окно задания комментария. Самый быстрый способ
удалить комментарий – щелкнуть по нему в окне листинга
и нажать Del.
В процессе автоматического анализа Ghidra сама добавляет
много концевых комментариев. Например, если загружен PEфайл, то Ghidra вставляет концевые комментарии для описания
полей в секции IMAGE_DOS_HEADER, в т. ч. комментарий Magic number.
Ghidra может это сделать, только если с конкретным типом данных ассоциирована такая информация. Обычно эта информация включается в библиотеки типов, которые отображаются
в окне диспетчера типов данных и обсуждаются в главах 8 и 13.
Из всех типов комментариев концевые лучше всего конфигурируются с помощью команды EditTool OptionsListing Fields
Управление дизассемблированием

189

(РедактированиеПараметры инструментовПоля лис­тинга),
открывающей окно, где можно настроить комментарии всех типов.

Предварительные и заключительные
комментарии
Предварительные (pre) и заключительные (post) комментарии – это полнострочные комментарии, расположенные непосредственно над или под некоторой строкой листинга дизассемблера.
В следующем листинге показаны многострочный предварительный комментарий и усеченный однострочный заключительный
комментарий, ассоциированный с адресом 08048476. Если задержать курсор над усеченным комментарием, то будет показан его
полный текст. По умолчанию предварительные комментарии
отображаются фиолетовым цветом, а заключительные — синим,
чтобы их было проще сопоставить с адресом в листинге.
08048473 PUSH EBP
08048474 MOV EBP,ESP
******** Предварительный комментарий - это многострочный комментарий.
******** В следующей команде выделяется 88 байт для локальных
******** переменных в кадре стека.
08048476 SUB ESP,0x58
******** Заключительный комментарий - теперь место выделено ...
08048479 MOV EAX,dword ptr [EBP + param_3]

Вводные комментарии
Вводный (plate) комментарий позволяет сгруппировать текст
комментария для отображения в любом месте окна листинга.
Он центрируется и размещается внутри прямоугольной области, обрамленной звездочками. Во многих рассматриваемых
в этой книге листингах имеется простой вводный комментарий
со словом FUNCTION в рамке, как показано на рис. 7.9. В этом
примере справа приведено также окно декомпилятора, чтобы
было наглядно видно, что в этом представлении по умолчанию
вводный комментарий присутствует в окне листинга, но отсутствует в окне декомпилятора.

190

ГЛАВА 7

Рис. 7.9. Пример вводного комментария
Открыв окно комментария, когда выбран первый адрес, принадлежащий функции, вы сможете заменить этот вводный комментарий, подставляемый по умолчанию, своим собственным, более информативным, как показано на рис. 7.10. Помимо замены
вводного комментария по умолчанию, Ghidra добавляет введенный вами комментарий в окно декомпилятора, но уже в стиле C.
Если бы в момент создания вводного комментария курсор находился в начале окна декомпилятора, то результат был таким же.

Рис. 7.10. Пример вводного комментария
ПРИМЕЧАНИЕ

По умолчанию в окне декомпилятора отображаются только вводные и предварительные комментарии, хотя это можно
изменить, выполнив команду EditTool
OptionsDecompilerDisplay.

Управление дизассемблированием

191

Повторяемые комментарии
Повторяемый комментарий вводится один раз, но может автоматически включаться в разные места листинга дизассемблера.
Поведение повторяемых комментариев связано с концепцией
перекрестных ссылок, которая подробно обсуждается в главе 9.
Проще говоря, повторяемый комментарий, введенный в месте
назначения перекрестной ссылки, повторяется в ее источнике.
В результате один повторяемый комментарий может отображаться во многих местах листинга (поскольку перекрестные
ссылки могут иметь тип «многие к одному»). В листинге дизассемблера повторяемый комментарий отображается оранжевым
цветом, а его повторения – серым, так чтобы их было легко отличить от комментариев других типов. В следующем листинге
демонстрируется использование повторяемого комментария.
08048432 JGE LAB_08048446 Повторяемый комментарий для адреса 08048446
08048434 SUB ESP,0xc
08048437 PUSH s_The_second_parameter_is_larger
0804843c CALL puts
08048441 ADD ESP,0x10
08048444 JMP LAB_08048470
LAB_08048446
08048446MOV EAX,dword ptr [EBP + param_2] Повторяемый комментарий для адреса 08048446

В этом листинге повторяемый комментарий задан для адреса
08048446  и повторен для адреса 08048432 , потому что команда
по адресу 08048432 ссылается на адрес 08048446 как на цель перехода (т. е. существует перекрестная ссылка с 08048432 на 08048446).
Если концевой и повторяемый комментарий связаны с одним и тем же адресом, то в листинге будет виден только концевой комментарий. Но в диалоговом окне задания комментарии
можно увидеть и изменить оба комментария. Если удалить
концевой комментарий, повторяемый станет виден в листинге.

Комментарии для параметров
и локальных переменных
Чтобы связать комментарий с переменной в стеке, укажите на
переменную и нажмите клавишу ;. На рис. 7.11 показано открывающееся минимальное окно комментария. Комментарий
192

ГЛАВА 7

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

Рис. 7.11. Комментарий для переменной в стеке

Аннотации
Ghidra предлагает интересную возможность аннотировать
комментарии ссылками на программы, URL-адреса, адреса
в листинге и символы; это делается в диалоговом окне задания комментария. Информация о символе в комментарии автоматически изменится при изменении имени символа. Если
аннотация используется для запуска указанной программы, то
для нее можно задать параметры, чтобы точнее определить поведение (да, нам тоже это кажется опасным).
Например, в состав вводного комментария на рис. 7.12 включена аннотация, содержащая гиперссылку на адрес в листинге. Дополнительные сведения о возможностях аннотаций см.
в справке по Ghidra.

Рис. 7.12. Пример аннотации, содержащей ссылку на адрес

Управление дизассемблированием

193

БАЗОВЫЕ ПРЕОБРАЗОВАНИЯ КОДА
Во многих случаях нам вполне достаточно листингов дизассемб­
лера, сгенерированных Ghidra. Но не всегда. По мере того как
типы анализируемых файлов все больше отличаются от обыкновенных исполняемых файлов, генерируемых распространенными компиляторами, возникает необходимость взять контроль
над анализом листинга и его отображением на себя. Особенно это
относится к анализу обфусцированного кода или файлов, в которых используется нестандартный (неизвестный Ghidra) формат.
Ghidra поддерживает следующие преобразования кода
(в числе прочих):
► изменение параметров отображения;
► форматирование операндов команд;
► манипулирование функциями;
► преобразование данных в код;
► преобразование кода в данные.
В общем случае, если двоичный файл очень сложен или
Ghidra незнакома с последовательностями команд, сгенерированными компилятором, на этапе анализа у Ghidra возникнет
больше проблем, и вам придется вносить корректировки в дизассемблированный код.

Изменение параметров отображения кода
Ghidra позволяет очень точно управлять форматированием
строк в окне листинга. Структура кода определяется форматером полей браузера (с которыми мы познакомились в главе 5).
Щелчок по значку форматера открывает окно со вкладками,
содержащее все поля листинга (см. рис. 5.8). Вы можете добавлять, удалять и изменять порядок полей, пользуясь простым
интерфейсом на основе перетаскивания, который позволяет
сразу же видеть изменения в листинге. Тесная связь между
элементом в поле листинга и в форматере полей браузера
очень полезна. Всякий раз, как вы перемещаете курсор в новое положение в окне листинга, форматер переходит на соответствующую вкладку и поле в ней, чтобы можно было сразу же
видеть параметры данного элемента. Форматер полей браузе194

ГЛАВА 7

ра обсуждается далее в разделе «Специальные средства редактирования для некоторых инструментов» главы 12.
Для управления внешним видом отдельных элементов в окне
листинга выберите команду EditTool Options, как описано
в главе 4. Отдельные подменю для каждого поля листинга позволяют точно настроить внешний вид и поведение поля. Хотя
возможности настройки для каждого поля свои, в общем случае
можно задавать цвет отображения, значение по умолчанию,
конфигурацию и формат. Например, пользователи, которые
любят на досуге почитать ассемблерный код, могут изменить
параметры по умолчанию в области поля концевых комментариев (EOL Comments Field), показанные на рис. 7.13, чтобы
включить режим показа точки с запятой в начале каждой строки (Show Semicolon at Start of Each Line) и просматривать
комментарии в более привычном формате.

Рис. 7.13. Меню параметров инструмента для поля концевых
комментариев
Чтобы изменить цвет фона для отдельных строк или более
крупных блоков в окне листинга, выберите из контекстного меню
пункт Colors (Цвета) и укажите цвет. Диапазон доступных цветов широк, а для цветов, которые выбирались недавно, имеется
Управление дизассемблированием

195

быстрая «напоминалка». То же меню позволяет очистить цвет
фона для строки, выбранной области или всего файла.
ПРИМЕЧАНИЕ

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

Форматирование операндов команд
В процессе автоматического анализа Ghidra принимает много
решений о форматировании операндов команд, особенно различных целочисленных констант, используемых в командах
разных типов. Среди прочего эти константы могут представлять относительные смещения в командах перехода или вызова, абсолютные адреса глобальных переменных, операнды
арифметических операций или постоянные, определенные
программистом. Чтобы сделать листинг дизассемблера понятнее, Ghidra пытается там, где возможно, использовать символические имена вместо чисел.
Иногда решения о форматировании принимаются с учетом
контекста команды (например, когда это команда вызова),
а иногда на основе используемых данных (например, доступ
к глобальной переменной либо смещение от начала кадра
стека или структуры). Зачастую точный контекст, в котором
используется константа, может оказаться непонятен Ghidra.
В таком случае константа обычно форматируется как шестнадцатеричное значение.
Если вы не принадлежите к числу тех немногих людей, для
которых работать с шестнадцатеричными числами так же
естест­венно, как есть, спать или дышать, то пора познакомиться со средствами форматирования операндов в Ghidra. Пусть
имеется следующий фрагмент в листинге дизассемблера:
08048485 MOV dword ptr [EBP + local_18],0xa
0804848c MOV byte ptr [EBP + local_58],0x41

196

ГЛАВА 7

Щелчок правой кнопкой мыши по шестнадцатеричной
константе 0x41 открывает контекстное меню, показанное на
рис. 7.14 (этот пример в контексте показан на рис. 6.7). Формат
константы можно изменить, выбрав любое из числовых представлений в правой части рисунка или представив ее в виде
символа (поскольку значение попадает в диапазон печатаемых символов ASCII). Это очень полезная возможность, потому
что мы не всегда знаем, какое из многочисленных представлений окажется наиболее подходящим. Во всех случаях в меню
отобра­жается тот текст, которым будет заменен операнд при
выборе данного пункта.

Рис. 7.14. Варианты форматирования констант
Часто программисты пользуются именованными константами в исходном коде. Такие константы могут определяться
директивой #define (или ее эквивалентом) или входить в состав
перечисления. К сожалению, по откомпилированному коду
уже невозможно понять, что это было: символическая или литеральная числовая константа. Но зато Ghidra располагает
обширным каталогом именованных констант, определенных
в различных библиотеках, в т. ч. стандартной библиотеке C и
в Windows API. Этот каталог доступен с помощью пункта Set
Equate (Приравнять) (клавиша E) в контекстном меню для
данного значения константы. Выбрав его для константы 0xa,
вы увидите окно приравнивания, показанное на рис. 7.15.

Управление дизассемблированием

197

Рис. 7.15. Диалоговое окно приравнивания
Для заполнения этого диалогового окна используется внут­
ренний список констант, в котором ищется константа, которую
мы пытаемся отформатировать. В данном случае мы видим все
известные Ghidra константы, равные 0xA. Если бы мы решили,
что значение имеет отношение к созданию сетевого соединения
по стандарту X.25, то могли бы выбрать символическую константу AF_CCITT, и строка в листинге дизассемблера приняла
бы такой вид:
08048485 MOV dword ptr [EBP + local_18],AF_CCITT

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

Манипулирование функциями
Ghidra дает возможность манипулировать функциями в лис­
тинге дизассемблера (например, изменить решение Ghidra
о принадлежности кода некоторой функции или изменить
атрибуты функции). Это особенно полезно, когда вы не со198

ГЛАВА 7

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

Создание новых функций
Новые функции можно создавать из существующих команд,
которые еще не принадлежат никакой функции. Для создания
функции щелкните правой кнопкой мыши по первой команде, которая станет ее частью, и выберите из контекстного меню
пункт Create Function (Создать функцию) (или нажмите клавишу F). Если выбран диапазон адресов, то он станет телом
функции. Если нет, то Ghidra проследит поток управления
в попытке определить, где кончается тело.

Удаление функций
Чтобы удалить существующую функцию, поместите курсор
внутрь ее сигнатуры и нажмите клавишу Del. Такое желание
может возникнуть, если вы полагаете, что Ghidra ошиблась
в автоматическом анализе, или если вы сами создали функцию
по ошибке. Отметим, что хотя после удаления функция со всеми ее атрибутами перестает существовать, с самими байтами,
составлявшими ее, ничего не происходит, так что при желании
функцию можно будет воссоздать.

Редактирование атрибутов функции
Ghidra связывает с каждой функцией несколько атрибутов, которые можно просмотреть, выбрав команду WindowFunctions
из меню браузера кода. (По умолчанию отображается только
пять атрибутов, но щелчком по заголовку столбца можно добавить дополнительные атрибуты из имеющихся шестнадцати.)
Для редактирования атрибутов откройте диалоговое окно, выУправление дизассемблированием

199

брав команду Edit Function (Редактировать функцию) из контекстного меню, которое открывается, когда курсор находится
между вводным комментарием функции и последней локальной переменной перед началом дизассемблированного кода
функции. Пример диалогового окна редактирования функции
показан на рис. 7.16.

Рис. 7.16. Диалоговое окно редактирования функции
Ниже описываются атрибуты, которые можно изменить
в этом диалоговом окне.
Имя функции
Можно изменить имя в текстовом поле в верхней части окна
или в поле Function Name.
Атрибуты функции
Есть пять необязательных атрибутов функции. Первые четыре – Varargs (С переменным числом аргументов), In Line
200

ГЛАВА 7

(Встраиваемая), No Return (Не возвращает управление)
и Use Custom Storage (Со специальной памятью) – флажки,
по умолчанию сброшенные. Пятый атрибут, Call Fixup (Корректировка вызова), находится в левой нижней части окна
и по умолчанию равен none, но из выпадающего списка можно выбрать другое значение. Если модифицировать любой из
атрибутов функции, то Ghidra автоматически распространит
новый прототип всюду, где он встречается в листинге.
Флажок Varargs означает, что функция принимает переменное число аргументов (как, например, printf). Этот флажок также поднят, если вы редактируете (в текстовом поле
в начале окна на рис. 7.16) список параметров, заканчивающийся многоточием (…). Флажок In Line только включает
ключевое слово inline в прототип функции (имейте в виду,
что если функция действительно была встроена компилятором, то вы не увидите ее как отдельный объект в листинге
дизассемблера, потому что ее тело стало частью вызывающей функции). Флажок No Return используется, когда
известно, что функция не возвращает управление (например, если в ней используется exit или это просто предикат
для перехода к другой функции). Если для функции поднят
флажок No Return, то Ghidra не будет предполагать, что
байты, следующие за вызовом функции, достижимы, если
только нет других свидетельств в пользу их достижимости,
например команды перехода на эти байты. Флажок Use
Custom Storage позволяет отменить проведенный Ghidra
анализ местоположения и размера памяти, отведенной для
параметров и возвращаемого значения.
Соглашение о вызове
Выпадающий список Calling Convention позволяет изменить соглашение о вызове, действующее для функции. Это
может повлиять на результаты анализа указателя стека,
поэтому важно указывать соглашение правильно.
Переменные функции
В области Function Variables можно редактировать переменные функции, получая помощь от Ghidra. Когда вы
изменяете данные в любом из четырех столбцов, Ghidra
Управление дизассемблированием

201

сообщает информацию, помогающую не наделать ошибок. Например, при попытке изменить значение в столбце Storage (Память) для param_1 будет выдано сообщение Enable 'Use Custom Storage' to allow editing of
Parameter and Return Storage (Отметьте флажок 'Use
Custom Storage', чтобы разрешить редактирование памяти
для параметров и возвращаемого значения). Расположенные справа значки позволяют добавлять, удалять и перемещаться по списку переменных.

Преобразование данных в код
(и наоборот)
В процессе автоматического анализа данные могут быть ошибочно сочтены кодом и представлены в виде команд. И наоборот, байты кода могут быть неправильно интерпретированы как
данные и соответственно отформатированы. Это может происходить по разным причинам, например потому, что компиляторы иногда встраивают данные в кодовые секции программы,
или потому, что на некоторые команды нет прямых ссылок, поэтому Ghidra решает не дизассемблировать их. В частности, обфусцированные программы сознательно стремятся затушевать
различие между кодом и данными (см. главу 21).
Первое, что можно предпринять для переформатирования, –
отменить текущее форматирование (кода или данных). Чтобы
отменить распознавание функций, кода или данных, щелк­
ните правой кнопкой мыши по соответствующему элементу
и выберите из контекстного меню команду Clear Code Bytes
(Очистить байты кода) (клавиша C). В результате байты, составляющие элемент, будут представлены в виде списка неформатированных байтов. Если нужно отменить распознавание большого участка, то предварительно выделите весь список
адресов, воспользовавшись буксировкой с нажатой кнопкой
мыши. Например, рассмотрим листинг простой функции:
004013e0
004013e1
004013e3
004013e4

202

ГЛАВА 7

PUSH EBP
MOV EBP,ESP
POP EBP
RET

После отмены ее распознавания получится показанная ниже
последовательность неклассифицированных байтов, которую
можно переформатировать как угодно:
004013e0 ?? 55h U
004013e1 ?? 89h
004013e2 ?? E5h
004013e3 ?? 5Dh ]
004013e4 ?? C3h

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

ОСНОВЫ ПРЕОБРАЗОВАНИЯ ДАННЫХ
Для понимания поведения программы правильно отформатированные данные не менее важны, чем правильно отформатированный код. Ghidra получает информацию из разных
источников и применяет алгоритмический подход, чтобы определить самый подходящий способ форматирования данных
диз­ассемблером. Например:
► типы и размеры данных можно вывести из того, какие
регистры используются. Если команда загружает данные из памяти в 32-разрядный регистр, значит, в памяти хранится 4-байтовый тип данных (хотя мы, возможно,
и не сумеем отличить 4-байтовое целое от 4-байтового
указателя);

Управление дизассемблированием

203

► для назначения типов данных параметрам функции
можно использовать прототипы функций. Для этой цели
в состав Ghidra входит обширная библиотека прототипов функций. Передаваемые функции параметры анализируются в попытке связать параметр с местоположением в памяти. Если такую связь удается установить, то
соответствующему адресу в памяти можно сопоставить
тип данных. Рассмотрим функцию, единственным параметром которой является указатель на CRITICAL_SECTION
(тип данных в Windows API). Если Ghidra сумеет определить адрес, переданный при вызове этой функции,
то этот адрес можно будет пометить как объект типа
CRITICAL_SECTION;
► анализ последовательности байтов может дать информацию о вероятных типах данных. Именно это происходит,
когда в двоичном файле ищется строковое содержимое. Если
встретилась длинная последовательность ASCII-символов,
то разумно предположить, что это массив символов.
В нескольких следующих разделах мы обсудим некоторые
простые преобразования, применимые к данным в листинге
дизассемблера.

Задание типов данных
Ghidra поддерживает размер данных и спецификатор типа.
Наиболее употребительные спецификаторы – byte, word, dword
и qword, представляющие 1-, 2-, 4- и 8-байтовые данные соответственно. Чтобы задать или изменить тип данных, щелкните
правой кнопкой мыши по строке листинга, содержащей данные (т. е. не команду), и выберите тип из меню, показанного
на рис. 7.17.

204

ГЛАВА 7

Рис. 7.17. Меню типов данных
Этот список позволяет сразу изменить форматирование
и размер выбранного элемента данных, выбрав его тип. Пункт
Cycle (Перебрать) позволяет быстро перебрать группу родственных типов данных, например числовые, символьные и с
плавающей точкой, как показано на рис. 7.18 (вместе с соответствующими горячими клавишами). Например, повторно нажимая F, мы будем перебирать типы float и double, потому что
только они и входят в эту группу.

Управление дизассемблированием

205

Рис. 7.18. Группы родственных типов
По мере изменения типа размер элементов данных может
уменьшаться, увеличиваться или оставаться без изменения.
Если размер элемента не изменяется, то единственное видимое изменение – смена формата. Если размер элемента уменьшается, например с ddw (4 байта) до db (1 байт), то оставшиеся
байты (в данном случае 3) становятся неопределенными. Если
размер данных увеличивается, то Ghidra предупредит о конф­
ликте и подскажет, как его разрешить. Пример, относящийся
к размеру массива, показан на рис. 7.19.

Рис. 7.19. Пример объявления массива и предупреждения
(сообщение говорит, что при вводе значения, большего 200,
существующие данные будут затерты)

Работа со строками
Команда SearchFor Strings (ПоискСтрок) открывает диалоговое окно, показанное на рис. 7.20, в котором можно задать
критерий поиска строки. Большинство полей не нуждаются
в объяснении, но обратим внимание на уникальную возможность Ghidra – ассоциирование модели слова с поиском. Модель слова (Word Model) позволяет определить, считается ли
конкретная строка словом в данном контексте. Модели слов
обсуждаются в главе 13.
Результаты поиска отображаются в окне, показанном на
рис. 7.21. Результаты последующих поисков будут отображаться во вкладках того же окна, а в полосе заголовка будет показана временная метка каждого поиска, чтобы в них было проще
ориентироваться.
206

ГЛАВА 7

Рис. 7.20. Диалоговое окно поиска строки

Рис. 7.21. Окно результатов поиска
Управление дизассемблированием

207

Значки в левом столбце таблицы результатов показывают статус определения (от «не определена» до «конфликтует»). Смысл значков описан на рис. 7.22. Чтобы показать или
скрыть строки из любой категории, переключите состояние соответствующего значка в полосе заголовка.
Значок Определение
Строка уже определена (ипотому присутствует вокне определенных
строк). Такие строки обычно являются целями перекрестных ссылок
Строка неопределена. Строка неявляется целью перекрестной ссылки,
абайты обычно являются отдельными шестнадцатеричными значениями
Часть строки была определена. Обычно это строка, для которой
ранее определенная строка является подстрокой
Строка конфликтует (перекрывается) счем-то ранее определенным,
например командами или данными

Рис. 7.22. Определения значков статуса строки
Значки позволяют легко идентифицировать элементы лис­
тинга, еще не определенные как строки, и превратить их
в строку или массив символов, для чего нужно их выделить
и нажать кнопку Make String (Сделать строкой) или Make
Char Array (Сделать массивом). Вновь определенные объекты
будут отображаться в окне определенных строк, которое обсуждается в одноименном разделе главы 5.

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

208

ГЛАВА 7

DAT_004195a4 XREF[1]: main:00411727(W)
004195a4 undefined4 ??
004195a8 ?? ??
004195a9 ?? ??
004195aa ?? ??
004195ab ?? ??
004195ac ?? ??
004195ad ?? ??
004195ae ?? ??
004195af ?? ??
004195b0 ?? ??
004195b1 ?? ??
004195b2 ?? ??
004195b3 ?? ??
004195b4 ?? ??
004195b5 ?? ??
004195b6 ?? ??

Ghidra может сгруппировать соседние определения данных
в одно определение массива. Чтобы создать массив, выберите
первый элемент и выполните команду DataCreate Array
(ДанныеСоздать массив) из контекстного меню (клавиша [). Будет предложено ввести количество элементов в массиве или согласиться со значением, предложенным Ghidra
по умолчанию. (Если выбран диапазон данных, а не одно
значение, то Ghidra вычислит размер массива по количеству
выделенных данных.) По умолчанию тип и размер элементов
массива определяются на основе типа первого выделенного
элемента. Массив будет представлен в сжатом формате, но
его можно раскрыть, если хочется увидеть отдельные элементы. Количество элементов в одной строке задается в меню
EditTool Options окна браузера кода. Более подробно
массивы обсуждаются в главе 8.

РЕЗЮМЕ
Вместе с предыдущей эта глава охватывает большинство
операций, которые чаще всего нужны пользователям Ghidra.
Манипулирование листингом дизассемблера позволяет объединить ваши знания со знаниями, собранными Ghidra на
Управление дизассемблированием

209

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

8

ТИПЫ ДАННЫХ
И СТРУКТУРЫ ДАННЫХ

Понимать типы и структуры данных, с которыми можно столкнуться при анализе двоичного файла, – совершенно необходимое умение
для обратной разработки. Данные, передаваемые
функции, – ключ к реконструкции сигнатуры функции (количества, типов и последовательности параметров).
Кроме того, объявленные и используемые внутри функции
типы и структуры данных дают дополнительную информацию о том, что функция делает. Все это аргументы в пользу глубокого понимания того, как типы и структуры данных
представляются в языке ассемблера и как ими можно манипулировать.
В этой главе мы уделим много времени этим вопросам, столь
критичным для успеха обратной разработки. Мы покажем,
как распознавать структуру данных в листинге дизассемб­
лера и как моделировать их в Ghidra. Далее мы продемонстрируем, как обширное поддерживаемое Ghidra собрание
сведений о структурах может сэкономить время в процессе

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

В ЧЕМ СМЫСЛ ЭТИХ ДАННЫХ?
Занимаясь обратной разработкой, вы, конечно, хотите понимать, в чем смысл данных, представленных в листинге дизассемблера. Чтобы связать с переменной какой-то тип, проще
всего понаблюдать, как эта переменная используется в качест­
ве параметра функции, о которой мы что-то знаем. В процессе анализа Ghidra делает все возможное, чтобы аннотировать
данные типами, если их можно вывести из использования вместе
с функциями, прототип которых известен.
Располагая информацией о библиотечных функциях,
Ghidra зачастую уже знает прототип функции. В таких случаях его легко увидеть, задержав мышь над именем функции
в окне листинга или дерева символов. Но даже если Ghidra
ничего не знает о последовательности параметров функции,
она может как минимум знать имя библиотеки, из которой
функция импортирована (см. папку Imports в окне дерева
символов). Если это так, то узнать о сигнатуре и поведении
функции лучше всего из страниц руководства и другой доступной документации по API. Если больше ничего не помогает, то Google вам в помощь.
Первый шаг на пути к пониманию поведения двоичной
программы – составление каталога библиотечных функций,
вызываемых программой. Если написанная на C программа
вызывает функцию connect, значит, она создает сетевое подключение. Если программа для Windows вызывает RegOpenKey,
значит, она обращается к реестру Windows. Но необходим дополнительный анализ, чтобы разобраться, как и почему вызываются эти функции.

212

ГЛАВА 8

Чтобы понять, как вызывается функция, нужно знать о ее
параметрах. Рассмотрим программу на C, которая вызывает функцию connect как часть процесса получения HTMLстраницы. Чтобы вызвать connect, программа должна знать
IP-адрес и номер порта сервера, на котором хранится страница,
а эту информацию дает библиотечная функция getaddrinfo.
Ghidra знает об этой функции и снабжает вызов комментарием, который дает нам дополнительную информацию в окне
листинга:
00010a30 CALL getaddrinfo int getaddrinfo(char * __name, c...

Узнать больше об этом вызове можно несколькими способами. Задержав мышь над сокращенным комментарием справа от команды, мы увидим, что Ghidra включила полный
прототип функции, чтобы сообщать нам о передаваемых ей
параметрах. Если задержать мышь над именем функции
в дереве символов, то прототип и переменные будут показаны во всплывающем окне. Или можно выбрать пункт Edit
Function из контекстного меню и получить ту же информацию в формате, допускающем редактирование, как показано
на рис. 8.1. Если этого не хватает, можете воспользоваться окном диспетчера типов данных, чтобы получить информацию
о конкретных параметрах, например о типе данных addrinfo.
Щелкнув по слову getaddrinfo в показанном выше листинге,
вы обнаружите, что сведения, показанные на рис. 8.1, повторены в самом листинге (в данном случае внутри функциипреобразователя, эту тему мы будем обсуждать на врезке
«Преобразователи» главы 10).

Типы данных и структуры данных

213

Рис. 8.1. Окно редактирования функции getaddrinfo
Наконец, чтобы сделать эти наблюдения, можно вообще
не заглядывать в окна дерева символов и диспетчера типов
данных, поскольку декомпилятор уже включил всю эту информацию в свое окно. Заглянув в окно декомпилятора, вы увидите, что Ghidra проставила имена полей в структуре addrinfo,
воспользовавшись информацией из загруженныхбиблиотек
типов. Ниже приведена выдержка из листинга декомпилятора: имена полей ai_family и ai_socktype позволяют сделать
вывод, что local_48 – структура, используемая для получения
информации, необходимой connect. В данном случае присваивание полю ai_family говорит, что используется IPv4-адрес
(2 – значение символической константы AF_INET), а значение
ai_socktype – что используется потоковый сокет (1 – значение
символической константы SOCK_STREAM):

214

ГЛАВА 8

local_48.ai_family = 2;
local_48.ai_socktype = 1;
local_10 = getaddrinfo(param_1,"www",&local_48,&local_18);

РАСПОЗНАВАНИЕ СТРУКТУР ДАННЫХ
В КОДЕ
Если примитивные типы данных часто умещаются в регистры
процессора или операнды команд, то составные типы – массивы и структуры – обычно требуют более сложных последовательностей команд для доступа к отдельным элементам. Преж­
де чем описывать, как Ghidra делает понятнее код, в котором
используются сложные типы данных, давайте посмотрим, как
этот код выглядит.

Доступ к элементам массива
Массивы – простейшая из составных структур данных с точки зрения размещения в памяти. Традиционно массивы располагаются
в непрерывных участках памяти и состоят из элементов одного
типа (однородная коллекция). Размер массива равен произведению числа элементов в нем на размер одного элемента. В языке C
минимальное число байтов, занятых массивом целых чисел
int array_demo[100];

вычисляется по формуле
int bytes = 100 * sizeof(int); // или 100 * sizeof(array_demo[0])

Для доступа к элементу массива нужно указать его индекс,
который может быть переменной или константой, как показано в следующих примерах:
 array_demo[20] = 15; // фиксированный индекс в массиве
for (int i = 0; i < 100; i++) {
 array_demo[i] = i; // переменный индекс в массиве
Типы данных и структуры данных

215

В предположении, что sizeof(int) равно 4 байтам, в предложении  производится доступ к целому числу, отстоящему
на 80 байт от начала массива, а в предложении  – доступ
к целым числам со смещениями 0, 4, 8, … 96 байт от начала
массива. Смещение в первом предложении вычисляется как
20 * 4. В большинстве случаев смещения во втором предложении должны вычисляться на этапе выполнения, поскольку
счетчик цикла i не фиксирован на этапе компиляции. Таким
образом, на каждой итерации цикла вычисляется произведение i * 4, равное смещению элемента в массиве.
Но способ доступа к элементу массива зависит не только от
типа индекса, но и от того, где в памяти выделено место для
массива.

Массивы в глобальной памяти
Если массив размещен в области глобальных данных программы (например, в секции .data или .bss), то его базовый адрес
известен на этапе компиляции, что позволяет компилятору
вычислить адреса всех элементов, для доступа к которым используется фиксированный индекс. Рассмотрим следующую
тривиальную программу, которая обращается к глобальному
массиву по фиксированному и переменному индексам.
int
int






}

global_array[3];
main(int argc, char **argv) {
int idx = atoi(argv[1]); // проверка выхода за границы массива
// для простоты опущена
global_array[0] = 10;
global_array[1] = 20;
global_array[2] = 30;
global_array[idx] = 40;

Чего в действительности ожидает C?
Для простоты мы сказали, что C ожидает получить целочисленный индекс в виде переменной или константы. На самом деле
подойдет любое выражение, значением которого является це-

216

ГЛАВА 8

лое число. Общее правило таково: «всюду, где можно использовать целое, можно использовать и выражение, результатом вычисления которого является целое». Разумеется, это относится
не только к целым. Язык C с готовностью примет любое предоставленное вами выражение и попытается привести его к ожидаемому типу. А что, если значение выходит за границы массива?
Разумеется, это причина многочисленных уязвимостей, допускающих эксплуатацию! Значения будут читаться или записываться
в область памяти, не принадлежащую массиву, или программа
просто «упадет», если целевой адрес вообще находится вне отведенной ей памяти.

Если дизассемблировать соответствующий зачищенный двоичный файл, то функция main будет содержать такой код:
...
00100657
0010065c
0010065f
00100669
00100673
0010067d
00100680
00100682
0010068a
00100691
...

CALL
MOV
MOV
MOV
MOV
MOV
CDQE
LEA
LEA
MOV

atoi
dword ptr
dword ptr
dword ptr
dword ptr
EAX,dword

[RBP + local_c],EAX
[DAT_00301018],10
[DAT_0030101c],20
[DAT_00301020],30
ptr [RBP + local_c]

RDX,[RAX*4]
RAX,[DAT_00301018]
dword ptr [RDX + RAX*1]=>DAT_00301018,40

Хотя в этой программе есть всего одна глобальная переменная (global_array), складывается впечатление, что в дизассемблированных строках  глобальных переменных три:
DAT_00301018, DAT_0030101c и DAT_00301020 соответственно. Однако команда LEA  загружает адрес глобальной переменной ,
который уже встречался раньше. В этом контексте, в сочетании
с вычислением смещения (RAX*4)  и масштабированным доступом к памяти , DAT_00301018, скорее всего, является базовым адресом глобального массива. Аннотированный операнд
=>DAT_00301018  дает нам базовый адрес массива, в который
записывается значение 40.

Типы данных и структуры данных

217

Что такое зачищенный файл?
В объектный файл компилятор должен включить достаточно информации, чтобы компоновщик мог сделать свою работу. Одна
из задач компоновщика – разрешение ссылок между объектными файлами, например при вызове функции, тело которой находится в другом файле. Для этого используется информация
о символах, сгенерированная компилятором. Часто компоновщик объ­единяет информацию из таблиц символов во всех объектных файлах и включает ее в результирующий исполняемый
файл. Для правильного выполнения эта информация не нужна,
но очень полезна для обратной разработки, поскольку Ghidra
(и другие инструменты, в частности отладчики) может использовать ее для восстановления имен и размеров функций и глобальных переменных.
Зачистка (stripping) исполняемого двоичного файла означает
удаление тех его частей, которые не нужны для работы. Для этой
цели используется командная утилита strip, применяемая для
постобработки исполняемого файла. Можно также задать флаги
компилятора и (или) компоновщика (-s в случае gcc/ld), которые
заставят их генерировать уже зачищенные двоичные файлы.
Помимо таблицы символов, strip может удалить всю отладочную информацию о символах, например об именах и типах локальных переменных, которая была включена в двоичный файл
на этапе его построения. В отсутствие информации о символах
инструменты обратной разработки должны применять алгоритмы для выявления функций и данных и присваивания им имен.

Из имен, присвоенных Ghidra, мы знаем, что глобальный
массив содержит по меньшей мере 12 байт, начиная с адреса
00301018. Компилятор использовал фиксированные индексы
(0, 1, 2) для вычисления фактических адресов соответствующих
элементов массива (00301018, 0030101c и 00301020), на которые
ссылаются глобальные переменные в командах ,  и . Исходя из значений, помещенных по этим адресам, мы можем
предположить, что в массиве хранятся 32-разрядные значения
(dword). Перейдя к соответствующим данным в листинге, мы
увидим такую картину:

218

ГЛАВА 8

DAT_00301018
00301018 ?? ??
00301019 ?? ??
0030101a ?? ??
0030101b ?? ??
DAT_0030101c
0030101c ?? ??
0030101d ?? ??
0030101e ?? ??
0030101f ?? ??
DAT_00301020
00301020 ?? ??
00301021 ?? ??
00301022 ?? ??
00301023 ?? ??

Вопросительные знаки показывают, что этот массив, скорее
всего, выделен в секции .bss и не инициализирован.
Массив легко распознать в листинге дизассемблера, если доступ к нему осуществляется по переменному индексу. Когда
для доступа к глобальному массиву используются постоянные
индексы, соответствующие элементы выглядят в листинге как
глобальные переменные. Но при использовании переменного
индекса становится виден базовый адрес массива (команда )
и размер одного элемента (команда ), поскольку необходимо вычислять смещение элемента с данным индексом. (Такие
операции масштабирования нужны для преобразования целочисленного индекса в C в байтовое смещение элемента массива
на ассемблере.)
Пользуясь операциями форматирования типов и массивов,
описанными в предыдущей главе (DataCreate Array), мы
можем отформатировать DAT_000301018 как массив трех целых
чисел и в результате получим команды доступа к именованному массиву по индексам вместо смещений:
00100660 MOV dword ptr [INT_ARRAY_00301018],10
0010066a MOV dword ptr [INT_ARRAY_00301018[1]],20
00100674 MOV dword ptr [INT_ARRAY_00301018[2]],30

Типы данных и структуры данных

219

По умолчанию Ghidra присвоила массиву имя INT_
ARRAY_00301018, которое включает тип массива и его начальный
адрес.

Обновление информации
о символах в комментариях
По мере того как вы идентифицируете типы данных, изменяете
имена символов и т. д., нужно позаботиться о том, чтобы ценные
комментарии, добавленные в листинг, не потеряли актуальности. Этой цели служат аннотации в комментариях, которые обновляются автоматически. Аннотация Symbol позволяет включать
ссылки на символы, которые обновляются при изменении символов (см. раздел «Аннотации» главы 7).

Рассмотрим окно декомпилятора до (рис. 8.2) и после (рис. 8.3)
создания массива. На рис. 8.2 важное предупреждение во второй строке – еще одно свидетельство в пользу того, что мы, возможно, имеем дело с массивом, а присваивание целых значений подкрепляет предположение о том, что элементы массива
имеют тип integer.

Рис. 8.2. Окно декомпилятора позволяет предположить, что
это массив
После создания массива целых чисел код в окне декомпилятора обновляется, теперь в нем используется новая переменная (рис. 8.3).
220

ГЛАВА 8

Рис. 8.3. Окно декомпилятора после объявления типа массива

Массивы в стеке
Компилятор не может знать абсолютного адреса массива, созданного в стеке как локальная переменная функции, поэтому
даже доступ по постоянному индексу требует вычислений на
этапе выполнения. Несмотря на различия, компиляторы часто
работают с массивами в стеке почти так же, как с массивами
в глобальной памяти.
Следующая программа – вариант предыдущей, только массив создан в стеке, а не в глобальной памяти.
int







}

main(int argc, char **argv) {
int stack_array[3];
int idx = atoi(argv[1]); // проверка выхода за границы массива
// для простоты опущена
stack_array[0] = 10;
stack_array[1] = 20;
stack_array[2] = 30;
stack_array[idx] = 40;

Адрес, по которому будет размещен массив stack_array, на
этапе компиляции неизвестен, поэтому компилятор не может
заранее вычислить адрес stack_array[2], как было сделано
Типы данных и структуры данных

221

для global_array[2]. Однако компилятор может вычислить относительное смещение любого элемента массива. Например,
смещение элемента stack_array[2] относительно начала массива равно 2*sizeof(int), и компилятор знает об этом. Если
компилятор решит разместить stack_array со смещением EBP0x18 относительно начала кадра стека, то может еще на этапе
компиляции вычислить выражение EBP-0x18+2*sizeof(int),
равное EBP-0x10, и избежать лишних арифметических операций для доступа к stack_array[2] на этапе выполнения. Это
хорошо видно в следующем листинге:
undefined main()
undefined AL:1
undefined4 Stack[-0xc]:4 local_c
undefined4 Stack[-0x10]:4 local_10
undefined4 Stack[-0x14]:4 local_14
undefined4 Stack[-0x18]:4 local_18
undefined4 Stack[-0x1c]:4 local_1c
undefined8 Stack[-0x28]:8 local_28
0010063a PUSH RBP
0010063b MOV RBP,RSP
0010063e SUB RSP,0x20
00100642 MOV dword ptr [RBP + local_1c],EDI
00100645 MOV qword ptr [RBP + local_28],RSI
00100649 MOV RAX,qword ptr [RBP + local_28]
0010064d ADD RAX,0x8
00100651 MOV RAX,qword ptr [RAX]
00100654 MOV RDI,RAX
00100657 MOV EAX,0x0
0010065c CALL atoi
00100661 MOV word ptr [RBP + local_c],EAX
00100664 MOV dword ptr [RBP + local_18],10
0010066b MOV dword ptr [RBP + local_14],20
00100672 MOV dword ptr [RBP + local_10],30
00100679 MOV EAX,dword ptr [RBP + local_c]
0010067c CDQE
0010067e MOV dword ptr [RBP + RAX*0x4 + -0x10],40
00100686 MOV EAX,0x0
0010068b LEAVE
0010068c RET

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

ГЛАВА 8

ременных  (local_c, local_10, local_14, local_18, local_1c
и local_28), а не массив трех целых чисел и целая переменная индекса. Две из этих локальных переменных (local_1c
и local_28) – параметры функции, argc и argv, сохраненные
для будущего использования .
Применение постоянного индекса скрывает присутствие
массива в стеке, потому что мы видим только присваивания
отдельным локальным переменным . Лишь операция умножения  дает намек на существование массива отдельных
элементов по 4 байта каждый. Разберем эту команду подробнее: RBP содержит базовый адрес указателя кадра; RAX*4 – это
индекс элемента массива (преобразованный функцией atoi
и сохраненный в local_c ), умноженный на размер элемента;
-0x10 – смещение начала массива от RBP.
Процесс преобразования локальных переменных в массив
несколько отличается от создания массива в секции данных
листинга. Поскольку структура информации в стеке связана
с первым адресом в функции, невозможно выбрать подмножест­
во переменных в стеке. Вместо этого нужно поместить курсор
на переменную в начале массива, local_18, выполнить команду Set Data Type, выбрать из контекстного меню пункт Array
и задать число элементов в массиве. Ghidra выдаст предупреждение о конфликте с локальными переменными, которые теперь становятся частью определения массива (см. рис. 8.4).

Рис. 8.4. Предупреждение о потенциальном конфликте
с определением массива в стеке
Если мы продолжим, несмотря на потенциальный конфликт,
то увидим массив в окне листинга:
Типы данных и структуры данных

223

...
00100664 MOV dword ptr [RBP + local_18[0]],10
0010066b MOV dword ptr [RBP + local_18[1]],20
00100672 MOV dword ptr [RBP + local_18[2]],30
...

Даже после того как массив определен, листинг декомпилятора на рис. 8.5 мало напоминает оригинальный исходный
код. Декомпилятор опустил статические присваивания элементам массива, потому что считает, что они никак не влияют
на возвращенный функцией результат. Вызов atoi и последующее присваивание остались, поскольку Ghidra не может
просчитать побочные эффекты atoi, однако Ghidra ошибочно
сочла результат atoi за четвертый элемент массива (local_c
в листинге дизассемблера и iVar1 в листинге декомпилятора).

Рис. 8.5. Окно декомпилятора, в котором показана функция со
всеми локальными переменными после определения массива

Массивы в куче
Для массивов в куче память выделяется с помощью таких
функций, как malloc (C) или new (C++). С точки зрения компилятора, основное отличие массива в куче заключается в том,
что базой для всех ссылок на него является адрес, возвращенный функцией выделения памяти. Следующая программа на
C размещает небольшой массив в куче:

224

ГЛАВА 8

int







}

main(int argc, char **argv) {
int *heap_array = (int*)malloc(3 * sizeof(int));
int idx = atoi(argv[1]); // проверка выхода за границы массива
// для простоты опущена
heap_array[0] = 10;
heap_array[1] = 20;
heap_array[2] = 30;
heap_array[idx] = 40;

Результат ее дизассемблирования несколько сложнее, чем
в предыдущих примерах:
undefined main()
undefined AL:1
undefined8 Stack[-0x10]:8 heap_array
undefined4 Stack[-0x14]:4 local_14
undefined4 Stack[-0x1c]:4 local_1c
undefined8 Stack[-0x28]:8 local_28
0010068a PUSH RBP
0010068b MOV RBP,RSP
0010068e SUB RSP,0x20
00100692 MOV dword ptr [RBP + local_1c],EDI
00100695 MOV qword ptr [RBP + local_28],RSI
00100699 MOV EDI,0xc
0010069e CALL malloc
001006a3 MOV qword ptr [RBP + heap_array],RAX
001006a7 MOV RAX,qword ptr [RBP + local_28]
001006ab ADD RAX,0x8
001006af MOV RAX,qword ptr [RAX]
001006b2 MOV RDI,RAX
001006b5 CALL atoi
001006ba MOV dword ptr [RBP + local_14],EAX
001006bd MOV RAX,qword ptr [RBP + heap_array]
001006c1 MOV dword ptr [RAX],10
001006c7 MOV RAX,qword ptr [RBP + heap_array]
001006cb ADD RAX,0x4
001006cf MOV dword ptr [RAX],20
001006d5 MOV RAX,qword ptr [RBP + heap_array]
001006d9 ADD RAX,0x8
001006dd MOV dword ptr [RAX],30
001006e3 MOV EAX,dword ptr [RBP + local_14]
001006e6 CDQE
001006e8 LEA RDX,[RAX*0x4]
001006f0 MOV RAX,qword ptr [RBP + heap_array]
Типы данных и структуры данных

225

001006f4
001006f7
001006fd
00100702
00100703

ADD  RAX,RDX
MOV dword ptr [RAX],40
MOV EAX,0x0
LEAVE
RET

Начальный адрес массива (возвращенный malloc в регист­
ре RAX) сохраняется в локальной переменной heap_array .
В этом примере, в отличие от предыдущих, любое обращение
к массиву начинается с чтения heap_array, чтобы получить
базовый адрес массива. Для обращения к heap_array[0], heap_
array[1] и heap_array[2] нужны смещения 0 , 4  и 8 байт 
соответственно. Доступ к массиву по переменному индексу
heap_array[idx] реализован несколькими командами, которые вычисляют смещение от начала массива, умножая индекс на размер элемента  и прибавляя результат к базовому
адресу .
У массивов в куче есть одна приятная особенность: количест­
во элементов можно вычислить, зная общий размер массива
и размер одного элемента. Параметр, переданный функции
выделения памяти (значение 12, переданное malloc ), содержит число байтов, выделяемых массиву. Разделив его на размер элемента (4 байта в этом примере, как видно из смещений
в командах  и масштабного коэффициента в команде ),
мы узнаем число элементов в массиве. В данном случае была
выделена память для массива, содержащего три элемента.
Декомпилятор также смог распознать массив, как видно по
рис. 8.6 (имя указателя на массив, puVar2, говорит, что это указатель на целое без знака – префикс pu).

226

ГЛАВА 8

Рис. 8.6. Функция, работающая с массивом в куче, в окне
декомпилятора
В этой функции, в отличие от той, что работала с массивом
в стеке, декомпилятор показывает присваивания элементам массива с постоянным индексом, хотя в других случаях исключил бы
их, потому что массив больше нигде не используется и не возвращается в качестве значения. Этот случай отличается, поскольку
присваивания – не просто манипулирование переменными в стеке: эта переменная является указателем на память в куче, запрошенную malloc. Запись через эту переменную производится
не в локальную переменную в стеке, а в кучу. Программа может
забыть указатель (адрес начала массива в куче) при выходе из
функции, но записанные значения останутся в памяти. (В этом
конкретном примере мы наблюдаем утечку памяти. Хотя такая
практика программирования достойна осуждения, она позволяет
нам продемонстрировать принципы работы с массивом в куче.)
Подводя итоги, отметим, что проще всего массив распознать,
когда некоторая переменная используется в качестве его индекса. Операция доступа к массиву, требующая умножения индекса
на размер элемента и последующего прибавления получившегося смещения к базовому адресу массива, очень хорошо видна
в листинге дизассемблера.
Типы данных и структуры данных

227

Доступ к полям структуры
Структуры в стиле C применяются для группировки нескольких элементов данных (часто разнородных) в составной тип.
В исходном коде обращение к полям структуры производится по
имени, а не по индексу. К сожалению, информативные имена
полей преобразуются компилятором в числовые смещения, так
что в листинге дизассемблера доступ к полю структуры очень
похож на доступ к элементам массива по постоянным индексам.
В последующих примерах мы будем использовать структуру
с пятью полями разных типов:
struct ch8_struct { //Размер Минимальное смещение Смещение по умолчанию
int field1; //
4
0
0
short field2; //
2
4
4
char field3; //
1
6
6
int field4; //
4
7
8
double field5; //
8
11
16
}; // Минимальный полный размер: 19 Размер по умолчанию: 24

Встретив определение структуры, компилятор запоминает,
сколько байтов требуется для каждого поля структуры, чтобы
можно было определить смещение каждого поля. Сумма размеров каждого поля дает минимальный объем памяти для структуры. Однако никогда не следует предполагать, что компилятор выделяет для структуры именно минимальную память.
По умолчанию компиляторы выравнивают поля структуры на
границы адресов в памяти, чтобы повысить эффективность чтения и записи полей. Например, 4-байтовые поля типа integer
выравниваются на границу, кратную четырем, а 8-байтовые
поля типа double – на границу, кратную восьми. В зависимости
от состава структуры компилятор может вставлять заполняющие байты, чтобы удовлетворить требования выравнивания,
а это значит, что фактический размер структуры будет больше,
чем сумма размеров полей. Смещения по умолчанию показаны
в одноименном столбце в комментариях к определению структуры выше, и, как мы видим, их сумма равна 24, а не 19.
Структуры можно уплотнять до минимального размера,
если воспользоваться соответствующими средствами компилятора. И Microsoft C/C++, и GNU gcc/g++ понимают прагму pack,
управляющую выравниванием полей структур. Компиляторы
228

ГЛАВА 8

GNU дополнительно понимают атрибут packed, управляющий
выравниванием на уровне отдельных структур. Если запросить
выравнивание на 1 байт, то компилятор уплотнит структуру до
минимума. Смещения для нашей структуры показаны в столбце «Минимальное смещение». (Отметим, что одни процессоры
просто работают быстрее, когда данные выравнены в соответствии с типом, тогда как другие возбуждают исключения, если
данные не выравнены на определенные границы.)
Памятуя обо всем вышесказанном, посмотрим, как структуры выглядят в откомпилированном коде. Как и в случае массивов, доступ к полям структуры выполняется путем сложения
базового адреса структуры со смещением поля. Но если смещения в массиве могут вычисляться во время выполнения по
значению индекса (поскольку размеры всех элементов массива
одинаковы), то смещения в структуре должны вычисляться на
этапе компиляции и в откомпилированном коде будут выглядеть как константы, т. е. почти так же, как при доступе к элементам массива по постоянному индексу.
Создание структур в Ghidra сложнее, чем создание массивов, поэтому мы рассмотрим его в следующем разделе, после
знакомства с несколькими примерами дизассемблированных
и декомпилированных структур.

Структуры в глобальной памяти
Как и в случае массивов, адреса структур в глобальной памяти известны на этапе компиляции. Это позволяет компилятору вычислить адрес каждого поля структуры и избежать
арифметических операций на этапе выполнения. Рассмотрим
следующую программу, которая обращается к структуре в глобальной памяти:
struct ch8_struct global_struct;
int main() {
global_struct.field1 = 10;
global_struct.field2 = 20;
global_struct.field3 = 30;
global_struct.field4 = 40;
global_struct.field5 = 50.0;
}
Типы данных и структуры данных

229

Если эта программа откомпилирована с выравниванием по
умолчанию, то после дизассемблирования мы, скорее всего,
увидим что-то такое:
undefined main()
undefined AL:1
001005fa PUSH RBP
001005fb MOV RBP,RSP
001005fe MOV dword ptr [DAT_00301020],10
00100608 MOV word ptr [DAT_00301024],20
00100611 MOV byte ptr [DAT_00301026],30
00100618 MOV dword ptr [DAT_00301028],40
00100622 MOVSD XMM0,qword ptr [DAT_001006c8]
0010062a MOVSD qword ptr [DAT_00301030],XMM0
00100632 MOV EAX,0x0
00100637 POP RBP
00100638 RET

Здесь нет никаких арифметических операций для доступа к полям структуры, и если нет исходного кода, невозможно с уверенностью утверждать, что тут вообще используется
структура. Поскольку все вычисления смещений проделаны
на этапе компиляции, программа выглядит так, будто обращается к пяти глобальным переменным, а не к пяти полям одной структуры. Вы, конечно, заметили сходство с показанным
выше доступом к массиву в глобальной памяти по постоянным
индексам.
На рис. 8.2 наличие одинаковых смещений вкупе со значениями позволило нам высказать предположение (оказавшееся
верным), что мы имеем дело с массивом. Здесь мы вправе заключить, что это не массив, потому что размеры переменных
не одинаковы (dword, word, byte, dword и qword), но достаточных
свидетельств в пользу структуры все же нет.

Структуры в стеке
Как и массивы, структуры, размещенные в стеке, трудно распознать на основе одних лишь смещений, и компилятор не дает
никаких подсказок. Если модифицировать предыдущую программу, разместив в стеке структуру, объявленную в main, то
дизассемблер сгенерирует такой код:
230

ГЛАВА 8

undefined main()
undefined AL:1
undefined8 Stack[-0x18]:8 local_18
undefined4 Stack[-0x20]:4 local_20
undefined1 Stack[-0x22]:1 local_22
undefined2 Stack[-0x24]:2 local_24
undefined4 Stack[-0x28]:4 local_28
001005fa PUSH RBP
001005fb MOV RBP,RSP
001005fe MOV dword ptr [RBP + local_28],10
00100605 MOV word ptr [RBP + local_24],20
0010060b MOV byte ptr [RBP + local_22],30
0010060f MOV dword ptr [RBP + local_20],40
00100616 MOVSD XMM0,qword ptr [DAT_001006b8]
0010061e MOVSD qword ptr [RBP + local_18],XMM0
00100623 MOV EAX,0x0
00100628 POP RBP
00100629 RET

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

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

231

Для демонстрации структур в куче модифицируем нашу
программу: объявим указатель внутри main и выделим блок
памяти, достаточный для размещения всей структуры:
int







}

main() {
struct ch8_struct *heap_struct;
heap_struct = (struct ch8_struct*)malloc(sizeof(struct ch8_struct));
heap_struct->field1 = 10;
heap_struct->field2 = 20;
heap_struct->field3 = 30;
heap_struct->field4 = 40;
heap_struct->field5 = 50.0;

Вот как выглядит дизассемблированный код:
undefined main()
undefined AL:1
undefined8 Stack[-0x10]:8 heap_struct
0010064a PUSH RBP
0010064b MOV RBP,RSP
0010064e SUB RSP,16
00100652 MOV EDI,24
00100657 CALL malloc
0010065c MOV qword ptr [RBP + heap_struct],RAX
00100660 MOV RAX,qword ptr [RBP + heap_struct]
00100664 MOV dword ptr [RAX],10
0010066a MOV RAX,qword ptr [RBP + heap_struct]
0010066e MOV word ptr [RAX + 4],20
00100674 MOV RAX,qword ptr [RBP + heap_struct]
00100678 MOV byte ptr [RAX + 6],30
0010067c MOV RAX,qword ptr [RBP + heap_struct]
00100680 MOV dword ptr [RAX + 8],40
00100687 MOV RAX,qword ptr [RBP + heap_struct]
0010068b MOVSD XMM0,qword ptr [DAT_00100728]
00100693 MOVSD qword ptr [RAX + 16],XMM0
00100698 MOV EAX,0x0
0010069d LEAVE
0010069e RET

В этом примере мы ясно видим точный размер и расположение полей в структуре. То, что структура занимает 24 байта, можно вывести из объема памяти, запрошенного malloc .
232

ГЛАВА 8

Структура содержит следующие поля с указанными ниже смещениями:
► 4-байтовое (dword) поле со смещением 0 ;
► 2-байтовое (word) поле со смещением 4 ;
► 1-байтовое поле со смещением 6 ;
► 4-байтовое (dword) поле со смещением 8 ;
► 8-байтовое (qword) поле со смещением 16 .
Исходя из использования команд с плавающей точкой
(MOVSD), мы можем сделать вывод, что поле типа qword на самом
деле имеет тип double.
Та же программа, откомпилированная с выравниванием структур на границу 1 байта, дает такой дизассемблированный код:
0010064a
0010064e
00100652
00100657
0010065c
00100660
00100664
0010066a
0010066e
00100674
00100678
0010067c
00100680
00100687
0010068b
00100693
00100698
0010069d
0010069e

PUSH
SUB
MOV
CALL
MOV
MOV
MOV
MOV
MOV
MOV
MOV
MOV
MOV
MOV
MOVSD
MOVSD
MOV
LEAVE
RET

RBP
RSP,16
EDI,19
malloc
qword ptr [RBP + local_10],RAX
RAX,qword ptr [RBP + local_10]
dword ptr [RAX],10
RAX,qword ptr [RBP + local_10]
word ptr [RAX + 4],20
RAX,qword ptr [RBP + local_10]
byte ptr [RAX + 6],30
RAX,qword ptr [RBP + local_10]
dword ptr [RAX + 7],40
RAX,qword ptr [RBP + local_10]
XMM0,qword ptr [DAT_00100728] =
qword ptr [RAX + 11],XMM0
EAX,0x0

Единственные отличия – меньший размер структуры (теперь
19 байт) и смещения, скорректированные с учетом другого выравнивания полей.
Независимо от выравнивания, использованного при компиляции программы, поиск структур, размещенных в куче, – самый простой способ определить размер и расположение полей.
Но имейте в виду, что многие функции не окажут вам любезТипы данных и структуры данных

233

ности и не станут обращаться к каждому полю, чтобы вам было
проще разобраться в устройстве структуры. Вместо этого придется прослеживать использование указателя на структуру
и отмечать, какие смещения встречались при разыменовании
указателя. Только так, постепенно, можно будет восстановить
все поля структуры. В разделе «Пример 3: автоматизированное
создание структуры» главы 19 мы увидим, как декомпилятор
может автоматизировать этот процесс.

Массивы структур
Некоторые программисты говорят, что красота составных
структур данных заключается в том, что они позволяют строить
сколь угодно сложные иерархически организованные структуры: массивы структур, структуры структур, структуры, содержащие массивы в качестве полей, и т. д. Приведенные выше
рассуждения о массивах и структурах равным образом применимы и к таким вложенным типам. Для примера рассмотрим
простую программу, в которой heap_struct указывает на массив
из пяти структур типа ch8_struct:
int



5);

}

main() {
int idx = 1;
struct ch8_struct *heap_struct;
heap_struct = (struct ch8_struct*)malloc(sizeof(struct ch8_struct) *
heap_struct[idx].field1 = 10;

Доступ к полю field1 сводится к умножению индекса на размер элемента массива (в данном случае на размер структуры)
и последующему прибавлению смещения нужного поля. Дизассемблированный код при этом выглядит следующим образом:
undefined main()
undefined AL:1
undefined4 Stack[-0xc]:4 idx
undefined4 Stack[-0x18]:8 heap_struct
0010064a PUSH RBP
0010064b MOV RBP,RSP

234

ГЛАВА 8

0010064e
00100652
00100659
0010065e
00100663
00100667
0010066a
0010066d
00100670
00100673
00100676
0010067a
0010067d
00100681
00100684
0010068a
0010068f
00100690

SUB
MOV
MOV
CALL
MOV
MOV
MOVSXD
MOV
ADD
ADD
SHL
MOV
MOV
ADD
MOV
MOV
LEAVE
RET

RSP,16
dword ptr
EDI,120
malloc
qword ptr
EAX,dword
RDX,EAX
RAX,RDX
RAX,RAX
RAX,RDX
RAX,3
RDX,RAX
RAX,qword
RAX,RDX
dword ptr
EAX,0

[RBP + idx],1

[RBP + heap_struct],RAX
ptr [RBP + idx]

ptr [RBP + heap_struct]
[RAX],10

Функция выделяет 120 байт  из кучи. Индекс массива
в RAX умножается на 24 с помощью последовательности операций , заканчивающейся командой SHL RAX, 3 , а затем результат прибавляется к начальному адресу массива . (Если
вам не очевидно, почему последовательность операций, начинающаяся в точке , эквивалентна умножению на 24, не расстраивайтесь. Такого рода последовательности обсуждаются
в главе 20.) Поскольку field1 – первое поле структуры, не нужно
никакого дополнительного смещения для вычисления конечного адреса, используемого в присваивании полю field1 .
Из всего вышесказанного мы можем заключить, что размер
элемента массива равен 24, что число элементов в массиве
равно 120 / 24 = 5 и что в каждом элементе массива имеется
4-байтовое (dword) поле со смещением 0. Этот короткий листинг
не дает достаточно информации о том, как распределены между полями остальные 20 байт структуры. Размер массива можно вывести еще проще, применив те же соображения к листингу декомпилятора на рис. 8.7 (шестнадцатеричное 0x18 равно
десятичному 24).

Типы данных и структуры данных

235

Рис. 8.7. Функция, работающая с массивом структур в куче,
в окне декомпилятора

СОЗДАНИЕ СТРУКТУР В GHIDRA
В предыдущей главе мы видели, как использовать средства
агрегирования массивов для сворачивания длинных списков
объявлений данных в одну строку листинга дизассемблера,
представляющую массив. В следующих разделах мы рассмот­
рим, какие возможности Ghidra предлагает, чтобы сделать
понятнее код, работающий со структурами. Наша цель – избавиться от загадочных ссылок на структуры вида [EDX + 10h],
заменив их чем-то более понятным, например [EDX + ch8_
struct.field_e].
Обнаружив, что программа манипулирует структурой данных, мы должны решить, хотим ли видеть в листинге имена
полей структуры или достаточно числовых смещений, разбросанных по всему листингу. Иногда Ghidra может распознать
использование структуры, определенной в стандартной биб­
лиотеке C или в Windows API, и воспользоваться своими знаниями о ней, чтобы преобразовать числовые смещения в символические имена полей. Это идеальный случай, потому что
на нашу долю остается гораздо меньше работы. Мы вернемся
к данной ситуации, когда немного лучше поймем, как Ghidra
вообще обращается с определениями структур.

236

ГЛАВА 8

Создание новой структуры
Если Ghidra ничего не знает о полях структуры, то мы можем
создать структуру, выделив данные и воспользовавшись контекстным меню. Выбрав команду DataCreate Structure
(ДанныеСоздать структуру) (или нажав Shift-[), мы увидим
окно создания структуры, показанное на рис. 8.8. Так как мы
выделили блок данных (определенных или неопределенных),
Ghidra попытается найти среди известных ей структур совпадающие по формату или по размеру. Мы можем выбрать в окне
одну из известных структур или создать новую. В этом примере
мы будем использовать рассмотренный выше пример структуры в глобальной памяти и создадим новую структуру ch8_struct.
После нажатия кнопки OK эта структура станет официально
признанным типом в окне диспетчера типов данных, и информация о ней распространится в другие окна браузера кода.

Рис. 8.8. Окно создания структуры
Рассмотрим, что происходит после создания структуры в окнах браузера кода, и начнем с окна листинга. Как было показано ранее в этой главе, листинг дизассемблера дает мало
подсказок о том, что мы, возможно, имеем дело со структурой,
Типы данных и структуры данных

237

потому что код изменяет ряд глобальных переменных, на первых взгляд не связанных между собой:
001005fa
001005fb
001005fe
00100608
00100611
00100618
00100622
0010062a
00100632
00100637
00100638

PUSH
MOV
MOV
MOV
MOV
MOV
MOVSD
MOVSD
MOV
POP
RET

RBP
RBP,RSP
dword ptr [DAT_00301020],10
word ptr [DAT_00301024],20
byte ptr [DAT_00301026],30
dword ptr [DAT_00301028],40
XMM0,qword ptr [DAT_001006c8]
qword ptr [DAT_00301030],XMM0
EAX,0
RBP

Когда мы перейдем к соответствующим элементам данных,
выберем диапазон (от 00301020 до 00301037) и создадим структуру, то увидим, что отдельные элементы теперь стали полями структуры с именем ch8_struct_00301020, получив имена,
в которых за префиксом field_ следует смещение от начала
структуры.
00401035
001005fb
001005fe
00100608
00100611
00100618
00100622
0010062a
00100632
00100637
00100638

POP
MOV
MOV
MOV
MOV
MOV
MOVSD
MOVSD
MOV
POP
RET

EBP
RBP,RSP
dword ptr [ch8_struct_00301020],10
word ptr [ch8_struct_00301020.field_0x4],20
byte ptr [ch8_struct_00301020.field_0x6],30
dword ptr [ch8_struct_00301020.field_0x8],40
XMM0,qword ptr [DAT_001006c8]
qword ptr [ch8_struct_00301020.field_0x10],XMM0
EAX,0
RBP

Это лишь одно из окон, которые изменяются после создания
структуры. Напомним, что окно декомпилятора выдало нам
полезное предупреждение о том, что мы, возможно, работаем
со структурой или массивом. После создания структуры это
предупреждение исчезает, и декомпилированный код становится больше похож на оригинальный код на C, как показано
на рис. 8.9.
238

ГЛАВА 8

Рис. 8.9. Окно декомпилятора после создания структуры

Объединения
Объединение (union) – конструкция, похожая на структуру. Основное различие между структурами и объединениями заключается
в том, что смещения всех полей структуры различны и каждому
полю отведена своя память, тогда как поля объединения перекрываются в памяти, поскольку смещение каждого равно нулю.
Поэтому все поля объединения совместно используют одну и ту
же область памяти. Окно редактора объединения в Ghidra похоже
на окно редактора структуры и внешне, и по функциональности.

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

Типы данных и структуры данных

239

Рис. 8.10. Вновь объявленная структура в окне диспетчера
типов данных и в окне ссылок

Редактирование полей структуры
В данный момент Ghidra представляет вновь созданную структуру в виде последовательности соседних неопределенных
байтов с перекрестными ссылками на каждое смещение, по которому обращается программа, а не последовательности определенных типов данных (распознанных по размеру и способу
использования каждого поля). Чтобы определить типы полей,
мы можем отредактировать структуру в окне листинга, щелк­
нув по ней правой кнопкой мыши и выбрав из меню подходящий тип. Или же можно отредактировать структуру в окне
диспетчера типов данных, дважды щелкнув по ней.
Если дважды щелкнуть по вновь созданной структуре в окне
диспетчера типов данных (рис. 8.10), то откроется окно редактора структуры (рис. 8.11), в котором показано 24 элемента
неопределенного типа, каждый длиной 1. Чтобы определить
количество элементов в структуре, а также их размеры и типы,
можно изучить листинг дизассемблера или воспользоваться
ответами в листинге декомпилятора, показанном на рис. 8.9.

240

ГЛАВА 8

Рис. 8.11. Окно редактора структуры
Оригинальный листинг декомпилятора, ассоциированный
с нашей новой структурой ch8_struct_00301020, показывает
пять элементов с именами, включающими два целых числа.
Первое число – это смещение от начала структуры, второе –
количество занятых полем байтов, служащее хорошим индикатором размера поля. Пользуясь этой информацией (и худобедно осмысленными именами некоторых полей), мы можем
обновить окно редактора структуры, как показано на рис. 8.12.
Прокручиваемая область Byte Offset/Component Bits (Смещение байта / Биты компонента) в окне редактора дает визуальное представление структуры. Во время редактирования
структуры окно декомпилятора (слева на рис. 8.12), окно лис­
тинга и другие связанные окна также обновляются.

Типы данных и структуры данных

241

Поскольку поле field_c состоит из одного символа, декомпилятор преобразовал целое значение 30 в ASCII-символ с кодом
30 (0x1e), который соответствует непечатаемому управляющему символу (RS). В редакторе структуры включены байты заполнения (мнемонически обозначенные ??) для выравнивания
полей, так что смещение каждого поля и общий размер структуры (24 байта) совпадают со значениями из рассмотренных
выше примеров.

Рис. 8.12. Окна декомпилятора и редактора структуры после
редактирования структуры

Наложение структур
Мы видели, как использовать известные определения структур и как создавать новые, чтобы наложить на область памяти
конкретную структуру полей. Мы также видели, что это определение распространяется на другие окна браузера кода. Загадочные ссылки вида [EBX+8] становятся более понятными, если
преобразовать числовые смещения полей в символические, на242

ГЛАВА 8

пример [EBX+ch8_struct.field_d], особенно если символическим
ссылкам дать осмысленные имена. Благодаря иерархической
нотации Ghidra точно показывает, к какой структуре и к какому полю в ней производится обращение.
Библиотека известных структур Ghidra хранит информацию,
собранную путем разбора общеупотребительных заголовочных
файлов C. В описании структуры указан ее полный размер,
имена и размеры полей и смещение каждого поля от начала
структуры. Описания структур можно использовать, даже если
с ними ничего не связано в секции данных, что особенно полезно при работе с указателями на структуры.
Встретив ссылку вида [reg+N] (например, [RAX+0x12]), где
reg – название регистра, а N – небольшая константа, имейте
в виду, что reg используется как указатель, а N – смещение от
начала области памяти, на которую указывает reg. Это стандартный способ доступа к полям структуры – reg указывает
на начало структуры, а N выбирает поле со смещением N. При
определенных обстоятельствах Ghidra с вашей помощью может уточнить такую ссылку на память, показав тип структуры
и конкретное поле в ней.
Рассмотрим 32-разрядную версию примера из начала этой
главы, в котором мы запрашивали HTTP-страницу от сервера.
Запрос посылает функция get_page. Для этой версии двоичного
файла Ghidra утверждает, что функция получает три параметра
в стеке. Эти параметры выглядят в окне листинга следующим
образом:
undefined get_page(undefined4 param_1, undefined param_2...
undefined AL:1
undefined4 Stack[0x4]:4 param_1
undefined Stack[0x8]:1 param_2
undefined4 Stack[0xc]:4 param_3

Окно декомпилятора показывает, что param_3 используется
в сочетании со смещениями при вызове connect:
iVar1=connect(local_14,*(sockaddr **)(param_3+20),*(socklen_t*)(param_3+16));

Типы данных и структуры данных

243

Проследив последовательность вызовов и возвращаемые
значения, мы приходим к выводу, что param_3 – указатель на
структуру addrinfo, поэтому заменяем param_3 на addrinfo* (нажав клавиши Ctrl-L в окне листинга или декомпилятора). Декомпилированное предложение с участием param_3 будет заменено гораздо более информативным:
iVar1 = connect(local_14, param_3->ai_addr, param_3->ai_addrlen);

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

ВВЕДЕНИЕ В ОБРАТНУЮ РАЗРАБОТКУ
КОДА НА C++
Классы C++ – это объектно-ориентированное расширение
структур C, поэтому будет логично завершить наше обсуждение структур данных обзором особенностей откомпилированного кода на C++. Детальное рассмотрение C++ выходит за
рамки этой книги. Здесь мы лишь попытаемся осветить наиболее важные моменты и некоторые различия между компиляторами Microsoft C++ и GNU g++.
Напомним, что уверенное владение языком C++ сильно поможет в понимании откомпилированного кода, написанного
на этом языке. Такие объектно-ориентированные концепции,
как наследование и полиморфизм, трудно освоить даже на
уровне исходного кода. А попытка разобраться в них на уровне
ассемблера, не понимая, как они устроены в исходном коде,
практически обречена на провал.

244

ГЛАВА 8

Указатель this
Указатель this передается всем нестатическим функциям-членам в C++. При вызове такой функции this инициализируется
указателем на объект, от имени которого вызывается функция.
Рассмотрим следующие вызовы функций на C++:
// object1, object2 и *p_obj имеют один и тот же тип.
object1.member_func();
object2.member_func();
p_obj->member_func();

В этих трех вызовах member_func this принимает соответственно значения &object1, &object2 и p_obj.
Проще всего рассматривать this как скрытый первый параметр, передаваемый всем нестатическим функциям-членам.
Как было отмечено в главе 6, компилятор Microsoft C++ применяет соглашение о вызове thiscall и передает this в регистре ECX
(x86) или RCX (x86-x64). Компилятор GNU g++ обращается с this
так, как если бы это был первый (самый левый) параметр нестатической функции-члена. В 32-разрядной ОС Linux x86 адрес
объекта, от имени которого вызывается функция, помещается на
вершину стека перед вызовом функции. В ОС Linux x86-64 this
передается как первый регистровый параметр в регистре RDI.
С точки зрения обратной разработки, копирование адреса
в регистр ECX непосредственно перед вызовом функции может
свидетельствовать о двух вещах. Во-первых, файл был откомпилирован компиляторомMicrosoft C++. Во-вторых, функция,
скорее всего, является членом класса. Если один и тот же адрес
передается двум или более функциям, то можно заключить,
что все они принадлежат одной иерархии классов.
Внутри функции использование ECX до инициализации
озна­чает, что инициализировать ECX должна была вызывающая сторона (вспомните обсуждение активности во врезке
«Регистровые параметры» главы 6), а это вероятный признак
того, что функция является членом класса (хотя, возможно,
она просто следует соглашению о вызове fastcall). Кроме того,
если функция-член передает this другим функциям, то они,
вероятно, являются членами того же класса.
Типы данных и структуры данных

245

В коде, откомпилированном GNU g++, вызовы функций-членов выделяются не так явственно, потому что this выглядит
как любой другой первый параметр. Однако никакая функция, не принимающая в первом параметре указатель, точно
не является членом класса.

Виртуальные функции и vf-таблицы
Виртуальные функции – это механизм реализации полиморфного поведения в программах на C++. Для каждого класса, содержащего виртуальные функции (или его подкласса),
компилятор генерирует таблицу указателей на все виртуальные функции класса. Они называются vf-таблицами (или
v-таблицами). В любой экземпляр класса, содержащего виртуальные функции, включается дополнительный член, содержащий указатель на vf-таблицу класса. Этот указатель на
vf-таблицу является первым членом данных, и конструктор
класса на этапе выполнения инициализирует его указателем
на подходящую vf-таблицу. Если объект вызывает виртуальную функцию, то функция, которая должна получить управление, ищется в vf-таблице объекта. Именно так производится
разрешение обращений к виртуальным функциям во время
выполнения.
Несколько примеров помогут лучше понять использование
vf-таблицы. Рассмотрим следующие определения классов C++:
class BaseClass {
public:
BaseClass();
 virtual void vfunc1() = 0;
virtual void vfunc2();
virtual void vfunc3();
virtual void vfunc4();
private:
int x;
int y;
};
class SubClass : public BaseClass{
public:
SubClass();
 virtual void vfunc1();

246

ГЛАВА 8





};

virtual void vfunc3();
virtual void vfunc5();
private:
int z;

Здесь класс SubClass наследует классу BaseClass . BaseClass
содержит четыре виртуальные функции , а SubClass – пять 
(четыре унаследованные от BaseClass, из которых две переопределены, и одну новую, vfunc5). В BaseClass функция vfunc1 является чисто виртуальной, о чем говорит конструкция = 0 
в ее объявлении. У чисто виртуальных функций нет реализации в том классе, где они объявлены, они должны быть переопределены в подклассе, чтобы этот подкласс считался конкретным. Иными словами, не существует функции с именем
BaseClass::vfunc1, и пока какой-нибудь подкласс не предоставит
реализацию, нельзя создать никаких объектов. Класс SubClass
предоставляет такую реализацию, поэтому мы можем создавать объекты SubClass. В объектно-ориентированной терминологии BaseClass::vfunc1 называется абстрактной функцией,
а BaseClass – абстрактным базовым классом (это означает, что
класс неполон, т. е. его объекты нельзя создать в силу отсутствия
реализации по меньшей мере одной функции).
На первый взгляд кажется, что BaseClass содержит два члена данных, а SubClass – три. Но напомним, что любой класс,
содержащий виртуальные функции, явно или благодаря наследованию содержит также указатель на vf-таблицу. Поэтому в откомпилированной реализации BaseClass имеется три
члена данных, а в объектах SubClass их четыре. В обоих случаях первым членом данных является указатель на vf-таблицу.
В SubClass указатель на vf-таблицу унаследован от BaseClass,
а не создан специально для SubClass. Все это показано на упрощенной схеме размещения в памяти на рис. 8.13, где динамически выделена память для одного объекта SubClass. В процессе
создания объекта указатель на vf-таблицу в нем инициализируется адресом правильной таблицы (в данном случае класса
SubClass).

Типы данных и структуры данных

247

vf-таблица BaseClass
&purecall
BaseClass *bc = new SubClass();

&BaseClass::vfunc2
&BaseClass::vfunc3
&BaseClass::vfunc4
vf-таблица SubClass

p_vftable

&SubClass::vfunc1

X

&BaseClass::vfunc2

Y

&SubClass::vfunc3

Z

&BaseClass::vfunc4

Данные в™куче

&SubClass::vfunc5
Данные, допускающие
только чтение
(.rdata/.rodata)

Рис. 8.13. Простое размещение vf-таблицы в памяти
vf-таблица класса SubClass содержит два указателя на
функции,
принадлежащие
BaseClass (BaseClass::vfunc2
и BaseClass::vfunc4), потому что SubClass не переопределяет их,
а наследует от BaseClass. На примере vf-таблицы класса BaseClass
показано, как обрабатываются чисто виртуальные функции.
Поскольку у чисто виртуальной функции BaseClass::vfunc1
нет реализации, не существует адреса, который можно было бы
сохранить в элементе vf-таблицы для vfunc1. В таких случаях
компиляторы вставляют адрес функции, которая в библиотеках
Microsoft называется purecall, а в библиотеках GNU – __cxa_
pure_virtual. Теоретически такие функции никогда не должны
вызываться, но бывают случаи, когда они все же вызываются,
и тогда программа завершается аварийно.
При работе с классами в Ghidra нужно учитывать наличие
указателя на vf-таблицу. Поскольку классы C++ – это расширения структур C, мы можем воспользоваться имеющимися
в Ghidra средствами определения структур для задания описания классов C++. Для полиморфных классов нужно включать
указатель на vf-таблицу первым полем, а также учитывать его
в общем размере объекта. Это особенно наглядно видно при
наблюдении за динамическим выделением объекта с помощью
оператора new, которому передается размер с учетом всех явно
объявленных полей класса (и его базовых классов) и указателя
на vf-таблицу. В следующем примере объект SubClass создается
248

ГЛАВА 8

динамически, и его адрес сохраняется в указателе на BaseClass.
Затем этот указатель передается функции (call_vfunc), которая использует его для вызова vfunc3:
void call_vfunc(BaseClass *bc) {
bc->vfunc3();
}
int main() {
BaseClass *bc = new Subclass();
call_vfunc(bc);
}

Поскольку vfunc3 – виртуальная функция, а bc указывает на
объект класса SubClass, компилятор должен гарантировать, что
вызывается функция SubClass::vfunc3. В следующем листинге
дизассемблера 32-разрядной версии call_vfunc, откомпилированной Microsoft C++, демонстрируется, как разрешается вызов виртуальной функции:
undefined __cdecl call_vfunc(int * bc)
undefined AL:1
int * Stack[0x4]:4 bc
004010a0 PUSH EBP
004010a1 MOV EBP,ESP
004010a3 MOV EAX,dword ptr [EBP + bc]
004010a6 MOV EDX,dword ptr [EAX]
004010a8 MOV ECX,dword ptr [EBP + bc]
004010ab MOV AX,dword ptr [EDX + 8]
004010ae CALL EAX
004010b0 POP EBP
004010b1 RET

Указатель на vf-таблицу (адрес vf-таблицы SubClass) читается из структуры и сохраняется в EDX . Затем указатель this
помещается в ECX . Далее к vf-таблице производится доступ
по индексу для чтения третьего указателя (в данном случае –
адреса SubClass::vfunc3) в регистр EAX . Наконец, вызывается
виртуальная функция .
Операция доступа по индексу к vf-таблице  выглядит очень
похоже на операцию ссылки на структуру. На самом деле они
и не отличаются, поэтому можно определить новые структуры
Типы данных и структуры данных

249

для класса и его vf-таблицы (щелкнуть правой кнопкой мыши
в окне диспетчера типов данных), а затем использовать их (см.
рис. 8.4), чтобы сделать листинги дизассемблера и декомпилятора понятнее.

Рис. 8.14. Класс SubClass и структура SubClass_vftable
в окне диспетчера типов данных
Окно декомпилятора со ссылками на новые структуры показано на рис. 8.15.

Рис. 8.15. Определенные для SubClass структуры в окне
декомпилятора
Прямые ссылки на vf-таблицу класса встречаются только
в двух случаях: в конструкторах класса и в его деструкторе.
Найдя vf-таблицу, вы можете использовать средства работы
с перекрестными ссылками в Ghidra (см. главу 9), чтобы быст­
ро отыскать все конструкторы и деструктор соответствующего
класса.

250

ГЛАВА 8

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

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

Класс хранения объекта определяет, в какой момент вызывается его конструктор1. Конструкторы глобальных и статически выделенных объектов (со статическим классом хранения) вызываются на этапе инициализации программы еще
до входа в функцию main. Конструкторы объектов, созданных
в стеке (с автоматическим классом хранения), вызываются,
когда объект входит в область видимости внутри функции,
в которой объявлен. Часто это происходит сразу после входа
в функцию. Но если объект объявлен во вложенном блоке,
то его конструктор вызывается только в момент входа в этот
блок, если такое вообще случается. Если объект динамически
создается в куче, то процесс создания состоит из двух этапов:
сначала вызывается оператор new, который выделяет память
для объекта, а затем конструктор, который инициализирует
объект. Microsoft C++ гарантирует, что результат вызова new
1

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

251

не равен null перед вызовом конструктора, а GNU g++ таких
гарантий не дает.

Что такое new?
Оператор new служит для динамического выделения памяти в C++,
как функция malloc в C. Он выделяет память из кучи и позволяет
программе запрашивать память в процессе выполнения. Оператор new встроен в язык C++, тогда как malloc – обычная функция
в стандартной библиотеке. Напомним, что C – подмножество C++,
поэтому malloc можно встретить и в программе на C++. Самое заметное различие между malloc и new заключается в том, что вызов
new для объекта класса неявно приводит к вызову его конструктора,
тогда как память, возвращенная malloc, не инициализирована.

Во время выполнения конструктора имеет место следующая
последовательность действий.
1. Если у класса имеется базовый класс, то вызывается конструктор базового класса.
2. Если в классе имеются виртуальные функции, то указатель
на vf-таблицу инициализируется адресом vf-таблицы класса. При этом может быть перезаписан указатель, инициализированный конструктором базового класса, и это именно то, что нам нужно.
3. Если в классе имеются данные-члены, которые сами являются объектами, то вызываются конструкторы каждого из
таких членов.
4. Наконец, выполняется конструктор класса, написанный автором этого класса.
С точки зрения программиста, у конструкторов нет типа возвращаемого значения, и они ничего не возвращают. Некоторые
компиляторы на самом деле возвращают указатель this, чтобы впоследствии его можно было использовать в вызывающей
функции, но это деталь реализации компилятора, а программист не имеет доступа к возвращенному значению.
Деструкторы, как следует из самого названия, вызываются в конце времени жизни объекта. Деструкторы глобальных
и статических объектов вызываются из кода очистки, который
252

ГЛАВА 8

выполняется сразу после возврата из функции main. Деструктор объекта, созданного в стеке, вызывается, когда объект покидает область видимости. Деструктор объекта, созданного
в куче, вызывается с помощью оператора delete непосредственно перед освобождением принадлежащей объекту памяти.
Действия деструкторов повторяют действия конструкторов,
только выполняются в обратном порядке.
1. Если в классе имеются виртуальные функции, то указатель
на vf-таблицу в объекте переустанавливается, так чтобы он
указывал на vf-таблицу класса объекта. Это необходимо на
случай, если подкласс перезаписал указатель на vf-таблицу
в процессе создания объекта.
2. Выполняется написанный программистом код деструктора.
3. Если в классе имеются данные-члены, которые сами являются объектами, то вызывается деструктор каждого из них.
4. Наконец, если объект имеет базовый класс, то вызывается
деструктор базового класса.
Понимая, как вызываются конструкторы и деструктор базового класса, мы сможем проследить иерархию наследования по
цепочке вызовов функций базовых классов.

Мне кажется, что ты перегружен
Перегруженными называются функции с одинаковым именем,
но разными параметрами. C++ требует, чтобы варианты перегруженной функции отличались последовательностью и (или) количеством типов параметров. Иными словами, хотя имена могут
совпадать, прототипы должны быть уникальны, и тело каждой
перегруженной функции можно однозначно определить в дизассемблированном двоичном файле. Не следует путать перегруженные функции с функциями типа printf, которые принимают
переменное число аргументов, но имеют лишь одно тело.

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

253

женных функций, компиляторы включают в имя функции дополнительные символы, в которых закодирована разного рода
информация о функции: пространство имен, которому она принадлежит (если таковое существует), или объемлющий класс
(если имеется) и последовательность параметров (типы и порядок следования) функции.
Декорирование имен – деталь реализации компилятора C++
и потому не является частью спецификации языка. Неудивительно, что поставщики компиляторов разработали собственные, часто несовместимые схемы декорирования имен. По счастью, Ghidra понимает схемы декорирования, применяемые
компиляторами Microsoft C++ и GNU g++ v3 (и последующими
версиями), а также некоторыми другими. Ghidra показывает
имена вида FUN_адрес вместо декорированных. Декорированные имена несут ценную информацию о сигнатурах функций,
и Ghidra включает ее в окно таблицы символов и распространяет на связанные окна, в частности на окно листинга. (Чтобы определить сигнатуру функции, не имея декорированного
имени, нужно было бы проделать трудоемкий анализ данных,
входящих и выходящих из функции.)

Идентификация типа во время выполнения
C++ предоставляет операторы для определения (typeid) и проверки (dynamic_cast) типа объекта во время выполнения. Для поддержки этих операций компилятор C++ должен включить информацию о типе для каждого полиморфного класса в двоичный файл
программы. Если выполняется операция typeid или dynamic_cast,
то библиотечные функции находят информацию о типе, чтобы
определить динамический тип полиморфного объекта. К сожалению, идентификация типа во время выполнения (RTTI) также
является деталью реализации компилятора, а не частью стандарта языка, и разные компиляторы делают это по-разному.
Мы кратко обсудим сходство и различие реализации RTTI
компиляторами Microsoft C++ GNU g++. Точнее, мы опишем,
как найти информацию RTTI и как из нее узнать имя класса,
к которому эта информация относится. Читателей, желающих
получить дополнительные сведения о реализации RTTI в компиляторе Microsoft, отсылаем к руководствам, перечисленным
254

ГЛАВА 8

в конце этой главы. В частности, в них описано, как обойти
иерархию наследования класса, в т. ч. множественного.
Рассмотрим простую программу, в которой используется полиморфизм:
class abstract_class {
public:
virtual int vfunc() = 0;
};
class concrete_class : public abstract_class {
public:
concrete_class(){};
int vfunc();
};
int concrete_class::vfunc() {return 0;}
 void print_type(abstract_class *p) {
cout ref_it,dword ptr [EDX]
0040102b MOV [write_it],EAX
00401030 CALL callflow
00401035 CMP dword ptr [read_it],3
0040103c JNZ LAB_0040104a
0040103e MOV dword ptr [write_it],2
00401048 JMP LAB_00401054
LAB_0040104a XREF[1]:0040103c(j)
0040104a MOV dword ptr [write_it],1
LAB_00401054 XREF[1]: 00401048(j)
00401054 CALL callflow
00401059 XOR EAX,EAX
0040105b MOV ESP,EBP
0040105d POP EBP
0040105e RET

Листинг 9.1. Результат дизассемблирования функции main из
файла simple_flows.exe
После каждой команды, кроме JMP  и RET , выполняется
следующая за ней команда. С командами вызова функций,
например CALL , связан поток вызова, показывающий, что
управление передается вызываемой функции. Потоку вызова
соответствует XREF в целевой функции (конечной точке потока). В листинге 9.2 показан результат дизассемблирования
функции callflow, на которую есть ссылка в листинге 9.1.

266

ГЛАВА 9

undefined __stdcall callflow(void)
undefined AL:1
callflow XREF[4]: 0040010c(*),
004001e4(*),
main:00401030(c),
main:00401054(c)
00401000 PUSH EBP
00401001 MOV EBP,ESP
00401003 POP EBP
00401004 RET

Листинг 9.2. Результат дизассемблирования функции callflow

Лишние XREF?
Иногда мы замечаем в листинге аномальные, на первый взгляд,
вещи. В листинге 9.2 есть две XREF типа указателя, 0040010c(*)
и 004001e4(*), объяснить которые нелегко. Обе XREF, которые можно проследить до вызовов callflow в main, затруднений не вызывают. Но как быть с двумя другими? Оказывается, что это интересный
артефакт данного конкретного кода. Программа была откомпилирована для Windows, в результате получился PE-файл, а эти две
аномальные XREF ведут нас в заголовок PE в секции Headers лис­
тинга. Ниже показаны оба адреса, указанных в ссылках:
0040010c 00 10 00 00 ibo32 callflow BaseOfCode
...
004001e4 00 10 00 00 ibo32 callflow VirtualAddress
Почему на эту функцию есть ссылка в заголовке PE? Поиск
в Google поможет понять, что происходит: callflow – просто самая первая функция в текстовой секции, а два поля PE-файла
косвенно ссылаются на начало текстовой секции, отсюда и неожиданные XREF, относящиеся к функции callflow.

В этом примере мы видим, что callflow вызывается из main
дважды: один раз по адресу 00401030, другой – по адресу
00401054. Перекрестные ссылки, являющиеся результатом вызова функции, можно отличить по суффиксу (c). В перекрестной ссылке отображается как адрес, по которому находится команда вызова, так и вызывающая функция.
Перекрестные ссылки

267

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

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

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

268

ГЛАВА 9

read_it XREF[2]: main:0040101e(R),

main:00401035(R)
0040b720 undefined4 ??
write_it XREF[3]: main:0040102b(W),

main:0040103e(W),

main:0040104a(W)
0040b724 ?? ??
0040b725 ?? ??
0040b726 ?? ??
0040b727 ?? ??
ref_it XREF[3]: main:00401014(*),

main:00401024(W),

main:00401029(R)
0040b728 undefined4 ??

Листинг 9.3. Глобальные переменные, на которые есть ссылки
в simple_flows.c
Перекрестная ссылка типа чтения означает, что содержимое памяти по данному адресу читается. Источником таких
перекрестных ссылок может быть только адрес в команде, но
относиться они могут к любому месту в программе. Глобальная
переменная read_it читается дважды в листинге 9.1. Комментарии к ней показывают, в каких именно местах main имеются ссылки на read_it, которые легко опознаются по суффиксу (R). Чтение read_it  в листинге 9.1 проистекает из команды
32-разрядного чтения в регистр ECX, поэтому Ghidra отформатировала read_it как undefined4 (4-байтовое значение неизвестного типа). Ghidra часто пытается вывести размер элемента
данных из того, как с ним работает двоичный код.
На глобальную переменную write_it в листинге 9.1 есть три
ссылки. Для них генерируются перекрестные ссылки типа
запи­си, а в комментариях указаны места, где переменная write_
it модифицируется; их отличительным признаком является
суффикс (W). В данном случае Ghidra не отформатировала
write_it как 4-байтовую переменную, хотя, на первый взгляд,
информации для этого достаточно. Перекрестные ссылки типа
записи, как и типа чтения, могут исходить только из команды,
но относиться могут к любому месту в программе. В общем случае перекрестная ссылка типа записи, ссылающаяся на байт
команды, – признак самомодифицируемого кода, они часто
встречаются в обфусцированных вредоносных программах.
Перекрестные ссылки

269

Третий тип перекрестных ссылок на данные – ссылки типа
указателя, показывающие, что используется адрес элемента
данных (а не его содержимое). В листинге 9.3 берется адрес глобальной переменной ref_it, о чем свидетельствует суффикс (*).
Такие ссылки часто являются результатом получения адреса
кода или данных. Как мы видели в главе 8, операции доступа
к массиву обычно реализуются путем прибавления смещения
к начальному адресу массива, а первый адрес большинства глобальных массивов можно опознать по присутствию перекрестной
ссылки типа указателя. По этой причине большинство строковых литералов (а в C/C++ строки являются массивами символов)
являются целями перекрестных ссылок типа указателя.
В отличие от перекрестных ссылок типа чтения и записи, источником которых могут быть только команды, ссылки типа указателя могут исходить как из команд, так и из данных. Примером
указателей, исходящих из секции данных программы, может
служить любая таблица адресов (например, vf-таблица, для которой каждая запись, соответствующая виртуальной функции,
порождает перекрестную ссылку типа указателя). Посмотрим,
как это выглядит в примере класса SubClass из главы 8. Ниже
показан результат дизассемблирования vf-таблицы SubClass:
SubClass::vftable XREF[1]: SubClass_Constructor:00401062(*)
00408148 void * SubClass::vfunc1 vfunc1
 0040814c void * BaseClass::vfunc2 vfunc2
00408150 void * SubClass::vfunc3 vfunc3
00408154 void * BaseClass::vfunc4 vfunc4
00408158 void * SubClass::vfunc5 vfunc5

Как видим, элемент данных по адресу 0040814c  является
указателем на BaseClass::vfunc2. Перейдя к BaseClass::vfunc2,
мы увидим следующий листинг:
**************************************************************
*
FUNCTION
*
**************************************************************
undefined __stdcall vfunc2(void)
undefined AL:1
undefined4 Stack[-0x8]:4 local_8 XREF[1]: 00401024(W)

270

ГЛАВА 9

BaseClass::vfunc2 XREF[2]: 00408138(*),

0040814c(*)
00401020 PUSH EBP
00401021 MOV EBP,ESP
00401023 PUSH ECX
00401024 MOV dword ptr [EBP + local_8],ECX
00401027 MOV ESP,EBP
00401029 POP EBP
0040102a RET

В отличие от большинства функций, в этой нет перекрестных ссылок на код. Вместо этого мы видим две перекрестные
ссылки типа указателя, показывающие, что в двух местах берется адрес функции. Вторая XREF  ссылается обратно на
запись vf-таблицы SubClass, которая обсуждалась выше. А первая XREF  приведет нас к vf-таблице класса BaseClass, которая также содержит указатель на эту виртуальную функцию.
Этот пример показывает, что в C++ виртуальные функции
редко вызываются напрямую и обычно не являются целями
перекрестной ссылки типа вызова. Из-за способа создания vfтаблиц на любую виртуальную функцию ссылается по меньшей мере одна запись vf-таблицы, поэтому она обязательно
является целью хотя бы одной перекрестной ссылки типа указателя. (Напомним, что переопределять виртуальную функцию необязательно.)
Если двоичный файл содержит достаточно информации,
то Ghidra сможет найти vf-таблицы. Каждая найденная vfтаблица представлена записью в соответствующем классе в папке Classes дерева символов. Щелкнув по vf-таблице
в окне дерева символов, вы перейдете к этой таблице в секции
данных программы.

ОКНА УПРАВЛЕНИЯ ССЫЛКАМИ
Вы уже, вероятно, обратили внимание, что аннотации XREF довольно часто встречаются в окне листинга. Это не случайность,
поскольку связи, образуемые перекрестными ссылками, – это
клей, который не дает распасться программе. Перекрестные
ссылки расскажут нам о зависимостях внутри и между функциями, и для успешной обратной разработки, как правило, неПерекрестные ссылки

271

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

Окно перекрестных ссылок
Заголовки XREF можно использовать, чтобы больше узнать
о конкретной перекрестной ссылке, как показано в следующем
листинге:
undefined4 Stack[-0x10]:4 local_10 XREF[1]: 0804847c(W)
undefined4 Stack[-0x14]:4 local_14 XREF[2]:08048482(W),

08048493(R)

Двойной щелчок по заголовку XREF[2]  открывает показанное на рис. 9.3 окно перекрестных ссылок, в котором приводится более подробная информация о ссылках. По умолчанию
в окне отображается адрес, метка (если имеется), ссылающаяся
сущность и тип ссылки.

Рис. 9.3. Окно перекрестных ссылок

272

ГЛАВА 9

Ссылки на
Еще одно окно, полезное для понимания потока программы, – окно
Ссылки на. Если щелкнуть правой кнопкой мыши по любому
адресу в окне листинга и выбрать из контекстного меню команды
ReferencesShow Reference to Address (Ссылки Показать
ссылку на адрес), то откроется окно, показанное на рис. 9.4.

Рис. 9.4. Окно Ссылки на
В этом примере мы выбрали адрес начала функции helper.
В окне можно перейти к ассоциированному адресу, щелкнув по
любой строке таблицы.

Ссылки на символы
Еще одно представление ссылок, с которым мы познакомились в разделе «Окна таблицы символов и ссылок на символы» главы 5, – это комбинация окон таблицы символов и ссылок на символы. По умолчанию при выполнении команды
WindowSymbol References (ОкноСсылок на символы)
открывается два взаимосвязанных окна. В одном отображаются все символы из таблицы символов, а в другом – связанные
с символами ссылки. Если выбрать любой элемент в окне таб­
лицы символов (функцию, vf-таблицу и т. д.), то ссылки на него
появятся в окне ссылок на символ.

Перекрестные ссылки

273

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

Дополнительные способы работы
со ссылками
В начале этой главы мы сказали, что обратная ссылка и перекрестная ссылка – одно и то же, и кратко упомянули, что в Ghidra
имеются также прямые ссылки, причем двух типов. Выведенные
прямые ссылки обычно добавляются в листинг автоматически
и взаимно однозначно соответствуют обратным ссылкам, только
ведут в обратном направлении. Иными словами, по обратной
ссылке мы проходим от целевого адреса к исходному, а по выведенной прямой ссылке – от исходного к целевому.
Второй тип – явные прямые ссылки. Существует несколько
видов таких ссылок, и управлять ими гораздо труднее, чем обратными ссылками. К явным прямым ссылкам относятся ссылки на память, внешние ссылки, ссылки на стек и ссылки на
регистры. Помимо просмотра ссылок, Ghidra позволяет добавлять и редактировать ссылки различных типов.
Добавлять собственные перекрестные ссылки приходится, когда в результате статического анализа Ghidra не может
определить адреса перехода или вызова, поскольку они вычисляются на этапе выполнения, но вы знаете адреса из каких-то
других соображений. В следующем коде, который мы последний раз видели в главе 8, вызывается виртуальная функция.
0001072e PUSH EBP
0001072f MOV EBP,ESP
00010731 SUB ESP,8

274

ГЛАВА 9

00010734
00010737
00010739
0001073c
0001073e
00010741
00010744
00010746
00010749
0001074a
0001074b

MOV EAX,dword
MOV EAX,dword
ADD EAX,8
MOV EAX,dword
SUB ESP,12
PUSH dword ptr
CALL EAX
ADD ESP,16
NOP
LEAVE
RET

ptr [EBP + param_1]
ptr [EAX]
ptr [EAX]
[EBP + param_1]

Значение в регистре EAX  зависит от значения указателя,
переданного в параметре param_1. Поэтому у Ghidra не хватает информации, чтобы создать перекрестную ссылку, связывающую 00010744 (адрес команды CALL) с вызываемой функцией. Добавление перекрестной ссылки вручную (например,
на SubClass::vfunc3), среди прочего, позволило бы включить
вызываемые функции в граф вызовов и тем самым помочь
Ghidra в анализе программы. Если щелкнуть правой кнопкой
мыши по вызову  и выбрать из контекстного меню команду
ReferencesAdd Reference from (СсылкиДобавить ссылку из), то откроется диалоговое окно, показанное на рис. 9.5. Это
же окно открывается по команде меню ReferencesAdd/Edit.

Рис. 9.5. Диалоговое окно добавления ссылки
Перекрестные ссылки

275

Задайте адрес целевой функции в поле To Address (На адрес)
и убедитесь, что в поле Ref-Type задан правильный тип ссылки. После нажатия кнопки Add (Добавить) Ghidra закроет окно
и создаст ссылку, которая появится рядом с целевым адресом
с индикатором (c). Дополнительные сведения о прямых ссылках, в т. ч. об остальных типах ссылок и операциях с ними,
можно найти в справке по Ghidra.

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

10

ГРАФЫ

Визуальное представление данных графами,
как в предыдущей главе (см. рис. 9.1), – это лаконичный и понятный механизм, позволяющий
продемонстрировать многие связи между вершинами графа, а также распознать закономерности, которые было бы трудно увидеть, если работать с графом как с абстрактным типом данных. Графовые представления в Ghidra
предлагают новый взгляд на содержимое двоичного файла
(в дополнение к листингам дизассемблера и декомпилятора). Они наглядно показывают поток управления в функции
и связи между функциями в файле, поскольку функции и блоки других типов представляются вершинами графа, а потоки
и перекрестные ссылки – ребрами (линиями, соединяющими
вершины). Немного попрактиковавшись, вы обнаружите, что
типичные управляющие конструкции, например предложения switch и вложенные if/else, проще разглядеть в графе,
чем в длинном текстовом листинге. В главе 5 мы уже познакомились с окнами графа функций и графа вызовов функции.
А в этой главе изучим графовые средства Ghidra более глубоко.

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

ПРОСТЫЕ БЛОКИ
В компьютерной программе простым блоком называется
группа, состоящая из одной или нескольких команд, с одной
точкой входа в начале блока и одной точкой выхода в конце. Все команды в блоке, кроме последней, передают управление ровно одной последующей команде, расположенной
внут­ри блока. Аналогично все команды, кроме первой, получают управление ровно от одной предшествующей команды,
принадлежащей блоку. В разделе «Перекрестные (обратные)
ссылки» главы 9 мы определили последовательный поток.
Иногда, увидев в середине простого блока вызов функции, вы
задаетесь вопросом: «А не должна ли эта команда, как и команда перехода, завершать блок?» Но при определении прос­
того блока тот факт, что функция передает управление вовне,
обычно не принимается во внимание, исключение составляет случай, когда мы точно знаем, что функция не возвращает
управление обычным способом.
После выполнения первой команды простого блока гарантируется, что весь блок будет выполнен до конца. Это может

278

ГЛАВА 10

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

ГРАФЫ ФУНКЦИЙ
В окне графа функции, описанном в главе 5, отображается
одна функция в графическом формате. Показанная ниже программа состоит из единственной функции, содержащей один
простой блок; это удобная отправная точка для демонстрации
графов функций.
int global_array[3];
int





}

main() {
int idx = 2;
global_array[0] =
global_array[1] =
global_array[2] =
global_array[idx]

10;
20;
30;
= 40;

Выбрав функцию main и открыв окно графа функции
(WindowFunction Graph), мы увидим граф, содержащий
всего один простой блок, как показано на рис. 10.1.

Графы

279

Рис. 10.1. Окно графа функции с одним блоком и вид
со спутника в правом нижнем углу
Между окнами листинга и графа функции имеется полезная
двусторонняя связь. Расположив оба окна бок о бок и рассматривая их одновременно, можно лучше понять поток управления
в функции. Изменения, которые вы вносите в окне графа функции (например, переименование функций, переменных и т. д.),
280

ГЛАВА 10

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

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

If/Else

If/Else

If/Else

If/Else

Рис. 10.2. Граф функции с сочлененными и несочлененными
ребрами
Если щелкнуть по любой строке текста в окне графа функции, то курсор в окне листинга переместится в соответствующее
место. Если дважды щелкнуть по данным в графе функции, то
окно листинга сдвинется к соответствующим данным в секции
Графы

281

данных, а окно графа функции останется сфокусированным на
функции. (В настоящее время Ghidra не поддерживает визуализацию данных и связей между ними в виде графа, но все же
позволяет одновременно просматривать данные в окне листинга и ассоциированный код в окне графа.)
Рассмотрим пример, демонстрирующий связь между окнами листинга и графа функции. Пусть мы видим переменную
global_array на рис. 10.1 и хотим узнать больше о ее типе.
Дважды щелкнув по ее имени в окне графа, мы увидим, что
Ghidra классифицировала global_array как массив неопределенных байтов (undefined1), к четвертому и восьмому элементам
которого производится доступ по индексу. Изменив определение массива в секции данных в окне листинга с undefined1[12]
на int[3] (показаны соответственно в верхней и нижней половинах рис. 10.3), мы сразу же увидим, как это отразилось на результате дизассемблирования в окне графа функции (а также
в окне декомпилятора): значения индексов стали равны 1 и 2,
поскольку теперь размер элементов массива равен 4 байтам.
global_array [4]

dword ptr [RBP + local_c],EAX

global_array [8]

dword ptr [global_array],10

global_array

dword ptr [global_array[4]] ,20
dword ptr [global_array[8]] ,30

global_array [1]

dword ptr [RBP + local_c],EAX

global_array [2]

dword ptr [global_array],10

global_array

dword ptr [global_array[1]],20
dword ptr [global_array[2]],30

Рис. 10.3. Последствия изменения объявления массива в окнах
графа функции и листинга
Навигация в окне листинга устроена достаточно удобно, при
условии что вы не щелкаете по другой функции. Можно прокрутить все содержимое окна листинга, щелкнуть и внести изменения в секции данных, изменить саму функцию и т. д. Если
же щелкнуть внутри другой функции, то окно графа перерисуется – в нем будет показан граф вновь выбранной функции.
282

ГЛАВА 10

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

На рис. 10.4 показаны меню и панели инструментов окна
графа функции.

n
o
p
q

Рис. 10.4. Панели инструментов окна графа функции
Граф функции – на самом деле просто графическое представление окна листинга, ограниченное одной функцией, поэтому
неудивительно, что все меню окна браузера кода (за исключением Window) доступны  и в окне графа функции. Подмножест­
во панели инструментов  включает значки сохранения текущего состояния открытого файла, отмены и повтора, а также
прохода вперед и назад по текущей цепочке навигации. Важно
отметить, что, поскольку окна связаны, таким образом можно
выйти за пределы текущей функции (и вернуться обратно),
в результате чего содержимое окна графа функции изменится.
Значки на панели инструментов графа функции  и их поведение по умолчанию описаны на рис. 10.5.
Графы

283

Скопировать в„буфер обмена
Ghidra
Вставить из буфера обмена
Ghidra

Эта возможность имеется во многих окнах Ghidra, а„ее функциональность зависит от конкретного окна и„того, что выбрано в„
момент выполнения операции. Если содержимое буфера
несовместимо с„местом вставки, то выдается сообщение
об ошибке

Перейти на точку входа
в функцию

Этот значок переносит вас в„блок точки входа в„окне
графа функции

Перезагрузить граф

После перезагрузки графа вся информация о„позиционировании
и„группировке теряется. Восстанавливается исходный вид

Формат вложенного кода

Формат вложенного кода позволяет сохранять информацию
о„группировке при изменении расположения

Редактировать поля блока
кода

Позволяет редактировать поля блока кода в„окне графа функции.
Не„влияет на поля блока кода в„окне листинга

Режим фокусирования
блока

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

Снимок

Эта кнопка создает и„открывает не„связанную с„листингом копию
текущего окна графа функции

Режим парящего блока

Рис. 10.5. Операции на панели инструментов окна графа
функции
У каждого простого блока имеется также панель инструментов , которая позволяет изменять блок и группировать его
с другими блоками, объединяя несколько блоков (вершин)
в один (на рис. 10.6 объяснено назначение значков на этой панели инструментов). Это средство исключительно полезно для
уменьшения сложности графов, возникшей из-за большой вложенности кода функции. Например, можно объединить в один
все блоки, вложенные в цикл, – после того как вы разберетесь
с поведением цикла и необходимость видеть код внутри него
отпадет. Удобочитаемость графа сильно зависит от количества
вложенных блоков. Для группировки нужно выбрать все вершины, которые должны войти в группу, щелкая по каждой с нажатой клавишей Ctrl, а затем щелкнуть по значку Combine
vertices (Объединить вершины) той вершины, которая, по вашему мнению, должна стать корнем группы. Кнопка восстановления группы позволяет ненадолго заглянуть внутрь группы, а затем снова свернуть ее.
284

ГЛАВА 10

Цвет фона

Выбрать цвет фона для блока или группы
блоков. Этим цветом будут закрашиваться
блоки в окне графа функции и в окне листинга

Перейти к XREF

Показать список перекрестных ссылок
на точку входа в функцию

Полноэкранный
режим представления
в виде графа

Эта кнопка переключает режим просмотра
блокаграфа между полноэкранным
и обычным

Объединить вершины

Объединить выбранные вершины в одну
группу

Восстановить группу

Этот значок отображается, только если
вершины разгруппированы, и позволяет
снова сгруппировать их

Разгруппировать
вершины

Этот значок доступен, только если
вершины были сгруппированы,
и позволяет разгруппировать их

Добавить вершину в
группу

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

Рис. 10.6. Панель простого блока в окне графа функции
Чтобы познакомиться с другими возможностями графа функции, придется рассмотреть примеры кода, содержащего несколько блоков. В качестве такового мы будем использовать
следующую программу.
int do_random() {
int r;
srand(time(0));
r = rand();
if (r % 2 == 0) {
printf("Случайное значение %d четно\n", r);
}
else {
printf("Случайное значение %d нечетно\n", r);
}
return r;
}
int main() {
do_random();
}
Графы

285

Функция do_random включает управляющие конструкции (if/
else), вследствие чего граф содержит четыре простых блока, которые мы пометили на рис. 10.8. При одном взгляде на функцию,
содержащую более одного блока, становится очевидно, что это
граф потока управления, ребра которого описывают возможные
потоки из одного блока в другой. Заметим, что в Ghidra топология графов функций называется топологией вложенного кода
и очень напоминает поток управления в коде на C. Это упрощает
просмотр графового представления окон листинга и декомпилятора в большой программе. Для просмотра этого представления
мы настоятельно рекомендуем изменить параметры графа, так
чтобы ребра обходили вершины (EditTool OptionsFunction
GraphNested Code LayoutRoute Edges Around
Vertices – РедактированиеПараметры инструментовГраф
функцииТопология вложенного кодаРебра обходят вершины). По умолчанию Ghidra проводит ребра под вершинами, изза чего зачастую складывается ложная картина связей между
вершинами.

Этот граф устарел
Хотя некоторые изменения в листинге отражаются в графе
функций сразу же, бывают случаи, когда граф устаревает (т. е.
не синхронизирован с представлением в листинге). Тогда Ghidra
отобра­жает в нижней части окна графа сообщение, показанное
на рис. 10.7.

Рис. 10.7. Предупреждение о том, что граф устарел
Значок рециклинга слева от сообщения позволяет перестроить граф, не возвращаясь к оригинальной топологии. (Разумеется, вы можете вместо этого обновить граф, восстановив топологию.)
В графе на рис. 10.8 BLOCK-1 – единственная точка входа
в функцию. В этом блоке, как и во всех простых блоках, поток управления от команды к команде последовательный.
286

ГЛАВА 10

Ни один из трех вызовов функций внутри блока (time, srand
и rand) не нарушает свойство простоты блока, поскольку Ghidra
предполагает, что все они возвращаются к последовательному
выполнению команд. В блок BLOCK-2 программа входит, если
условие в команде JNZ в конце BLOCK-1 равно false, т. е. случайное значение четно. В блок BLOCK-3 программа входит,
если условие в команде JNZ равно true, т. е. случайное значение нечетно. И в последний блок BLOCK-4 программа входит
после завершения BLOCK-2 или BLOCK-3. Отметим, что щелчок по ребру делает его активным, так что оно выглядит толще
остальных. На рисунке активно ребро, соединяющее BLOCK-1
и BLOCK-3, поэтому оно нарисовано жирной линией.

BLOCK-1

BLOCK-1

If/Else

If/Else

BLOCK-2

BLOCK-2

If/Else

BLOCK-3

BLOCK-3

If/Else

BLOCK-4

BLOCK-4

Рис. 10.8. Граф функций: жирная линия показывает, по какому
пути следует функция, когда условие выполнено
Графы

287

Если встретился особенно длинный блок, который вы хотели
бы разбить на меньшие, или если требуется визуально выделить
секцию кода для дальнейшего анализа, то можно расщепить
простой блок в графе функции, включив в него новые метки.
С помощью горячей клавиши L вставьте метку в строке 0010072e
в блоке BLOCK-1 перед вызовом srand, тогда в графе появится
новый блок, как показано на рис. 10.9. Новое ребро представляет поток, никакая перекрестная ссылка с ним не связана.

If/Else

If/Else

Рис. 10.9. Граф функций, в который добавлена метка и вместе
с ней новый блок

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

288

ГЛАВА 10

Ребра
Цвет ребра зависит от характера представленного им перехода.
Цвета можно задавать в окне EditTool Options, как показано на рис. 10.10. По умолчанию зеленым цветом отображается
условный переход, когда условие истинно (т. е. переход выполнен), красным – когда ложно (переход не выполнен), а синим –
безуслов­ный переход. Щелчок по одному ребру или группе ребер
увеличивает толщину линии.

Рис. 10.10. Настройка цветов в окне графа функции
Вершины
Содержимое вершин – это код соответствующего простого блока
в листинге дизассемблера. Мы взаимодействуем с этим кодом
точно так же, как в окне листинга. Например, если задержать
мышь над именем, то откроется всплывающее окно, в котором
будет показан дизассемблированный код вместе с этим именем.
Если задержать мышь над вершиной, то Ghidra анимирует путь,
проходящий по связанным с ней ребрам, чтобы показать направление потока управления в соответствии с текущими параметрами подсветки пути. Эту функциональность можно отключить с помощью меню EditTool Options.

Спутник
На виде со спутника (уменьшенном представлении графа) текущий
блок окружен желтым ореолом, как и в окне графа функции. Чтобы
входной блок функции было проще различить, он нарисован зеленым цветом на виде со спутника, тогда как все возвратные блоки (содержащие команду RET или эквивалентную ей) – красным. Даже пос­
ле изменения цвета фона соответствующего блока в графе цвета
входного и возвратных блоков на виде со спутника не изменяются.
Цвета остальных блоков будут такими же, как в окне графа функции.

Графы

289

ГРАФЫ ВЫЗОВОВ ФУНКЦИЙ
Граф вызовов функций полезен, когда нужно составить общее
представление об иерархии вызовов функций в программе.
Граф вызовов функций похож на граф функции, но каждая
вершина в нем представляет функцию целиком, а каждое реб­
ро – перекрестную ссылку из одной функции на другую.
Для обсуждения графов вызова функций мы будем использовать следующую тривиальную программу с простой иерархией вызовов:
#include
void depth_2_1() {
printf("внутри depth_2_1\n");
}
void depth_2_2() {
fprintf(stderr, "внутри depth_2_2\n");
}
void depth_1() {
depth_2_1();
depth_2_2();
printf("внутри depth_1\n");
}
int main() {
depth_1();
}

После сборки динамически скомпонованной версии этой программы компилятором GNU gcc и загрузки двоичного файла
в Ghidra мы можем построить граф вызова функций командой
WindowFunction Call Graph (ОкноГраф вызова функций). По умолчанию создается граф вызовов с центром в текущей выбранной функции. На рис. 10.11 показано, как выглядит граф, когда выбрана функция main. (Чтобы не отвлекаться,
вид со спутника в этих примерах скрыт. Чтобы показать его,
воспользуйтесь значком в правом нижнем углу рис. 10.11.)

290

ГЛАВА 10

Рис. 10.11. Простой граф вызова функций с центром в main
Строка main (3 functions; 2 edges) в полосе заголовка окна показывает, в какой функции мы находимся, а также количество
отображаемых функций и ребер. Если задержать мышь над
вершиной графа, то снизу и (или) сверху от вершины появятся
значки + и –, как показано на рис. 10.12.

Рис. 10.12. Граф вызова функций со значками раскрытия
и сворачивания

Графы

291

Значок + сверху или снизу означает, что можно показать дополнительные входящие или исходящие вызовы, а значок –
озна­чает, что вершины можно свернуть. Например, щелчок по
значку – снизу от функции depth_1, когда она раскрыта и граф
выглядит, как показано на рис. 10.13, приведет к тому, что вершина свернется, и граф будет выглядеть, как на рис. 10.11.

Рис. 10.13. Раскрытая вершина main графа вызовов функций
292

ГЛАВА 10

Контекстное меню, ассоциированное с каждой вершиной,
позволяет раскрыть или свернуть сразу все ребра, исходящие
из всех вершин на одном горизонтальном уровне. Это эквивалентно одновременному щелчку по значку + или – на всех
вершинах одного ранга. Наконец, двойной щелчок по вершине помещает ее в центр графа и полностью раскрывает все
входящие и исходящие ребра. Многие находят полезной возможность увеличивать и уменьшать масштаб, хотя по умолчанию она выключена. Чтобы ее включить, выполните команду
EditTool Options и отметьте флажок Scroll Wheel Pans
(Колесико мыши панорамирует). Ghidra хранит в кеше историю графов при изменении фокуса, чтобы быстро восстановить состояние графа после возврата. Это позволяет раскрывать и сворачивать вершины, уходить в другое место листинга,
а вернувшись, застать граф в том состоянии, в котором вы его
оставили, и продолжить анализ.
На рис. 10.14 показана та же программа, только в фокусе
находится вершина _start, а не main, и большинство вершин
полностью раскрыты, чтобы граф был виден во всей красе.
Помимо функции main и вызываемых из нее, мы видим вспомогательный код, добавленный компилятором. Этот код отвечает за инициализацию и очистку библиотеки, а также за
настройку окружения до передачи управления функции main.
(Внимательные читатели, наверное, заметили, что компилятор заменил вызовы printf и fprintf вызовами puts и fwrite
соответственно, потому что при печати статических строк они
эффективнее.)

Графы

293

Рис. 10.14. Раскрытая вершина _start графа вызовов функций

Шлюзы
Вы, возможно, обратили внимание, что в графе на рис. 10.14 показано несколько (по-видимому, рекурсивных) вызовов puts. Доб­
ро пожаловать в волшебный мир функций-шюзов. Функция-шлюз
(thunk function) – это механизм, применяемый компилятором для
вызова функций, адрес которых неизвестен на этапе компиляции
(например, из DLL-библиотеки). В Ghidra функции с неизвестным
адресом называются шлюзованными (thunked). Компилятор заменяет все вызовы шлюзованных функций вызовом заглушки функ-

294

ГЛАВА 10

ции-шлюза, вставляемым в исполняемый файл. Эта заглушка
обычно производит поиск в таблице, чтобы узнать адрес шлюзованной функции, а затем передать ей управление. Таблица заполняется на этапе выполнения, когда адреса шлюзованных функций
уже известны. В исполняемых файлах Windows эта таблица называется таблицей импорта, а в двоичных ELF-файлах – глобальной таблицей смещений (global offset table, или got).
Перейдя к puts из функции depth_1 в окне листинга, мы увидим такой код:
**************************************************************
*
THUNK FUNCTION
*
**************************************************************
thunk int puts(char * __s)
Thunked-Function: ::puts
int
EAX:4

char *
RDI:8
__s
puts@@GLIBC_2.2.5
puts
XREF[2]: puts:00100590(T),
puts:00100590(c), 00300fc8(*)
00302008
??
??
00302009
??
??
0030200a
??
??
Этот листинг шлюзованной функции находится в секции программы,
которую Ghidra называет EXTERNAL. Подобные листинги – результат
механизма динамической загрузки и компоновки внешних библиотек
во время выполнения, а это значит, что такие библиотеки недоступны на этапе статического анализа. Хотя листинг содержит указание
на вызов библиотечной функции, код этой функции недоступен (если
только библиотека также не загружена в Ghidra, что легко сделать
с помощью страницы параметров в процессе импорта).
Здесь же мы видим новый тип XREF. Суффикс (T) в первой XREF
говорит, что эта XREF ссылается на шлюзованную функцию.

Теперь вернемся к статически скомпонованной версии программы call_tree. Начальный граф, сгенерированный по
функции main , совпадает с графом для динамически скомпонованной версии на рис. 10.11. Однако чтобы получить представление о потенциальной сложности графов статически скомпонованных двоичных файлов, рассмотрим два продолжения,
на первый взгляд, совершенно невинных. На рис. 10.15 показаны вызовы, исходящие из функции puts. В полосе заголовка
Графы

295

мы видим puts(9 functions; 11 edges). Заметим, что итоговые
цифры в полосе заголовка могут быть не точны до того момента, как программа будет полностью проанализирована.

Рис. 10.15. Граф вызова функций для статически
скомпонованного двоичного файла
Переведя фокус на вершину _lll_lock_wait_private, мы увидим ошеломительный граф, содержащий аж 70 вершин и более 200 ребер, часть его показана на рис. 10.16.

Рис. 10.16. Раскрытый граф вызовов функций для статически
скомпонованного двоичного файла
296

ГЛАВА 10

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

ДЕРЕВЬЯ
Ghidra представляет многие иерархические концепции, связанные с двоичным файлом, в виде древовидных структур. Это
не всегда деревья в смысле теории графов, но они позволяют
раскрывать и сворачивать вершины и видеть иерархические
связи между вершинами разных типов. Обсуждая окно браузера кода в главе 5, мы сталкивались с деревьями программы,
деревом символов, деревом вызова функции и диспетчером типов данных (который также представлен в виде дерева). Эти
древовидные представления можно использовать одновременно с другими, чтобы взглянуть на анализируемый двоичный
файл с разных сторон.

РЕЗЮМЕ
Графы – действенный инструмент анализа любого двоичного
файла. Если вы привыкли просматривать дизассемблированный код в чисто текстовом формате, то на то, чтобы приспособиться к графовому представлению, может уйти некоторое
время. В Ghidra нужно лишь осознать, что вся информация,
доступная в текстовом виде, доступна и в виде графа, хотя отформатирована по-другому. Например, перекрестные ссылки
становятся ребрами, соединяющими блоки в вершинах графа.
На какой граф смотреть, зависит от того, что вы хотите узнать
о двоичном файле. Если вас интересует, как управление доходит до конкретной функции, то, скорее всего, вам подойдет граф
вызова функций. Если же нужно знать, по какому пути достигается конкретная команда, то лучше выбрать граф функции.
Оба дают ценные сведения о работе программы.
Графы

297

Итак, теперь вы знаете, какую функциональность Ghidra
предлагает инженеру, работающему над обратной разработкой
в одиночестве. А в следующей главе мы увидим, как Ghidra
Server и создаваемое им окружение поддерживают коллективную работу.

Часть III

Поставить GHIDRA
себе на службу

11

КОЛЛЕКТИВНАЯ
ОБРАТНАЯ РАЗРАБОТКА
ПРОГРАММ

Вы уже должны были освоиться с навигацией
по проекту Ghidra и с многочисленными имеющимися в вашем распоряжении инструментами
и окнами. Вы знаете, как создать проект, как импортировать файлы, как осуществлять навигацию
и манипулировать листингом дизассемблера. Вы разбираетесь
в типах данных, структурах данных и перекрестных ссылках.
Но понимаете ли вы, как масштабировать проект? Дизассемб­
лированный 200-мегабайтовый двоичный файл, скорее всего,
будет содержать миллионы строк и состоять из сотен тысяч
функций. Даже на самом большом вертикально ориентированном мониторе, который вам удастся отыскать, одновременно
видно лишь несколько сотен строк.
Один из способов справиться с этой грандиозной задачей, –
выделить для ее решения группу людей, но тогда возникает новая проблема: как синхронизировать их работу, чтобы измене-

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

КОЛЛЕКТИВНАЯ РАБОТА
SRE – сложный процесс, и немного найдется людей, досконально разбирающихся во всех его тонкостях. Возможность
распределить работу над одним двоичным файлом между
аналитиками, обладающими различными навыками, значительно сократит сроки получения желаемых результатов.
Человек, блистательно справляющийся с прослеживанием
потоков управления в сложной программе, может сникнуть,
если ему поручат проанализировать и документировать соответствующие структуры данных. Эксперт по анализу вредоносных программ не всегда подходит для работы по обнаружению
уязвимостей, а когда время поджимает, вряд ли кто-то захочет
вставлять кучу комментариев, которые, конечно, оказались бы
весьма кстати попозже, но прямо сейчас крадут время, которое можно посвятить анализу дополнительного кода. Пять сотрудников, возможно, захотят проанализировать один и тот же
файл по отдельности, но придут к выводу, что некоторые шаги
придется сделать каждому. Один специалист может передать
свою часть работы другому, потому что тот лучше разбирается
в теме, или просто на время отпуска. А иногда полезно, когда несколько пар глаз смотрят на одно и то же, чтобы ничего
не упустить. Как бы то ни было, благодаря возможности разделения проекта Ghidra поддерживает разные формы коллективного SRE.

302

ГЛАВА 11

ПОДГОТОВКА СЕРВЕРА GHIDRA
Совместную работу в Ghidra обеспечивает разделяемый экземпляр сервера Ghidra. У администратора, отвечающего за
подготовку сервера Ghidra, есть много вариантов действий,
например развернуть на «голом железе» или в виртуальной
среде, обеспечивающей простоту миграции и повторяемость
установки. Тот способ развертывания, который используется
в этой главе для демонстрации возможностей коллективной
работы в Ghidra, подходит только для разработки и экспериментов. Перед развертыванием сервера Ghidra в производственном режиме следует внимательно прочитать документацию и выбрать конфигурацию, наиболее отвечающую
вашей среде и предполагаемому способу использования.
(Описанию установки и настройки сервера Ghidra, всех параметров и подходов можно было бы посвятить целую книгу,
но только не эту.)
Хотя сервер Ghidra можно сконфигурировать на всех платформах, поддерживающих Ghidra, мы ограничимся средой
Linux и будем предполагать, что читатель хоть немного знаком
с командной строкой Linux и администрированием системы.
Мы внесем несколько небольших изменений в конфигурационный файл сервера Ghidra (server/server.conf), чтобы можно было продемонстрировать концепции, интересующие нас
в этой главе, и не слишком сильно зависеть от использования
командного интерфейса Linux после начальной установки,
настройки, администрирования и определения прав доступа.
К этим изменениям относятся замена каталога репозитория
Ghidra по умолчанию нашим собственным, как рекомендовано в документации по серверу Ghidra, и настройка параметров
управления пользователями и контроля доступа.

Что предлагает сервер Ghidra
Ваш сервер Ghidra счастлив предоставить следующие возможности установки.
Платформы: «голое железо», виртуальные машины, контейнеры
и другие!
Коллективная обратная разработка программ

303

Операционные системы: многочисленные варианты Windows,
Linux и macOS. Каждому найдется что-то по вкусу.
Методы аутентификации: выбирайте, каким образом друзья
и коллеги смогут получить доступ к вашим щедрым дарам – от
«открыто для всех» до «только при наличии инфраструктуры открытых ключей» и все, что в промежутке.
Подготовка: установка возможна из контейнера, с помощью
скрипта, bat-файлов или путем следования подробным инструкциям. А можете создать собственный рецепт, стуча по клавишам,
пока не получится что-то путное.
Если ничто из описанного не пришлось вам по нраву, не расстраивайтесь – ведь это лишь часть доступного. Возможности сервера
Ghidra могут удовлетворить потребности даже самых разборчивых гостей, позволив им создать среду своей мечты. Спасибо, что
заглянули на вечеринку к серверу Ghidra. Дополнительные сведения смотрите в расширенном меню – файле server/svrREADME.
html, который лежит в каталоге Ghidra прямо рядом с вами.

Далее приводится пошаговое описание скрипта создания
среды и начального множества пользователей Ghidra на машине под управлением Ubuntu.
1. Определить переменные среды, которые будут использоваться в скрипте, в т. ч. номер устанавливаемой версии Ghidra:
# задать переменные среды
OWNER=ghidrasrv
SVRROOT=/opt/${OWNER}
REPODIR=/opt/ghidra-repos
GHIDRA_URL=https://ghidra-sre.org/ghidra_version.zip
GHIDRA_ZIP=/tmp/ghidra.zip

2. Установить два пакета (unzip и OpenJDK), необходимых
для установки и эксплуатации сервера:
sudo apt update && sudo apt install -y openjdk-version-jdk unzip

3. Создать непривилегированного пользователя, от имени которого будет запущен сервер и создан каталог для хранения
разделяемых репозиториев Ghidra вне каталога, в который
установлен сервер Ghidra. Хранение исполняемых файлов
304

ГЛАВА 11

и ваших репозиториев в разных каталогах рекомендуется в руководстве по конфигурированию сервера, поскольку это упрощает обновление сервера в будущем. Средство
админист­рирования сервера Ghidra (svrAdmin) будет использовать домашний каталог пользователя, являющегося
администратором сервера.
sudo useradd -r -m -d /home/${OWNER} -s /usr/sbin/nologin -U ${OWNER}
sudo mkdir ${REPODIR}
sudo chown ${OWNER}.${OWNER} ${REPODIR}

4. Скачать Ghidra, распаковать и переместить в корневой каталог сервера. Убедитесь, что скачиваете последнюю публичную версию (дата выпуска входит в состав имени zip-файла):
wget ${GHIDRA_URL} -O ${GHIDRA_ZIP}
mkdir /tmp/ghidra && cd /tmp/ghidra && unzip ${GHIDRA_ZIP}
sudo mv ghidra_* ${SVRROOT}
cd /tmp && rm -f ${GHIDRA_ZIP} && rmdir ghidra

5. Создать резервную копию оригинального конфигурационного файла сервера и изменить место хранения репозиториев:
cd ${SVRROOT}/server && cp server.conf server.conf.orig
REPOVAR=ghidra.repositories.dir
sed -i "s@^$REPOVAR=.*\$@$REPOVAR=$REPODIR@g" server.conf

6. Добавить флаг -u в состав параметров запуска сервера
Ghidra Server, чтобы пользователи могли задавать имя при
подключении, а не были привязаны к своему локальному
имени пользователя. Это позволяет подключаться к серверу
от имени нескольких разных пользователей с одной машины (полезно для демонстрации) и входить в одну учетную
запись с разных машин. (Некоторые версии Ghidra ожидают, что путь к репозиторию является последним параметром
в командной строке, поэтому мы изменили parameter.2 на
parameter.3 и добавили новый parameter.2=-u перед этой измененной строкой.)
Коллективная обратная разработка программ

305

PARM=wrapper.app.parameter.
sed -i "s/^${PARM}2=/${PARM}3=/" server.conf
sed -i "/^${PARM}3=/i ${PARM}2=-u" server.conf

7. Сделать владельцем процесса сервера Ghidra и его каталога
пользователя ghidrasvr. (Поскольку это всего лишь демонстрационный сервер, мы оставили все остальные параметры
без изменения. Настоятельно рекомендуем прочитать файл
server/svrREADME.html и решить, какие конфигурационные
параметры стоит задать в вашей производственной среде.)
ACCT=wrapper.app.account
sed -i "s/^.*$ACCT=.*/$ACCT=$OWNER/" server.conf
sudo chown -R ${OWNER}.${OWNER} ${SVRROOT}

8. Наконец, установить сервер Ghidra как службу и добавить
пользователей, имеющих право подключаться к серверу.
sudo
sudo
sudo
sudo

./svrInstall
${SVRROOT}/server/svrAdmin -add user1
${SVRROOT}/server/svrAdmin -add user2
${SVRROOT}/server/svrAdmin -add user3

Более подробно мы обсудим контроль доступа ниже в этой главе, но добавить пользователей необходимо уже сейчас, потому что
они должны присутствовать в системе аутентификации сервера
Ghidra. Это делается на самом сервере. По умолчанию каждый
пользователь должен войти из клиента Ghidra в течение 24 часов с паролем по умолчанию changeme (который должен быть
изменен в ходе первого сеанса работы). Если пользователь не активирует свою учетную запись в течение 24 часов, то она будет
заблокирована и должна быть восстановлена. Ghidra предлагает
на выбор несколько способов аутентификации, начиная с обычных паролей и кончая инфраструктурой открытых ключей (PKI).
Мы выбрали локальный пароль Ghidra (режим по умолчанию).
Если вы собираетесь установить собственный сервер Ghidra
или просто хотите почитать подробное описание различных вариантов и параметров установки, обратитесь к файлу server/
svrREADME.html в своем каталоге Ghidra.
306

ГЛАВА 11

Репозиторий проекта
Одно из преимуществ коллективной работы заключается в том,
что несколько человек могут работать с одним и тем же файлом
одновременно. И это же – один из недостатков коллективной
работы. Когда несколько пользователей изменяют одни и те же
данные, существует возможность гонки. Состояние гонки возникает, когда порядок выполнения операций (например, сохранений измененного файла) влияет на конечный результат. В Ghidra
имеется репозиторий проекта и система версионирования для
управления тем, какие изменения фиксируются, когда и кем.
Репозиторий позволяет извлекать и возвращать файлы, отслеживать историю версий и видеть, какие файлы в данный момент
извлечены. Выполняя операцию извлечения, вы получаете копию файла. А после того как вы закончили работать с файлом
и возвратили его в репозиторий, создается новая версия, которая
становится частью наследия файла. Если же два человека захотят возвратить новую версию файла, то репозиторий поможет
разрешить конфликты. Мы продемонстрируем взаимодействие
с репозиторием ниже в этой главе.

РАЗДЕЛЯЕМЫЕ ПРОЕКТЫ
До сих пор мы создавали только автономные проекты Ghidra,
с которыми может работать один аналитик с одного компьютера.
Теперь, сконфигурировав сервер Ghidra и предоставив себе доступ, разберем процедуру создания разделяемого проекта. Разделяемый проект можно сделать доступным всем пользователям,
которым разрешено подключаться к серверу Ghidra; он открывает
возможность коллективного и одновременного доступа к проекту.

Создание разделяемого проекта
Если при создании нового проекта (FileNew Project) выбрать разделяемый проект, то нужно будет задать информацию о сервере Ghidra, как показано слева на рис. 11.1. Номер
порта по умолчанию задан, но вы должны указать имя или IPадрес сервера и, возможно, аутентифицироваться (последнее
зависит от конфигурации сервера).

Коллективная обратная разработка программ

307

Рис. 11.1. Вход в репозиторий сервера Ghidra
Справа на рисунке показано, как мы входим от имени одного
из пользователей, созданных скриптом установки (user1). Если
это первый вход от имени данного пользователя, то нужно будет поменять пароль changeme, о чем уже было сказано выше.
Затем выберите существующий репозиторий или создайте
новый, введя имя репозитория (рис. 11.2). В этом примере мы
создадим репозиторий CH11.

Рис. 11.2. Диалоговое окно нового проекта

308

ГЛАВА 11

После нажатия кнопки Next (Далее) будет создан новый
репозиторий и новый проект, и мы окажемся в уже знакомом
окне проекта (рис. 11.3).









Рис. 11.3. Табличное представление в окне разделяемого
проекта
Мы импортировали несколько файлов  и показываем их
в таблице, а не в дереве, как подразумевается по умолчанию.
Табличное представление, находящееся в одной из вкладок ,
предоставляет гораздо больше информации о каждом из файлов проекта. В окне проекта показано имя репозитория проекта (CH11), ваша роль в нем (Administrator)  и справа значок,
несущий информацию о подключении к серверу . В данном
случае если задержать мышь над этим значком, то появится
сообщение Connected as user1 to 172.16.4.35 (Подключен
как user1 к 172.16.4.35). Если подключение отсутствует, то вы
увидите значок разорванного звена цепи, а не цельного, как
на рисунке.
Коллективная обратная разработка программ

309

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

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

Не хочу ни с кем делиться
Установка сервера Ghidra для неразделяемых проектов тоже
имеет свои преимущества. При первоначальном знакомстве
с Ghidra мы уделили основное внимание установке на одном
компью­тере и использованию этого компьютера для доступа
к своим проектам и файлам (все они на этом компьютере и хранились). Это означает, что весь анализ зависит от данного компьютера. Сервер Ghidra позволяет обращаться к своим файлам
с различных устройств. Можно потребовать, чтобы пользователь
предварительно аутентифицировался. При желании можно также преобразовать неразделяемый проект в разделяемый. Есть,
правда, одно ограничение – чтобы извлечь или возвратить файлы, нужно подключиться к серверу Ghidra.

310

ГЛАВА 11

МЕНЮ ОКНА ПРОЕКТА
После того как мы настроили сервер Ghidra и подключились
к нему, различные возможности, имеющиеся в окне проекта,
обретают смысл, поскольку некоторые из ранее недоступных
средств теперь оказались в другом контексте. Здесь и в главе 12
мы обсудим некоторые компоненты меню и их использование
для повышения качества анализа.

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

ŚŷŰŭũŻƅŶŷūƄŲŹũŰŭŮŴƈŮŵƄŲűŴűŶŮŹũŰŭŮŴƈŮŵƄŲŸŹŷŮųŻ
ŗŻųŹƄŻƅźżƂŮźŻūżƇƂűŲŹũŰŭŮŴƈŮŵƄŲűŴűŶŮŹũŰŭŮŴƈŮŵƄŲ
ŸŹŷŮųŻ
ŋƄūŮźŻűźŸűźŷųŶŮŭũūŶŷŷŻųŹƄŻƄžŸŹŷŮųŻŷūƀŻŷŪƄŷŻųŹƄŻƅ
ŸŹŷŮųŻŰũŶŷūŷ
ŐũųŹƄŻƅŻŮųżƂűŲŸŹŷŮųŻ
ŚŷžŹũŶűŻƅŻŮųżƂűŲŸŹŷŮųŻ

n

ŜŭũŴűŻƅŻŮųżƂűŲŸŹŷŮųŻ ŰũŸŹũƁűūũŮŻźƈŸŷŭŻūŮŹůŭŮŶűŮ

o

ʼnŹžűūűŹŷūũŻƅŻŮųżƂűŲŸŹŷŮųŻ
ŋŷźźŻũŶŷūűŻƅũŹžűūűŹŷūũŶŶƄŲŸŹŷŮųŻ
ŘŷųũŰũŻƅųŷŶŽűŬżŹũſűŷŶŶƄŮŸũŹũŵŮŻŹƄ*KLGUD źŵŬŴũūż
ŗŪźżůŭũŮŻźƈūŬŴũūŮ
ōŷŪũūűŻƅŽũŲŴūŸŹŷŮųŻ źŵŬŴũūż

p

ōŷŪũūűŻƅŶŮźųŷŴƅųŷŽũŲŴŷūūŸŹŷŮųŻ

q

ŗŻųŹƄŻƅŭŹŮūŷūűŭŶŷŮŸŹŮŭźŻũūŴŮŶűŮŽũŲŴŷūŷŲźűźŻŮŵƄ
ūŶŷūŷŵŷųŶŮ
ŋƄŲŻűűŰŸŹűŴŷůŮŶűƈ*KLGUD

Рис. 11.4. Меню File

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

Коллективная обратная разработка программ

311

ся вероятность непреднамеренного удаления. Чтобы удалить
проект, нужно выполнить три шага:
1) выбрать из меню команду FileDelete Project;
2) найти (или ввести имя) проект, подлежащий удалению;
3) подтвердить в открывающемся окне, что вы хотите удалить
проект.
При удалении проекта удаляются все относящиеся к нему
файлы. Поэтому имеет смысл предварительно архивировать
проект с помощью команды Archive Current Project .

Архивирование проектов
При архивировании создается моментальный снимок проекта,
содержащий все относящиеся к нему файлы и конфигурационные параметры инструментов. Для архивирования существуют
следующие причины:
► вы собираетесь удалить проект, но сохранить копию «на
всякий случай»;
► вы хотите упаковать проект для переноса на другой сервер;
► вам нужно обеспечить простой перенос между разными
версиями Ghidra;
► вы хотите создать резервную копию проекта.
Для архивирования проекта необходимо выполнить следующие действия:
1) закрыть окно браузера кода и всех инструментов;
2) выбрать из меню команду FileArchive Current Project;
3) выбрать имя и местоположение архивного файла на своей
локальной машине.
Если указанный файл уже существует, вам будет предоставлена возможность изменить имя или перезаписать существующий файл. Архивированные файлы легко восстановить с помощью команды Restore Project.

Пакетный импорт
Команда Batch Import ( на рис. 11.4) позволяет за один раз
импортировать сразу несколько файлов. Ghidra открывает окно
браузера, показанное на рис. 11.5. Здесь вы можете перейти
в каталог, содержащий нужные файлы.
312

ГЛАВА 11

Рис. 11.5. Окно выбора подлежащих импорту файлов, несколько
файлов выделено
Можно выбрать один или несколько файлов из одного каталога или весь каталог. Выбрав файлы и нажав кнопку Select
files, вы окажетесь в окне пакетного импорта, где будут показаны уже выбранные для импорта файлы. На рис. 11.6 файлы из каталога BinBatchImport1 выбирались по отдельности,
а каталог BinBatchImport2, содержащий пять файлов (это видно справа от имени каталога), был добавлен целиком. Файлы
можно добавлять в список импорта и удалять из него. Имеется
также несколько параметров, управляющих импортом, в том
числе глубина рекурсии при просмотре каталогов.
Чтобы определить необходимую глубину просмотра в окне
пакетного импорта или просто побродить по файловой системе, воспользуйтесь командой меню Open File System ( на
рис. 11.4). Она открывает в отдельном окне выбранный контейнер файловой системы (zip-файл, tar-файл, каталог и т. д.).
(Лучше определить глубину заранее, потому что для работы
в обоих окнах одновременно вам понадобится второй экземп­
ляр Ghidra. При наличии единственного экземпляра одно окно
блокирует доступ к другому.)

Коллективная обратная разработка программ

313

Рис. 11.6. Диалоговое окно подтверждения пакетного импорта

Меню Edit
Меню Edit показано на рис. 11.7. Пункты Tool Options
и Plugin Path будут рассмотрены в главе 12, но параметры
инфраструктуры открытых ключей (PKI) относятся к настройке сервера Ghidra, так что им место в этой главе.

řŮŭũųŻűŹŷūũŻƅŸũŹũŵŮŻŹƄűŶźŻŹżŵŮŶŻŷū źŵŬŴũūż
ŘũŹũŵŮŻŹƄŸŴũŬűŶŷū źŵŬŴũūż
ŗƀűźŻűŻƅűżźŻũŶŷūűŻƅźŮŹŻűŽűųũŻƄ3.,
ŭŴƈũżŻŮŶŻűŽűųũſűű

Рис. 11.7. Меню Edit

314

ГЛАВА 11

Сертификаты PKI
В начале этой главы мы говорили, что при настройке сервера
Ghidra можно выбрать метод аутентификации. Мы сконфигурировали простой сервер, в котором для аутентификации используются имя пользователя и пароль. Сертификаты PKI сложнее.
Есть разные реализации PKI, но в следующем примере описан
разумный процесс аутентификации клиента сервера Ghidra:
Пользователь User1 хочет аутентифицироваться для работы со своим проектом на сервере Ghidra. У него имеется клиентский сертификат, содержащий имя и открытый
криптографический ключ. У него также имеется закрытый
ключ, соответствующий открытому ключу в сертификате,
который безопасно хранится для таких важных случаев,
как этот. Сертификат подписан удостоверяющим центром
(УЦ), которому сервер Ghidra доверяет.
User1 предъявляет серверу свой сертификат, из которого
сервер может извлечь открытый ключ и имя пользователя.
Сервер проверяет, что сертификат действителен (что он отсутствует в списке отозванных сертификатов, что срок еще не истек, что подпись доверенного УЦ не подделана и, возможно,
что-то еще). Если все проверки проходят успешно, то сервер
подтверждает действительность сертификата и связывает
личность User1 с открытым ключом. Теперь User1 должен
доказать, что обладает соответствующим закрытым ключом,
который сервер Ghidra мог бы сверить с уже известным ему
открытым ключом. Если User1 и вправду располагает закрытым ключом, то он считается аутентифицированным.
Процесс управления удостоверяющими центрами описан
в файле server/svrREADME.html. Команды меню Set PKI
Certificate и Clear PKI Certificate позволяют пользователю
связать с собой (или отвязать от себя) файл с ключом (*.pfx,
*.pks, *.p12). При установке сертификата PKI пользователю
предлагается окно навигации по файловой системе, в котором
он может выбрать подходящее хранилище ключей. В любой момент сертификат можно очистить, выбрав команду Clear PKI
Certificate. Если вы выберете PKI-аутентификацию, то для
управления ключами, сертификатами и хранилищами ключей
можно использовать написанную на Java утилиту keytool.
Коллективная обратная разработка программ

315

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

ŗŻųŹƄŻƅŸŹŷŮųŻűŴűŹŮŸŷŰűŻŷŹűŲŻŷŴƅųŷŭŴƈƀŻŮŶűƈ

n

ŘŷųũŰũŻƅźŸűźŷųŶŮŭũūŶŷŷŻųŹƄŻƄžŸŹŷŮųŻŷūűŹŮŸŷŰűŻŷŹűŮū
ŐũųŹƄŻƅūƄŪŹũŶŶŷŮŸŹŮŭźŻũūŴŮŶűŮŸŹŷŮųŻũűŴű
ŹŮŸŷŰűŻŷŹűƈŷŻųŹƄŻŷŮŭŴƈƀŻŮŶűƈ
ŕŮŶƇŸũŹũŵŮŻŹŷūŹũŪŷƀŮŬŷŸŹŷźŻŹũŶźŻūũ źŵŬŴũūż

o

őŰŵŮŶűŻƅŸũŹŷŴƅżƀũźŻŶűųũŹũŰŭŮŴƈŮŵƄžŸŹŷŮųŻŷū

p

ŘŷųũŰũŻƅŻũŪŴűſżųŷŶŻŹŷŴƈŭŷźŻżŸũŭŷŸżźųũƇƂżƇŹŮŭũųŻűŹŷūũŶűŮ
ũŭŵűŶűźŻŹũŻŷŹũŵűŸŹŷŮųŻũűŻŷŴƅųŷƀŻŮŶűŮūźŮŵűŷźŻũŴƅŶƄŵű

q

ŘŷųũŰũŻƅŸŷŭŹŷŪŶżƇűŶŽŷŹŵũſűƇŷŻŮųżƂŮŵŸŹŷŮųŻŮ

Рис. 11.8. Меню Project

Просмотр проектов и репозиториев
Первые четыре пункта  относятся к просмотру проектов
и репозиториев. Из них первые два, View Project и View
Repository, открывают проект (локальный) или репозиторий
(на удаленном сервере) в новом окне рядом с окном активного проекта, допускающим только чтение. На рис. 11.9 локальный проект ExtraFiles открыт бок о бок с активным проектом.
Открытый для чтения проект можно изучать или перетащить
из него любой файл либо каталог в окно активного проекта.
На рис. 11.9 три выбранных файла (с расширением NEW) были
скопированы из окна данных проекта в активный проект CH11.
Следующий пункт, View Recent, показывает список недавних проектов, чтобы ускорить процедуру поиска проекта или репозитория. Пункт Close View закрывает открытое
для чтения представление (хотя в некоторых версиях Ghidra
этот пункт выглядит неактивным). Более простой и надежный
способ закрыть проект – щелкнуть по значку X на язычке его
вкладки, показанной справа внизу на рис. 11.9.
316

ГЛАВА 11

Рис. 11.9. Использование окна проекта для просмотра еще
одного проекта

Изменение паролей и управление доступом
к проекту
Пункт Change Password ( на рис. 11.8) доступен только
участникам разделяемых проектов, при условии что в сервере
Ghidra сконфигурирован метод аутентификации по паролю.
Это двухшаговая процедура, в которой сначала запрашивается
подтверждение, как показано на рис. 11.10, а затем открывается такое же окно, как при изменении первоначального пароля.

Рис. 11.10. Диалоговое окно подтверждения смены пароля
Коллективная обратная разработка программ

317

Любой пользователь может контролировать собственный пароль, но в разделяемых проектах есть также возможность задавать, кто имеет доступ к проекту и какими правами обладает каждый пользователь. Как отмечалось в этой главе ранее,
администратор сервера Ghidra располагает некоторыми средствами контроля доступа. Именно, системный администратор
может назначать администраторов отдельных репозиториев,
а также создавать и удалять учетные записи пользователей.
На стороне клиента администратор тоже может управлять
доступом с помощью пункта Edit Project Access List ( на
рис. 11.8) в меню Project. При выборе этого пункта открывается окно на рис. 11.11, которое позволяет добавлять и удалять
пользователей проекта и задавать их права. Каждый пользователь может быть помещен ровно в один класс, от наименее
привилегированного (Read Only слева) до наиболее привилегированного (Admin справа).

Рис. 11.11. Окно контроля доступа

Просмотр информации о проекте
Последним в меню значится пункт View Project Info ( на
рис. 11.8). Состав диалогового окна зависит от того, размещен
ли проект на сервере Ghidra. На рис. 11.12 показаны диалоговые окна информации о проекте на локальной машине (слева) и на сервере (справа). Поля не нуждаются в пояснениях,
обратим лишь внимание на кнопки в нижней части каждого
окна: кнопка Convert to Shared позволяет преобразовать не318

ГЛАВА 11

разделяемый проект в разделяемый, а кнопка Change Shared
Project Info – изменить параметры проекта.
При нажатии кнопки Convert to Shared открывается диалоговое окно, в котором предлагает задать информацию о сервере и ввести имя и пароль пользователя, который станет
администратором проекта. На последующих шагах задается
репозиторий, добавляются пользователи, определяются их
права и подтверждается намерение преобразовать проект. Отметим, что эту операцию нельзя отменить и что вся накопленная локальная история удаляется.

Рис. 11.12. Окна информации о неразделяемом и разделяемом
проектах

РЕПОЗИТОРИЙ ПРОЕКТА
Наверное, у вас уже возник вопрос, как можно разделять проекты и при этом поддерживать их целостность. В этом разделе описывается процедура, благодаря которой Ghidra гарантирует, что в разделяемом проекте работа каждого члена
коллектива сохраняется. Но прежде чем переходить к деталям, поговорим о типах файлов, относящихся к разделяемому
Коллективная обратная разработка программ

319

проекту Ghidra. Начнем с обсуждения связи между проектом
и репозиторием.
Репозиторий – ключ к системе версионирования. При создании нового неразделяемого проекта создается файл проекта (gpr-файл) и каталог репозитория с расширением .rep. Есть
и другие файлы для управления блокировками, версионированием и т. д., но для успешной работы с Ghidra понимать назначение каждого необязательно. В случае неразделяемых проектов все файлы хранятся на вашем компьютере в каталогах,
которые вы указали в момент создания проекта (см. главу 4).
При создании разделяемого проекта вы можете либо создать
новый репозиторий, либо выбрать один из существующих, как
обсуждалось выше в этой главе (см. рис. 11.2). Если одновременно создаются новый проект и новый репозиторий, то между
ними существует взаимно однозначное соответствие, и вы становитесь администратором проекта. Если выбран существующий репозиторий, то вы не являетесь администратором созданного проекта (если только не владеете репозиторием). В любом
случае у gpr-файла и у rep-каталога базовое имя одинаково.
Если репозиторий называется RepoExample, то файл проекта
будет называться RepoExample.gpr, а каталог репозитория –
RepoExample.rep. (Несмотря на наличие расширения, репозиторий является каталогом, а не файлом.)
Подведем итог: если вы создаете репозиторий, то являетесь
администратором проекта и можете решать, кто еще будет
иметь доступ крепозиторию. Если же вы используете сущест­
вующий репозиторий, то являетесь пользователем с правами,
которые вам предоставил администратор проекта. Так что же
происходит, когда несколько пользователей изменяют один
и тот же проект? Тут в игру вступает управление версиями.

320

ГЛАВА 11

Управление версиями
и отслеживание версий
Ghidra включает две совершенно разные системы версионирования. В этой главе мы обсуждаем управление версиями и надеемся, что очень скоро вы поймете, в чем ее суть. Но в Ghidra
есть еще механизм отслеживания версий. Он используется, когда нужно найти различия (и сходства) между двумя двоичными
файлами. В сообществе SRE эта процедура обычно называется
двоичной дельтой. Цели могут быть разными: понять, чем отличаются две версии одного двоичного файла, идентифицировать
функции, используемые семейством вредоносных программ, выявить сигнатуры и т. д. Эта функциональность важна, учитывая,
что исходный код разных версий, который можно было бы сравнить, обычно недоступен. Отслеживание версий в Ghidra подробно рассматривается в главе 23.

Управление версиями
Управление версиями – важное средство в любой системе, в которой изменения могут вносить несколько пользователей или
желательно хранить историю изменений. Система управления
версиями берет на себя контроль над обновлениями, препятствуя возникновению гонок. В окне проекта имеется панель инструментов управления версиями (рис. 11.13). Для многих операций требуется, чтобы обрабатываемые файлы были закрыты.

Рис. 11.13. Панель инструментов управления версиями в окне
проекта
Значки доступных в данный момент операций над выбранными файлами активны. Основные операции системы управления версиями показаны на рис. 11.14 (мы включили столбец,
содержащий приблизительные эквиваленты в Git, для поклонников этой системы).
Коллективная обратная разработка программ

321

ŐŶũƀŷų

ōŮŲźŻūűŮ

ŚŸŮſűũŴƅŶƄŮŸũŹũŵŮŻŹƄ

ŘŷžŷůűŮųŷŵũŶŭƄJLW

ōŷŪũūűŻƅŽũŲŴūźűźŻŮŵż
żŸŹũūŴŮŶűƈūŮŹźűƈŵű

ŗźŻũūűŻƅŽũŲŴ
űŰūŴŮƀŮŶŶƄŵ

JLWDGG
JLWFRPPLW

őŰūŴŮƀƅŽũŲŴ

ŖŮŻ

JLWFORQH LVK

ŘŷŴżƀűŻƅŸŷźŴŮŭŶƇƇ
ūŮŹźűƇűŰūŴŮƀŮŶŶŷŬŷ
ŽũŲŴũ

ŗźŻũūűŻƅŽũŲŴűŰūŴŮƀŮŶŶƄŵ

ŋŷŰūŹũŻűŻƅŽũŲŴ

ŚŷŰŭũŻƅNHHSŽũŲŴ
ŗźŻũūűŻƅŽũŲŴűŰūŴŮƀŮŶŶƄŵ
ŚŷŰŭũŻƅNHHSŽũŲŴ

JLWSXOO

JLWFRPPLW
JLWSXVK

ŗŻŵŮŶűŻƅűŰūŴŮƀŮŶűŮ

ŚŷžŹũŶűŻƅųŷŸűƇ
źŹũźƁűŹŮŶűŮŵNHHS

JLWFKHFNRXW

řŮųżŹźűūŶŷŶũŲŻűūźŮ
ŵŷűűŰūŴŮƀŮŶŶƄŮŽũŲŴƄ

ŖŮŻ

JLWVWDWXV

Рис. 11.14. Значки на панели инструментов управления версиями
Помимо использования значков на панели инструментов,
действия по управлению версиями можно выполнять с по­
мощью контекстного меню.

Объединение файлов
Когда член коллектива пытается поместить в систему измененный им файл проекта, может сложиться одно из двух условий.
► Конфликт отсутствует. В этом случае с момента извлечения файла никто не создавал его новых версий.
Поскольку никакого потенциального конфликта не существует (нет зафиксированных конфликтующих изменений, о которых пользователь не знал), то возвращаемый файл становится новой версией. Старая версия тоже
хранится, а номер версии увеличивается, чтобы цепочку
версий можно было проследить.
► Потенциальный конфликт. В этом случае другой пользователь зафиксировал изменения, пока первый пользователь работал с извлеченным файлом. От порядка возврата файлов может зависеть результирующая «текущая
322

ГЛАВА 11

версия». В этой ситуации Ghidra начинает процесс объединения. Если обе версии не конфликтуют между собой,
то Ghidra завершает автоматический процесс объединения. Если же обнаружены конфликты, то они должны
быть вручную разрешены пользователем.
В качестве примера конфликта предположим, что пользователи user1 и user2 извлекли один и тот же файл и user2 изменил
имя FUN_00123456 на hash_something и зафиксировал свое изменение. Тем временем user1 проанализировал ту же функцию
и назвал ее compute_hash. Когда user1, наконец, захочет зафиксировать сделанные изменения (после user2), система проинформирует его о конфликте имен и предложит выбрать, какое
имя оставить: hash_something или compute_hash, – и только после
этого операция фиксации может быть доведена до конца. Дополнительные сведения об этом процессе см. в справке по Ghidra.

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

Рис. 11.15. Диалоговое окно комментария после
добавления файла в систему управления версиями
Коллективная обратная разработка программ

323

В полосе заголовка показано выполняемое действие, а под ним
имеется еще значок и описание того, что нужно ввести в поле
Comments. Если было выбрано больше одного файла, то комментарии будут относиться к первому, если только не нажата
кнопка Apply to All (Применить ко всем). Под полем Comments
располагаются дополнительные параметры операции, которые
пользователь может задать или не задавать. Эти параметры описаны в третьем столбце на рис. 11.4.

Пример
В вопросе о разделяемых проектах много тонкостей, вариантов
и перегруженной терминологии. Чтобы прояснить некоторые
понятия, связанные с сервером Ghidra и разделяемыми проектами, подробно рассмотрим пример, на котором продемонстрируем все, что обсудили, начиная с концепции проекта.
Проект – это локальная сущность, размещенная на клиентской машине (как локальный репозиторий Git). С разделяемыми проектами также связан репозиторий на сервере
Ghidra (как на удаленном сервере Git), и именно в нем хранятся все результаты коллективного анализа. Файлы становятся
общими после импорта и добавления в систему управления
версиями, а до того остаются частными. Таким образом, пользователь может импортировать файлы в проект, и они будут
оставаться его частной собственностью, пока он не добавит их
в систему управления версиями, после чего они становятся
общим достоянием.

Караул!
У меня похитили файл!
В Ghidra имеется специальный термин (и значок в дереве данных проекта) для ситуации, часто возникающей в разделяемой
среде работы над проектами. Если у вас имеется частный файл
(импортированный, но еще не добавленный в систему управления версиями) и другой пользователь добавил файл с таким
же именем в репозиторий, то говорят, что ваш файл похитили
(hijack)! Это такая частая проблема, что в контекстное меню
включен специальный пункт для ее решения. Вы должны закрыть похищенный файл, а затем выбрать команду Undo Hijack

324

ГЛАВА 11

(Отменить похищение) из контекстного меню. В ответ будет предложена возможность принять файл в репозитории и сохранить
копию собственного файла, если необходимо. Другие варианты
разрешения проблемы – переименовать файл, переместить его
в другой проект или удалить.

Права доступа к проекту в действительности являются правами доступа к репозиторию. Создавая проект с существующим
репозиторием, вы на самом деле говорите: «За этим локальным проектом стоит репозиторий на удаленном сервере» (как
в случае клонов в Git). Пройдем по шагам последовательность
операций с разделяемым проектом и посмотрим, как они отражаются на разделяемой среде.
1. Пользователь user1 создает новый разделяемый проект
(и ассоциированный новый репозиторий) CH11-Example,
добавляет пользователей user2 и user3 и предоставляет им
права (см. рис. 11.16).

Рис. 11.16. Пример, шаг 1
2. Пользователь user2 создает новый разделяемый проект,
связанный с репозиторием CH11-Example (т. е. user2 клонирует CH11-Example). Заметим, что это не тот же проект,
что создал user1, но репозиторий (удаленный) у них один
и тот же. Дополнительно в нижней части окна показаны
права user2 в репозитории (рис. 11.17).
Коллективная обратная разработка программ

325

Рис. 11.17. Пример, шаг 2
3. Пользователь user1 импортирует файл и добавляет его
в систему управления версиями, user2 тоже может видеть
этот файл (приблизительный эквивалент команд git add/
commit/push). Это показано на рис. 11.18.

Рис. 11.18. Пример, шаг 3
326

ГЛАВА 11

4. Затем user1 и user2 импортируют в свои проекты один и тот
же файл, но не добавляют их в систему управления версиями. Это их частные файлы (см. рис. 11.19).

Рис. 11.19. Пример, шаг 4
5. user2 добавляет этот второй файл в систему управления
версиями (и тем самым фиксирует его). Теперь файл уже
не частный. user1 видит его как похищенный файл (см.
рис. 11.20).

Рис. 11.20. Пример, шаг 5

Коллективная обратная разработка программ

327

6. user1 выбирает команду Undo Hijack из контекстного
меню и может заменить свой файл версией из репозитория
и при желании сохранить копию. Он решает согласиться
с версией в репозитории, а свой файл скопировать (он перемещен в другой проект и теперь имеет расширение .keep).
Все снова наладилось. В данном случае user1 видит файл
в том состоянии, в каком он находился, когда user2 добавил
его в систему управления версиями (см. рис. 11.21).

Рис. 11.21. Пример, шаг 6
7. user1 извлекает второй файл, анализирует его и возвращает. Теперь user1 и user2 видят проанализированную версию
файла (версию 2), как показано на рис. 11.22.

Рис. 11.22. Пример, шаг 7
328

ГЛАВА 11

8. user3 создает проект и связывает его с тем же самым репозиторием (рис. 11.23). Теперь user3 видит все файлы и может вносить изменения локально (в т. ч. добавлять частные
файлы), но не может ничего поместить в репозиторий, потому что права записи у него нет (в нижней части окна имеется примечание Read Only, означающее, что проект открыт
только для чтения).

Рис. 11.23. Пример, шаг 8
9. user2 возвращает все свои файлы, перед тем как пойти домой. Это важно, потому что он собирается продолжить работу над проектом из дома. Поскольку на его домашнем
компьютере проекта нет, ему нужно будет зайти на сервер
Server и создать проект, связав его с существующим репозиторием. Это даст ему возможность работать из дома. (Если
бы он не возвратил все файлы, перед тем как уйти с работы,
то не смог бы получить из дома доступ к последней версии.)
10. Остальные пользователи уходят домой в полной уверенности, что их коллективный сервер Ghidra работает как надо.

Коллективная обратная разработка программ

329

РЕЗЮМЕ
Не всем нужен сервер Ghidra для коллективной обратной разработки, но многие его возможности применимы и к неразделяемым проектам. В оставшихся главах мы будем говорить
в основном о неразделяемых проектах, упоминая разделяемые
и сервер Ghidra по мере необходимости. Но какую бы конфигурацию Ghidra вы ни выбрали, велики шансы, что настройки,
инструменты и представления, подразумеваемые по умолчанию, не отвечают вашему технологическому процессу. В следующей главе мы будем рассматривать конфигурации, инструменты и рабочие пространства Ghidra и опишем, как заставить
их работать лучше.

12

НАСТРОЙКА GHIDRA

Попривыкнув к Ghidra, вы, наверное, захотите
«заточить» под себя параметры по умолчанию,
предлагаемые при открытии каждого нового проекта или применяемые ко всем файлам в проекте.
Вас, возможно, удивляет, почему некоторые измененные вами параметры сохраняются на протяжении многих сеансов, тогда как другие нужно заново устанавливать при каждом
создании проекта или загрузке файла. В этой главе мы рассмот­
рим, как можно настроить внешний вид и поведение Ghidra по
умолчанию, чтобы они лучше отвечали вашим потребностям.
Чтобы понимать область действия различных настроек, полезно уяснить (расплывчатое) различие между плагином и инструментом. Вообще говоря, справедливы следующие утверждения.
► Плагин. Это программный компонент (например, средство просмотра байтов, окно листинга и т. д.), который
расширяет функциональность Ghidra. Плагины часто
предстают в виде окон, хотя многие работают за кулисами (например, анализаторы).

► Инструмент. Это может быть один плагин или несколько плагинов, работающих совместно. Обычно они предлагают полезный графический интерфейс (GUI), помогающий пользователям решать их задачи. Инструмент,
с которым мы много работали, браузер кода, выглядит
как окно, играющее роль графического каркаса. Граф
функции – тоже инструмент.
Не впадайте в панику, если вам кажется, что Ghidra не следует этим определениям строго. Часто различие между тем
и другим вообще несущественно. Например, некоторые меню,
в частности Tool Options (Параметры инструментов), обсуждаемое ниже в этой главе, содержат параметры, которые в равной мере применимы и к инструментам, и к плагинам, хотя
используется термин «инструмент». В этом контексте, как и во
многих других, различие не важно, поскольку обращение с инструментами и плагинами одинаковое. Вы сможете успешно
настроить систему под себя, несмотря на то что термины употребляются не слишком формально.
Помимо настройки Ghidra, мы обсудим рабочие пространства. Рабочее пространство соединяет инструмент с конфигурацией и позволяет спроектировать и использовать персонализированный виртуальный рабочий стол.

БРАУЗЕР КОДА
В главах 4 и 5 мы познакомились с браузером кода и многими
связанными с ним окнами. Мы уже рассматривали некоторые
параметры, а теперь разберем более полный пример настройки браузера, прежде чем переходить к окну проекта и рабочим
пространствам.

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

ГЛАВА 12

► Закрыть. Для закрытия окон служит значок X в правом
верхнем углу окна. (Если заново открыть закрытое окно,
то оно появится в том же месте, а не в первоначальной
позиции, подразумеваемой по умолчанию.)
► Переместить. Окна перемещаются путем буксировки.
► Стопка. Пользуйтесь буксировкой, чтобы расположить
окна стопкой (одно под другим) или разобрать стопку.
► Изменение размера. Если задержать мышь на границе
между двумя окнами, то появится стрелка, которая позволяет увеличивать и уменьшать окна по разные стороны
от границы.
► Отстыковать. Инструмент можно отстыковать от окна
браузера кода, но пристыковать его обратно не так прос­
то, как хотелось бы (см. рис. 12.1).




Рис. 12.1. Обратная пристыковка окна декомпилятора
Чтобы пристыковать окно, не нужно щелкать по полосе заголовка , поскольку тогда вы просто будете буксировать окно по
экрану поверх браузера кода. Вместо этого щелкните по внутренней полосе заголовка  – это позволит пристыковать окно или поместить его в стопку. Научившись реорганизовывать окна, давайте займемся их настройкой с помощью меню EditTool Options.
Настройка Ghidra

333

Редактирование параметров
инструментов
При выборе пункта EditTool Options открывается окно параметров браузера кода, показанное на рис. 12.2. Оно позволяет управлять параметрами отдельных компонентов брау­
зера кода.
Состав параметров определяется разработчиками компонентов, а их разнобой отражает специфику различных инструментов. Поскольку для описания всех параметров понадобилась бы
отдельная книга, мы рассмотрим лишь несколько – те, что влия­
ют на инструменты, которые обсуждались в предыдущих главах, и те, что используются во многих инструментах, но похожи.

Рис. 12.2. Окно EditTool Options, открываемое из браузера кода
Многие инструменты по умолчанию отображаются оттенками серого, поэтому не сразу понятно, что они могут быть цвет334

ГЛАВА 12

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

Рис. 12.3. Диалоговое окно редактора цветов
На рис. 12.3 можно выбрать цвета шести элементов в окне
средства просмотра байтов: Block Separator (Разделитель
блоков), Current View Cursor (Курсор в текущем представлении), Cursor (Курсор), Edit Cursor (Курсор редактирования),
Highlight Cursor Line (Подсветка строки с курсором) и NonFocus Cursor (Курсор не в фокусе). Помимо настройки цветов
в окне средства просмотра байтов, можно также задать шрифт
и указать, нужно ли подсвечивать строку с курсором. Удобно, что в любом окне параметров инструментов браузера кода
имеется кнопка Restore Defaults (Восстановить значения по
умолчанию) в правом нижнем углу. Это дает возможность использовать специальные цветовые схемы на каких-то шагах
анализа, а затем вернуться к схеме по умолчанию.
Помимо косметических изменений, можно задать параметры
многих инструментов. Мы уже намекали на этот потенциал,
когда знакомились с новой функциональностью в предыдущих
главах, например возможностью управлять тем, какие анализаторы будут участвовать в автоматическом анализе. В общем
Настройка Ghidra

335

случае, когда некий параметр имеет значение по умолчанию,
существует способ изменить его на что-то иное.
Параметры некоторых ключевых инструментов также допус­
кают модификацию в окне Options. Например, раздел Key
Bindings (Назначения клавиш) позволяет назначить горячие
клавиши действиям в окне браузера кода (всего их больше
550). Назначение горячих клавиш полезно во многих случаях,
например чтобы выполнять дополнительные команды с клавиатуры, изменять клавиши по умолчанию на другие, которые
легче запомнить или не конфликтующие с горячими клавишами, используемыми операционной системой или приложением
терминала. Можно даже переназначить все клавиши, так чтобы они совпадали с используемыми в других дизассемблерах.
На рис. 12.4 показаны три поля, определяемых для каждого
назначения клавиши. Первое – Action Name (Имя действия).
Иногда имя действия совпадает с названием пункта меню (например, AnalysisAuto Analyze). А иногда это параметр
команды меню (например, Aggressive Instruction Finder
(Агрессивный поиск команд) в меню Analysis Options).

Рис. 12.4. Назначение клавиш в окне EditTool Options
Во втором столбце показана назначенная действию горячая клавиша, а в последнем имя плагина, в котором реализовано действие1. Не всем действиям назначены клавиши, но
1

336

Это еще один пример, когда термины употребляются как синонимы, если
различие несущественно. В столбце Plugin Name указаны в основном
плагины, но встречаются и инструменты, например Configure. Кстати,
назначать клавиши можно тем и другим.

ГЛАВА 12

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

Редактирование параметров инструмента
Последним в окне EditTool Options является пункт Tool.
Его семантика зависит от инструмента, из меню которого было
открыто диалоговое окно параметров. Чаще всего это окно
браузера кода или проекта. На рис. 12.5 показаны конфигурационные параметры браузера кода по умолчанию. В полосе
заголовка диалогового окна ясно сказано, что мы смотрим на
страницу параметров браузера кода.

Рис. 12.5. Окно EditTool OptionsTool редактирования
параметров браузера кода
Настройка Ghidra

337

Специальные средства редактирования
для некоторых инструментов
Для некоторых инструментов средства редактирования интег­
рированы в их окна, так что эффект изменения параметров
виден сразу же. Самым широким набором таких встроенных
средств обладает окно листинга. Оно содержит дизассемблированный код в текстовом виде и конфигурируется с помощью
форматера полей браузера, описанного в разделе «Изменение
параметров отображения кода» главы 7. На рис. 12.6 показано
окно листинга с открытым форматером по умолчанию.

n
o

Рис. 12.6. Окно листинга с открытым форматером полей
браузера по умолчанию

338

ГЛАВА 12

В верхней части форматера находится строка вкладок ,
представляющих различные типы полей, встречающихся
в лис­тинге дизассемблера. В данном случае мы смотрим на
команды, поэтому выбрана вкладка Instruction/Data (Команды/Данные). Кнопки в оставшейся части форматера  относятся к отдельным полям в секции команд и данных. В данном
случае курсор находится в поле адреса в листинге, поэтому выделена кнопка Address.
Форматер полей браузера можно использовать для изменения внешнего вида листинга. Возможности обширны, и у каждого поля есть собственный набор параметров. Мы рассмотрим
некоторые возможности попроще, напоминающие настройку
внешнего вида окон браузера кода. Поля можно реорганизовывать, перетаскивая их в новое место, можно увеличивать
и уменьшать ширину поля; поля можно добавлять, удалять,
активировать и деактивировать.
На рис. 12.7 показано содержимое того же листинга после
удаления поля Bytes. Это поле было удалено в большинстве
изображений листингов в предыдущих главах, чтобы сделать
листинг компактнее и показать на имеющейся площади больше полезных вещей.

Рис. 12.7. Окно листинга с настроенным форматером полей
браузера
Настройка Ghidra

339

Сохранение конфигурации браузера кода
При закрытии браузера кода можно сохранить вместе с файлом
все изменения конфигурации. Можно выйти и без сохранения,
тогда будет выдано предупреждение о возможных последствиях. Если выполнить команду FileSave Tool в окне браузера
кода, то текущий вид браузера будет ассоциирован с текущим
файлом в активном проекте. При следующем открытии этого файла Ghidra воспользуется сохраненной конфигурацией
браузера. Если одновременно открыто несколько экземпляров
браузера кода и некоторые (или все) модифицированы, то возможен конфликт конфигураций инструмента. Тогда Ghidra
откроет диалоговое окно сохранения инструмента, показанное
на рис. 12.8.

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

ОКНО ПРОЕКТА В GHIDRA
Переключим передачу и вернемся к окну проекта, показанному на рис. 12.9. Главное меню мы обсуждали в предыдущей
главе. Но прежде чем перейти к настройке, взглянем на две
области этого окна, которых пока не касались.

340

ГЛАВА 12

n

o

p
Рис. 12.9. Окно проекта
В области Tool Chest  (Ящик с инструментами) отображаются значки всех инструментов, способных работать с импортированными в проект двоичными файлами. По умолчанию
доступно два инструмента. Значок дракона обозначает браузер
кода, а значок следов – систему управления версиями, встроенную в Ghidra. Ниже мы покажем, как пополнить ящик с инструментами путем модификации и импорта инструментов,
а также создания собственных.
В области Running Tools  (Работающие инструменты) находятся значки, соответствующие каждому экземпляру запущенных инструментов. В данном случае каждый файл проекта
открыт в отдельном окне браузера кода, поэтому запущено четыре экземпляра. Щелчок по любому значку переместит окно
соответствующего инструмента на передний план.
Настройка Ghidra

341

Вернемся в меню окна проекта  и рассмотрим некоторые
возможности настройки окна. Начнем с изучения четырех действий в окне EditTool Options, показанном на рис. 12.10.
Два из них такие же, как в браузере кода: Key Bindings и Tool.
На рис. 12.10 показано, как выглядит окно, когда выбрано действие Key Bindings. У инструмента «Проект» гораздо
меньше действий, чем у браузера кода, поэтому и вариантов
назначения клавиш тоже меньше. Если вы уже успели самостоятельно поэкспериментировать, то, наверное, заметили, что
большинство действий связаны с плагином FrontEndPlugin.
(Инструмент «Проект» в Ghidra называется также Frontend,
и эти термины употребляются как синонимы во всей среде
Ghidra, включая и справку.)

Рис. 12.10. Окно EditTool Options для проекта Ghidra (также
Ghidra Frontend)
342

ГЛАВА 12

Интеграция с Eclipse – тема главы 15, поэтому мы отложим
обсуждение этого пункта. Пункт Recovery (Восстановление)
просто позволяет задать частоту сохранения снимков. По умолчанию сохранение производится раз в 5 минут. Если задать
значение 0, то сохранение снимков отключается.
А вот с последним пунктом, Tool, поэкспериментировать интересно. Мы уже говорили, что в этом контексте термин tool
относится к активному инструменту. В данном случае таковым
является инструмент «Проект». Относящиеся к нему парамет­
ры показаны на рис. 12.11. Нас будут интересовать параметры
Swing Look And Feel (Внешний облик Swing) и Use Inverted
Colors (Использовать инвертированные цвета), которые меняют внешний вид окон Ghidra.

Рис. 12.11. Параметры инструмента «Проект»
Комбинация инвертированных цветов с выбором элемента
Metal из списка Swing Look And Feel дает темную тему, популярную у многих специалистов по обратной разработке. Изменения вступят в силу после перезапуска Ghidra, и новые стили
распространяются на все окна Ghidra, включая окна браузера
кода и декомпилятора. Часть получившегося в результате окна
браузера кода показана на рис. 12.12.
Настройка Ghidra

343

Рис. 12.12. Часть окна браузера кода при использовании
темной темы
Теперь, зная, как настроить внешний облик Ghidra на свой
вкус, вернемся к меню File и посмотрим, что означает конфигурирование в этом контексте. Команда FileConfigure
отобра­жает три класса плагинов Ghidra (рис. 12.13). У каждого
класса свое назначение.
В класс Ghidra Core входят плагины, которые используются в конфигурации Ghidra по умолчанию. Они составляют
базовую функциональность, необходимую для обратной разработки. В класс Developer входят плагины, помогающие разрабатывать новые плагины. Это неплохая отправная точка
для тех, кто хочет больше узнать о разработке Ghidra. Последняя группа плагинов называется Experimental. Эти плагины еще не были тщательно протестированы и могут привести к нестабильной работе Ghidra, так что используйте их
с осторожностью.
344

ГЛАВА 12

Рис. 12.13. Классы плагинов Ghidra
По умолчанию после установки Ghidra активированы только
плагины из класса Ghidra Core, но вы можете активировать
и другие, отметив соответствующий флажок. Ссылка Configure
под названием класса позволит выбрать (или исключить) отдельные плагины в данном классе. На рис. 12.14 показан список плагинов в классе Ghidra Core с кратким описанием и указанием категории. Если щелкнуть по плагину в этом списке, то
в нижней части окна будет показана дополнительная информация о нем.
К конфигурированию Ghidra относятся еще два пункта меню
проекта. Первый, FileInstall Extensions (ФайлУстановить
расширения), мы будем обсуждать в главе 15. Второй,
EditPlugin Path (РедактированиеПуть к плагинам), позволяет добавлять, изменять и удалять пути к плагинам и тем
самым сообщать Ghidra, где искать дополнительные классы
Java, помимо установленных по умолчанию. После изменения
путей к плагинам Ghidra необходимо перезапустить, чтобы новые параметры вступили в действие.

Настройка Ghidra

345

Рис. 12.14. Окно конфигурирования Ghidra Core, в котором
выделен плагин ImporterPlugin
Познакомившись с возможностями изменения параметров
плагинов, перейдем к их расширенному использованию. Меню
Tools содержит действия, относящиеся к инструментам, в т. ч.
создание новых инструментов (если ни один из существующих
не отвечает вашим потребностям на сто процентов). В этом случае мы собираем инструменты из уже имеющихся плагинов,
вместо того чтобы писать новые плагины с нуля.

МЕНЮ TOOLS
На рис. 12.15 описана большая часть команд в меню Tools
окна проекта. До сих пор мы работали только с браузером
кода, который является инструментом по умолчанию для пер346

ГЛАВА 12

вичного анализа. А теперь покажем, как создавать в Ghidra
специальные инструменты.

ŚŷŰŭũŻƅŸżźŻŷŲűŶźŻŹżŵŮŶŻųŷŻŷŹƄŲŸŷŻŷŵŵŷůŶŷŪżŭŮŻ
ŶũŸŷŴŶűŻƅŸŴũŬűŶũŵű
ŐũŸżźŻűŻƅűŶźŻŹżŵŮŶŻűŰƈƂűųũźűŶźŻŹżŵŮŶŻũŵű
ŜŭũŴűŻƅűŶźŻŹżŵŮŶŻűŰƈƂűųũźűŶźŻŹżŵŮŶŻũŵű
řũŰŹŮƁűŻƅƆųźŸŷŹŻűŶźŻŹżŵŮŶŻũŭŴƈźŷūŵŮźŻŶŷŬŷ
űźŸŷŴƅŰŷūũŶűƈ
őŵŸŷŹŻűŹŷūũŻƅűŶźŻŹżŵŮŶŻƄŸŷżŵŷŴƀũŶűƇūźūŷŲ
ƈƂűųźűŶźŻŹżŵŮŶŻũŵű
őŵŸŷŹŻűŹŷūũŻƅűŶźŻŹżŵŮŶŻūƈƂűų
źűŶźŻŹżŵŮŶŻũŵű
ŚŷŰŭũŻƅũźźŷſűũſűűŵŮůŭżűŶźŻŹżŵŮŶŻũŵű źŵŬŴũūż
ʼnźźŷſűűŹŷūũŻƅŻűŸƄŽũŲŴŷūźųŷŶųŹŮŻŶƄŵűűŶźŻŹżŵŮŶŻũŵű

Рис. 12.15. Команды из меню Tools
Если вы экспериментировали с модификацией браузера
кода, то, наверное, вам не понравилось, что вид инструмента
по умолчанию изменяется при открытии последующих файлов. Рассмотрим частный случай, когда требуется исследовать
файл, содержащий много вызовов функций, в котором трудно ориентироваться. В главе 10 мы продемонстрировали, как
использование графов вызовов функций и графов функций
помогает понять поток управления в программе. Оба графа
открываются в отдельных окнах, и это само по себе создает
проблемы, когда открыто много файлов. Давайте решим эти
проблемы с помощью специализированного инструмента
ExamineControlFlow, который позволяет анализировать поток
управления.
При выборе пункта меню ToolsCreate Tool… открывается два окна (на рис. 12.16 они показаны друг под другом).
В верхнем окне показан список классов плагинов, похожий
на изображенный на рис. 12.13, но с одним дополнительным
классом, Function ID, который мы обсудим в главе 13. Ниже
расположено пустое, никак не озаглавленное окно разработки
инструмента, в котором мы будем создавать свой инструмент
ExamineControlFlow.
Настройка Ghidra

347

Рис. 12.16. Окно конфигурирования инструмента
Мы можем собрать новый инструмент из плагинов класса
Ghidra Core. После выбора класса окно разработки заполняется
плагинами из этого класса, как показано на рис. 12.17. Получившееся окно имеет много общего с браузером кода. Это и понятно, потому что браузер кода тоже основан на Ghidra Core.

Рис. 12.17. Новый неозаглавленный инструмент до начала
конфигурирования
348

ГЛАВА 12

Нам нужно будет удалить плагины, которые в новом инструменте лишние, а затем указать, какие окна мы хотим видеть.
Щелкните по ссылке Configure под названием класса Ghidra
Core и удалите следующие плагины (можно удалить еще много
других, но мы решили этого не делать для краткости):
► Console;
► DataTypeManagerPlugin;
► EclipseIntegrationPlugin;
► ProgramTreePlugin.
С каждым из этих плагинов связаны другие, поэтому при
удалении их из нового инструмента Ghidra будет выводить
предупреждение, содержащее список дополнительно удаляемых плагинов. Их можно в любой момент вернуть, выполнив
команду FileConfigure из меню нового инструмента. Пример предупреждения, выдаваемого при попытке удалить плагин DataTypeManagerPlugin, показан на рис. 12.18.

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

349

желаемые окна с помощью меню Window нового инструмента,
а затем перетащим их в нужные места. Новый, все еще не озаглавленный инструмент показан на рис. 12.19.

Рис. 12.19. Новый неозаглавленный инструмент
Поскольку мы планируем использовать этот инструмент часто,
да еще и вместе с коллегами, следует сохранить его, выбрав
пункт FileSave Tool As (ФайлСохранить инструмент
как), после чего нам будет предложено назвать инструмент
и выбрать для него значок (см. рис. 12.20). Можно взять один
из имеющихся значков или указать свой файл в одном из поддерживаемых форматов (например, .jpg, .png, .gif и т. д.).

Рис. 12.20. Значки для новых инструментов

350

ГЛАВА 12

Этот новый инструмент (и другие созданные вами) становится частью ящика с инструментами и будет отображаться в проектах, как показано на рис. 12.21.
Чтобы разделить новый инструмент с коллегами, экспортируйте командой ToolsExport Tools. Ghidra попросит указать, в какой папке сохранять инструмент, и создаст файл
с расширением .tool, содержащий описание инструмента. Для
импорта инструмента служит команда ToolsImport Tool.

Рис. 12.21. Новый проект – в ящике с инструментами виден
новый инструмент
Двойной щелчок по файлу в окне проекта Ghidra по умолчанию открывает файл в браузере кода, но можно вместо этого
щелкнуть по файлу правой кнопкой мыши и затем выбрать из
контекстного меню любой инструмент из ящика. Можно также
перетащить файл на инструмент в ящике.
Чем дольше вы работаете с Ghidra, тем яснее понимаете, что
не существует одного на любой случай интерфейса, который
предлагал бы именно те инструменты, которые нужны для решения конкретной задачи SRE. Подход к обратной разработке
сильно зависит от анализируемого файла, цели анализа и близости к конечной цели.
Мы посвятили значительную часть этой и предыдущих глав
описанию того, как адаптировать внешний облик Ghidra и ее
инструментов под свои потребности. Последний шаг настройки
Ghidra – сохранение созданной конфигурации, чтобы ее можно
было впоследствии выбрать для очередного проекта. Для этого
служат рабочие пространства.
Настройка Ghidra

351

РАБОЧИЕ ПРОСТРАНСТВА
Рабочее пространство в Ghidra можно рассматривать как виртуальный рабочий стол, включающий сконфигурированные
инструменты и файлы. Пусть требуется проанализировать двоичный файл. Глядя на файл, вы замечаете черты, напоминающие файл, который анализировали на прошлой неделе. Хотелось бы сравнить два файла и выявить сходство между двумя
функциями, но также хотелось бы продолжить анализ. Налицо
две разные задачи и один файл.
Один из способов гнаться сразу за двумя зайцами – создать
отдельное рабочее пространство для каждой задачи анализа. Вы можете сохранить текущий анализ, выбрав команду
ProjectWorkspaceAdd (ПроектРабочее пространство
Добавить) в окне проекта и присвоив имя новому рабочему пространству. В данном примере назовем его FileAnalysis.
Затем можно открыть другой инструмент из ящика – например, сравнить два файла с помощью специального инструмента Diff View (см. главу 23) и точно таким же способом
создать второе рабочее пространство (FileComparison). Между рабочими пространствами можно легко переключаться,
выбирая нужное из выпадающего списка, показанного на
рис. 12.22, или воспользовавшись командой Switch в меню
ProjectWorkspace, которая циклически перебирает имеющиеся рабочие пространства.

352

ГЛАВА 12

Рис. 12.22. Список рабочих пространств в окне проекта

РЕЗЮМЕ
В начале работы с Ghidra вас, возможно, вполне удовлетворяет ее поведение по умолчанию и организация браузера кода.
Но по мере накопления опыта вы наверняка захотите подстроить Ghidra под свой технологический процесс обратной разработки. Хотя в одной главе невозможно рассказать обо всех
возможностях, предлагаемых Ghidra, мы привели примеры некоторых средств настройки, которые, скорее всего, пригодятся
вам в работе. Открытие и исследование других полезных инструментов оставляем любопытным читателям.

Настройка Ghidra

353

13

РАСШИРЕНИЕ ВЗГЛЯДА
НА МИР GHIDRA

От высококачественного инструмента обратной
разработки мы ожидаем, в частности, полностью
автоматического распознавания и аннотирования максимально большой части двоичного файла.
В идеале должно быть идентифицировано 100 процентов функций, составляющих файл. У каждой из них должно быть имя и полный прототип, и, кроме того, должны быть
выявлены данные, с которыми работает функция, – только
тогда программист сможет составить полное представление об
исходных типах данных. В этом и заключается цель Ghidra,
точнее этапов импорта двоичного файла и последующего анализа. Все, с чем Ghidra не справилась на этих двух шагах, становится упражнением для ее пользователя.
В этой главе мы поговорим о том, как Ghidra распознает различные конструкции в двоичных файлах и как можно развить
ее способности в этом плане. Начнем с обсуждения процессов
начальной загрузки и анализа. От ваших решений зависит, какие ресурсы Ghidra привлечет для обработки файла. Это ваш
шанс предоставить Ghidra информацию, которую она, возможна, не смогла выявить автоматически; тогда на различных эта-

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

ИМПОРТ ФАЙЛОВ
Диалоговое окно, показанное на рис. 13.1, содержит собранную Ghidra во время загрузки информацию о файле. Вы можете изменить некоторые поля или согласиться с выводами
Ghidra. Дополнительные параметры, открывающиеся при
нажатии кнопки Options…, зависят от типа загружаемого
файла. На рис. 13.1 показаны параметры для PE-файла, а на
рис. 13.2 – параметры ELF-файла.

Рис. 13.1. Диалоговые окна импорта и параметров PE-файла

Рис. 13.2. Диалоговые окна импорта и параметров ELF-файла
356

ГЛАВА 13

Спецификация языка и компилятора
Поле Language (Язык) на рис. 13.1 и 13.2 говорит, как именно Ghidra
должна интерпретировать байты загружаемого файла, распознанные как машинный код. Спецификация языка и компилятора состоит из разделенных двоеточиями полей в количестве от трех до пяти:


в поле имени процессора задается тип процессора, для которого был собран двоичный файл. Оно направляет Ghidra
в определенный подкаталог каталога Ghidra/Processors;



в поле порядка байтов указывается порядок байтов в архитектуре процессора: прямой (LE) или обратный (BE);



в поле разрядности обычно указывается размер указателя
для выбранного процессора (16, 32 или 64 бита);



в поле варианта (режима) процессора задается конкретная
модель выбранного процессора или режим работы. Например, для процессора x86 возможны следующие режимы:
System Management Mode (режим управления системой),
Real Mode (реальный режим), Protected Mode (защищенный
режим) или default (по умолчанию). Для процессора ARM возможны модели v4, v4T, v5, v5T, v6, Cortex, v7, v8, v8T и другие;



поле компилятора содержит название компилятора, если
оно известно, и в некоторых случаях соглашение о вызове, использованное при компиляции. Возможные значения: windows, gcc, borlandcpp, borlanddelphi и default.

На рис. 13.3 спецификатор языка ARM:LE:32:v7:default разложен
на подполя. Одна из самых важных задач загрузчика – правильно
определить спецификацию языка и компилятора.
Язык
Процессор

Порядок
байтов

Размер

Вариант

ARM

LE

32

v7

Компилятор
Default

Рис. 13.3. Пример спецификации языка и компилятора
В поле Format описывается, какой загрузчик будет импортировать файл. Ghidra рассчитывает, что загрузчик знает все
детали формата, необходимые для определения характерис­
тик файла и выбора подходящих плагинов для его анализа.
Хорошо написанный загрузчик распознает специфическое содержимое или структурные особенности, по которым можно
определить тип файла, архитектуру и, если повезет, компиРасширение взгляда на мир Ghidra

357

лятор, создавший файл. Наличие информации о компиляторе
может повысить качество идентификации функций. Для решения этой проблемы загрузчик исследует структуру двоичного
файла, отыскивая в ней признаки конкретного компилятора
(например, количество, имена, позиции и порядок секций программы), или ищет специфические последовательности байтов
(например, блоки кода или строки). Так, нередко в двоичных
файлах, созданных gcc, присутствуют строки с информацией
о версии, например GCC: (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0.
Закончив процесс загрузки, Ghidra выводит окно сводки результатов импорта, показанное на рис. 13.4.



Рис. 13.4. Окно сводки результатов импорта двоичного ELF-файла
358

ГЛАВА 13

Из этой сводки видно, что ELF-файл требует библиотеку
lib.so.6  (отметим, что такого требования не было бы, если
бы файл был скомпонован статически). Если бы исполняемый файл зависел от нескольких разделяемых библиотек, то
в списке присутствовали бы все они. Знание о том, от каких
библиотек зависит файл, может подсказать, какие ресурсы понадобятся для анализа программы. Например, если в списке
требуемых библиотек встречаются libssl.so или libcrypto.so, то,
наверное, стоит найти документацию и, возможно, исходный
код OpenSSL. Как Ghidra может использовать исходный код,
мы обсудим ниже в этой главе. После того как файл успешно
импортирован, можно приступать к автоматическому анализу.

АНАЛИЗАТОРЫ
Автоматический анализ – результат совместной работы ряда
инструментов анализа (анализаторов), которые активируются
либо вручную (например, при открытии нового файла), либо
автоматически, когда обнаружено изменение, способное повлиять на результат дизассемблирования. Анализаторы работают последовательно в определенном порядке, потому что
изменения, сделанные одним анализатором, могут повлиять
на последующие. Например, анализаторы стека не могут рассматривать функции, пока анализатор функций не увидел все
вызовы и не создал функции. Мы рассмотрим эту иерархию
более подробно в главе 15, когда будем строить анализатор.
Когда вы открываете новый файл в браузере кода и просите автоматически проанализировать его, Ghidra предлагает
список анализаторов, применимых к этому файлу. Список подразумеваемых по умолчанию и дополнительных анализаторов
зависит от информации, полученной от загрузчика (она предъявляется пользователю в составе сводки импорта, показанной
на рис. 13.4). Например, анализатор RTTI для Windows x86 PE
вряд ли поможет при анализе двоичного файла в формате ELF
или ARM. Выбор анализаторов по умолчанию можно настроить
с помощью меню EditTool Options.
Некоторые анализаторы для однократного использования
можно также выбрать с помощью пункта меню AnalysisOne
Расширение взгляда на мир Ghidra

359

Shot (АнализОднократный) браузера кода. Анализатор
присутствует в списке, если он поддерживает однократное использование и применим к файлу данного типа. Однократный
анализ полезен для запуска анализаторов, которые не были
выбраны в начале автоматического анализа, или когда нужно повторно запустить анализатор после получения новой информации, которую стоит проанализировать дополнительно.
Например, если в процессе начального анализа появилось сообщение об отсутствии PDB-файла, то можно найти PDB-файл
и заново запустить анализатор PDB.
Команда Analyze All Open (Анализировать все открытые)
в меню Analysis браузера кода позволяет проанализироватьвсе
открытые в проекте файлы с применением списка анализаторов,
выбранных в окне EditTool Options. Если все открытые файлы предназначены для одной и той же архитектуры (спецификации языка и компилятора), то все и будут проанализированы.
Файлы, архитектура которых отлична от архитектуры текущего
файла, в анализ не включаются. Тем самым гарантируется, что
анализаторы совместимы с типом анализируемого файла.
Многим инструментам браузера кода, в т. ч. анализаторам
для идентификации важных конструкций в файле, необходимы различные артефакты. По счастью, мы можем расширять
многие из этих артефактов и тем самым улучшать качество работы Ghidra. Начнем с обсуждения файлов моделей слов и их
применения для выделения специальных строк и типов строк
в результатах поиска.

МОДЕЛИ СЛОВ
Модель слова – это способ выявлять специальные строки и типы
строк, например известные идентификаторы, адреса электронной почты, пути к каталогам, расширения имен файлов и т. д.
Если с поиском строки связана модель слова, в результатах
поиска будет столбец IsWord, в котором указано, является ли
найденная строка словом в смысле заданной модели. Определение представляющих интерес строк как допустимых слов
и последующий поиск допустимых слов – хороший способ выделить строки, нуждающиеся в первоочередном внимании.
360

ГЛАВА 13

На верхнем уровне модель слова пользуется обучающими
наборами правильных строк для выводов типа «если триграмма X (последовательность трех символов) встречается в последовательности Y длины Z, то Y с вероятностью P является
словом». Полученная вероятность косвенно используется как
порог, определяющий, стоит ли считать строку допустимым
словом в процессе анализа.
Файл StringModel.sng на рис. 13.5 содержит модель слова,
применяемую по умолчанию при поиске строк в Ghidra.

Рис. 13.5. Диалоговое окно поиска строк
Следующий фрагмент файла StringModel.sng демонстрирует формат модели слова.











#
#
#
#
#
#
#
#
#
#

Тип модели: lowercase
Обучающий файл: contractions.txt
Обучающий файл: uniqueStrings_012615_minLen8.edited.txt
Обучающий файл: connectives
Обучающий файл: propernames
Обучающий файл: web2
Обучающий файл: web2a
Обучающий файл: words
[^] обозначает начало строки
[$] обозначает конец строки
Расширение взгляда на мир Ghidra

361












# [SP] обозначает пробел
# [HT] обозначает горизонтальную табуляцию
[HT] [HT] [HT] 17
[HT] [HT] [SP] 8
[HT] [HT] ( 1
[HT] [HT] ; 1
[HT] [HT] \ 25
[HT] [HT] a 2
[HT] [HT] b 1
[HT] [HT] c 1

Первые 12 строк – комментарии к модели. В данном случае
модель имеет тип  lowercase, т. е. буквы верхнего и нижнего
регистров не различаются. Перечислены имена использованных обучающих файлов . Обычно имя косвенно указывает
на содержимое: файл contractions.txt, вероятно, содержит список стяженных форм типа can’t. Четыре строки  описывают, как обозначаются не имеющие графического начертания
ASCII-символы в триграммах. Сами триграммы начинаются
в строке ; каждая строка содержит три символа триграммы,
за которыми следует значение, используемое для вычисления
вероятности того, что триграмма является частью слова.
Вы можете дополнить или заменить модель слова по умолчанию, отредактировав файл StringModel.sng или создав новые файлы модели в каталоге Ghidra/Features/Base/data/
stringngrams, а затем указав одну из них в поле Word Model диалогового окна поиска строк. Есть много причин для модификации
модели слова, например включение строк, встречающихся в известных семействах вредоносных программ, или распознавание
слов в языках, отличных от английского. В общем и целом модели
слов – эффективное средство управления типами строк, которым
Ghidra назначает высокий приоритет в окне результатов поиска.
Аналогично можно редактировать и расширять состав типов
данных, распознаваемых Ghidra.

ТИПЫ ДАННЫХ
Диспетчер типов данных позволяет управлять всеми типами
данных, ассоциированными с файлом. Ghidra позволяет повторно использовать определения типов данных, сохранив
362

ГЛАВА 13

их в архивных файлах типов данных. Каждый корневой узел
в окне диспетчера типов данных является архивом типов.
На рис. 13.6 показано окно диспетчера типов данных с тремя
архивами, выбранными загрузчиком Ghidra.

Рис. 13.6. Окно диспетчера типов данных
В список всегда входит архив BuiltInTypes. Он включает все типы, моделируемые внутри Ghidra классами Java,
которые реа­лизуют интерфейс ghidra.program.model.data.
BuiltInDataType (и только их). Ghidra ищет такие классы в своем пути к классам и заполняет ими архив.
Второй архив специфичен для анализируемого файла и называется так же, как сам файл. В данном случае архив ассоциирован с файлом global_array_demo_x64. Галочка рядом с архивом
показывает, что он ассоциирован с активным файлом. Первоначально Ghidra заполняет этот архив типами данных, специфичными для формата файла (например, относящимися к формату
PE или ELF). В процессе автоматического анализа Ghidra копирует в него дополнительные типы из других архивов, по мере
того как обнаруживает их использование в программе. Иными
словами, в этот архив входят те из известных диспетчеру типов
данных, которые встречаются в текущей программе. Сюда же
помещаются типы данных, созданные вами в Ghidra, как было
описано в разделе «Создание структур в Ghidra» главы 8.
Третий архив содержит прототипы функций из 64-разрядного стандарта ANSI C и типы данных из библиотеки C. В него
помещается информация, извлеченная из заголовочных файлов стандартной библиотеки C для 64-разрядной системы
Linux; это один из нескольких платформенно зависимых арРасширение взгляда на мир Ghidra

363

хивов в установке Ghidra по умолчанию. Он присутствует, потому что этот конкретный двоичный файл зависит от библиотеки libc.so.6, как показано на рис. 13.4. В установке Ghidra по
умолчанию есть еще четыре платформенно зависимых архива
в подкаталогах каталога Ghidra/Features/Base/data/typeinfo.
Имя файла содержит указания на поддерживаемую платформу: generic_clib.gdt, generic_clib_64.gdt, mac_osx.gdt, windows_
vs12_32.gdt и windows_vs12_64.gdt. (Расширение .gdt общее
для всех архивов типов данных в Ghidra.)
Помимо архивов, которые загрузчик Ghidra выбирает автоматически, можно добавить собственные архивы типов данных, которые станут узлами дерева в окне диспетчера типов
данных. Для демонстрации на рис. 13.7 показано окно диспетчера после добавления всех gdt-файлов, имеющихся в установке по умолчанию. В правой части рисунка показано меню для
работы с архивами и типами данных. Для загрузки дополнительных архивов предназначена команда Open File Archive
(Открыть архив файлов), которая открывает обозреватель файлов и позволяет выбрать интересующий архив.
Чтобы добавить новые встроенные типы в архив BuiltInTypes,
нужно поместить соответствующие class-файлы в каталог, указанный в пути к классам Ghidra. Если типы добавляются во
время работы Ghidra, то, чтобы они появились в списке, нужно
выполнить команду Refresh BuiltInTypes (см. рис. 13.7). Эта
операция заставляет Ghidra заново просмотреть путь к классам в поиске вновь добавленных встроенных типов. Любо­
знательный читатель может найти многочисленные примеры встроенных типов в своем дистрибутиве Ghidra в каталоге
Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/
program/model/data.

364

ГЛАВА 13

Рис. 13.7. Окно диспетчера типов данных, в котором видны все
стандартные архивы и раскрыто меню команд

Создание новых архивов типов данных
Невозможно предвидеть, какие типы данных могут встретиться при анализе двоичных файлов. Архивы, включенные в дистрибутив Ghidra, содержат типы данных, определенные в наиболее часто используемых библиотеках для Windows (Windows
SDK) и Unix (стандартная библиотека C). Если Ghidra не располагает информацией о типах данных, встречающихся в анализируемой программе, то она предлагает создать новые архивы типов, заполнить их и поделиться с другими членами
команды. В следующих разделах мы обсудим три способа создания новых архивов.

Разбор заголовочных файлов C
Один из самых распространенных источников информации
о типах данных – заголовочные файлы C. В предположении,
Расширение взгляда на мир Ghidra

365

что нужные заголовочные файлы имеются или вы готовы создать их самостоятельно, новый архив типов можно создать,
воспользовавшись плагином C-Parser, который извлекает информацию из заголовочного файла. Например, если вам часто
приходится анализировать двоичные файлы, скомпонованные
с криптографической библиотекой OpenSSL, то имеет смысл
скачать исходный код OpenSSL и попросить Ghidra разобрать
входящие в его состав заголовочные файлы и создать архив типов данных и сигнатур функций OpenSSL.
Этот процесс совсем не так прост, как может показаться. Заголовочные файлы часто испещрены макросами, изменяющими поведение в зависимости от используемого компилятора,
операционной системы и архитектуры процессора. Например,
C-структура
struct parse_demo {
uint32_t int_member;
char *ptr_member;
};

занимает 8 байт при компиляции для 32-разрядной системы
и 16 байт, если компилируется для 64-разрядной системы. Это
создает проблему для Ghidra, которая пытается выступать
в роли универсального препроцессора, и именно вам придется
направлять Ghidra в процессе разбора, если хотите получить
полезный архив. Когда придет время воспользоваться своим
архивом, вы должны быть уверены, что архив совместим с анализируемым двоичным файлом (т. е. что вы не загружаете
64-разрядный архив для анализа 32-разрядного файла).
Для разбора одного или нескольких заголовочных файлов
C выберите команду FileParse C Source (ФайлРазобрать
исходный код на C) в браузере кода – откроется диалоговое
окно, показанное на рис. 13.8. В секции Source files to parse
(Исходные файлы, подлежащие разбору) отображается упорядоченный список заголовочных файлов, которые будет разбирать плагин. Порядок важен, потому что типы данных и директивы препроцессора из одного файла становятся доступны
следующему.
366

ГЛАВА 13

В поле Parse Options (Параметры разбора) приведен список параметров, влияющих на поведение плагина C-Parser; это
те же параметры, которые задаются в командной строке при
запуске компилятора. Плагин понимает только параметры -I
(каталог включаемых файлов) и -D (определить макрос), распознаваемые большинством компиляторов. Ghidra предлагает
ряд конфигураций препроцессора в виде prf-файлов; вы можете выбрать тот, который содержит умолчания для одной из
распространенных комбинаций операционной системы и компилятора. Существующую конфигурацию можно изменить или
создать новую с нуля и сохранить в своем prf-файле. Типичное
изменение заключается в правильном задании архитектуры,
для которой разбираются заголовки, потому что во всех готовых конфигурациях подразумевается архитектура x86. Например, можно заменить флаг -D_X86_ в конфигурации для Linux
флагом -D__ARMEL__, если вы собираетесь анализировать двоичные файлы для ARM с прямым порядком байтов.
Результат работы плагина можно объединить с текущим активным файлом, воспользовавшись кнопкой Parse to Program
(Результат разбора в программу), или сохранить в отдельном
файле архива типов данных (gdt-файле), нажав кнопку Parse
to File (Результат разбора в файл). Дополнительные сведения
о плагине C-Parser смотрите в справке по Ghidra.

Расширение взгляда на мир Ghidra

367

Рис. 13.8. Диалоговое окно разбора исходного кода на C

Создание нового архива файлов
Вместо разбора заголовочных файлов C можно «складывать»
типы данных, созданные вами в процессе анализа файла, в архив, которым затем можно будет поделиться с коллегами или
использовать в других проектах Ghidra. Команда New File
Archive (Создать архив файлов) в меню диспетчера типов данных (см. рис. 13.7) просит ввести имя файла и место для его сохранения, а затем создает новый пустой архив, который будет
отображаться в окне диспетчера. Для добавления новых типов
в архив можно поступать так, как описано в разделе «Создание
структур в Ghidra» главы 8.

368

ГЛАВА 13

Создание нового архива проекта
Архив данных проекта существует только внутри проекта, в котором был создан. Это может быть полезно, если вы планируете
использовать созданные вами типы данных в нескольких файлах проекта, но не ожидаете, что они понадобятся где-то еще.
Команда New Project Archive (Создать архив проекта) в меню
диспетчера типов данных (см. рис. 13.7) предлагает выбрать папку для размещения нового архива, после чего создает новый пус­
той архив, который отображается в окне диспетчера. В него, как
и в любые другие архивы, можно добавлять новые типы данных.

ИДЕНТИФИКАТОРЫ ФУНКЦИЙ
Вознамерившись заняться обратной разработкой двоичного
файла, последнее, на что мы хотим тратить время, – анализ
библиотечных функций, о поведении которых гораздо проще
узнать из страницы руководства, из исходного кода или в интернете. К сожалению, в статически скомпонованных библиотеках различие между прикладным и библиотечным кодами
размыто: библиотеки целиком компонуются с кодом приложения, так что образуется один монолитный исполняемый файл.
Однако, на наше счастье, Ghidra располагает инструментами,
которые распознают и помечают библиотечный код вне зависимости от того, взят он из библиотечного файла или просто
используется в нескольких двоичных файлах. Это позволяет
нам сконцентрироваться на уникальном коде приложения.
Анализатор идентификаторов функций распознает многие
часто встречающиеся библиотечные функции, пользуясь базой
данных сигнатур функций, входящей в состав Ghidra. Эту базу
данных можно расширять с помощью плагина Function ID.
Анализатор идентификаторов функций работает с базами
идентификаторов функций (FidDb), в которых для описания
функций используется иерархия хеш-кодов. Для каждой функции вычисляются полный хеш-код (устойчивый к изменениям,
вносимым компоновщиком) и специальный хеш-код (позволяющий различать варианты функции). Основное различие между ними заключается в том, что специальный хеш-код может
включать конкретные значения константных операндов (на осРасширение взгляда на мир Ghidra

369

нове эвристик), а полный хеш-код – нет. Комбинация двух хешкодов вкупе с информацией о родительской и дочерних функциях образует цифровой отпечаток библиотечной функции,
который заносится в базу данных FidDb. Анализатор идентификаторов функций вычисляет такой же отпечаток для всех
функций в двоичном файле и сравнивает его с известными
отпечатками. Если найдено совпадение, то Ghidra восстанавливает имя функции из базы, помечает функцию соответствующим образом, добавляет ее в окно дерева символов и обновляет вводный комментарий. Ниже приведен пример вводного
комментария для функции _malloc:
**************************************************************
* Library Function – SingleMatch
*
* Name: _malloc
*
* Library: Visual Studio 2005 Release
*
**************************************************************

Информация о функциях хранится в FidDb иерархически
и включает имя, версию и вариант. В поле варианта кодируются, в частности, настройки компилятора, которые влияют на
хеш-коды, но не являются частью номера версии.
У анализатора идентификаторов функций есть несколько
параметров, которые задаются после его выбора в диалоговом
окне автоматического анализа (рис. 13.9). В поле Instruction
count threshold (Порог счетчика команд) задается пороговое
значение, позволяющее уменьшить количество ложноположительных результатов из-за случайного совпадения хеш-кодов
коротких функций. Ложноположительный результат имеет
место, когда функция неправильно отождествлена с библио­
течной. Ложноотрицательный результат имеет место, когда
функция не отождествлена с библиотечной, хотя должна бы.
Порог – это минимальное суммарное количество команд в самой функции, а также в ее родительских и дочерних функциях, при котором делается попытка отождествления. Подробнее
об оценке совпадений см. раздел «Scoring and Disambiguation»
(Балльная оценка и разрешение неоднозначностей) справки
по Ghidra.
370

ГЛАВА 13

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

ПЛАГИН FUNCTION ID
Плагин Function ID (не путать с анализатором идентификаторов функций, Function ID) позволяет создавать и изменять
ассоциации в базе данных FidDb. После установки Ghidra он
по умолчанию не активирован. Чтобы включить его, выберите
команду FileConfigure в меню браузера кода и отметьте
флажок напротив Function ID. Выберите Configure в описании Function ID, а затем FidPlugin, чтобы ознакомиться с дополнительной информацией о действиях с плагином
(рис. 13.10).

Расширение взгляда на мир Ghidra

371

Рис. 13.10. Подробное описание плагина FidPlugin
После активации плагин Function ID управляется из меню
ToolsFunction ID браузера кода, как показано на рис. 13.11.

Рис. 13.11. Подменю Function ID браузера кода
Прежде чем переходить к примеру использования плагина
Function ID для расширения базы сигнатур Ghidra, кратко обсудим назначение всех пяти пунктов меню.

372

ГЛАВА 13

Choose active FidDbs (Выбрать активные FidDb). Отображает список активных баз идентификаторов функций. Флажки позволяют выбрать любую из них или отменить выбор.
Create new empty FidDb (Создать новую пустую FidDb).
Позволяет создать и поименовать новую базу идентификаторов функций. Новые FidDb появятся в списке, отображаемом
командой Choose active FidDbs.
Attach existing FidDb (Присоединить существующую
FidDb). Отображает диалоговое окно выбора файлов, которое
позволяет добавить существующую FidDb в список активных.
Добавленная база появляется в списке, отображаемом командой Choose active FidDbs.
Detach existing FidDb (Отсоединить существующую FidDb).
Применимо только к базам, присоединенным вручную. Операция удаляет ассоциацию между выбранной FidDb и текущим
экземпляром Ghidra.
Populate FidDb from programs (Заполнить FidDb из программ). Генерирует цифровые отпечатки новых функций для
добавления в существующую базу FidDb. Для управления этим
процессом используется диалоговое окно на рис. 13.12, которое
мы вскоре обсудим.

Рис. 13.12. Диалоговое окно заполнения базы данных FidDb

Расширение взгляда на мир Ghidra

373

Пример применения плагина Function ID: UPX
Когда анализируется двоичный файл, содержащий очень
мало функций, кроме библиотечных, известных Ghidra, задача обратной разработки упрощается. Мы можем сосредоточиться на тех немногих функциях, которые Ghidra не распо­
знала, в предположении, что именно там сосредоточена новая
интересная функциональность. Наша задача становится
куда труднее, если Ghidra не может распознать ни одной
функции. Когда мы (люди) распознаем функции и расширяем возможности Ghidra, наделяя ее даром распознавать те же
функции в будущем, мы упрощаем себе жизнь. На конкретном примере покажем, насколько эффективным может быть
такое расширение.
Допустим, что мы загрузили в Ghidra 64-разрядный ELFфайл для Linux и подвергли его автоматическому анализу. Получившееся дерево символов показано на рис. 13.13. Мы находим в дереве символов точку входа и начинаем исследовать код.
Начальный анализ позволяет предположить, что двоичный
файл упакован программой Ultimate Packer for eXecutatbles
(UPX), а функции, которые мы видим, добавлены упаковщиком UPX, чтобы распаковать файл во время выполнения. Эта
гипотеза подтверждается путем сравнения байтов entry с опуб­
ликованными байтами точки входа в UPX. (Можно было бы
также упаковать с помощью UPX собственный двоичный файл
и сравнить.) Добавим эту информацию в нашу базу FidDb, чтобы не проделывать ту же работу всякий раз, как встретится
упакованный UPX 64-разрядный ELF-файл.
Добавляемые в FidDb функции должны иметь осмысленные
имена. Поэтому изменим имена функций в нашем примере,
чтобы показать, что это части упаковщика UPX, как представлено на рис. 13.14, а затем добавим их в новую базу идентификаторов функций, чтобы Ghidra могла должным образом пометить эти функции в будущем.

374

ГЛАВА 13

Рис. 13.13. Предположительно функции упаковщика UPX
в файле upx_demo1_x64_static.upx

Рис. 13.14. Переименованные функции упаковщика UPX для
файла upx_demo1_x64_static.upx
Создадим новую базу FidDb командой ToolsFunction
IDCreate new empty FidDb и назовем ее UPX.fidb. Затем заполним новую базу информацией, извлеченной из модифицированного двоичного файла, выполнив команду ToolsFunction
IDPopulate FidDb from programs. Введем информацию
о FidDb в диалоговом окне, как показано на рис. 13.15.

Расширение взгляда на мир Ghidra

375

Рис. 13.15. Диалоговое окно заполнения базы данных FidDb
Ниже описано назначение каждого поля и введенные нами
значения.
► Fid Database (База данных Fid). Наша новая база данных называется UPX.fidb. Выпадающий список позволяет выбрать одну из ранее созданных FidDb.
► Library Family Name (Имя семейства библиотек). Выберите имя, описывающее библиотеку, из которой извлекаются данные о функциях. В нашем случае это UPX.
► Library Version (Версия библиотеки). Это может быть
номер версии, или имя платформы, или комбинация того
и другого. Поскольку UPX доступна для многих платформ,
мы решили привязывать версию к архитектуре процессора.
► Library Variant (Вариант библиотеки). В этом поле
можно ввести любую дополнительную информацию, отличающую эту библиотеку от других с такой же версией.
Мы взяли идентификатор фиксации данной версии UPX
из репозитория на GitHub (https://github.com/upx/).
► Base Library (Базовая библиотека). Здесь можно сослаться на другую FidDb, которую Ghidra будет использовать для установления связей родитель–потомок. Мы не
используем базовую библиотеку, потому что UPX полностью автономна.
376

ГЛАВА 13

► Root Folder (Корневая папка). В этом поле записывается
имя папки проекта Ghidra. Все файлы в этой папке будут
обработаны в процессе импорта функций. В данном случае мы выбрали папку /UPX из выпадающего списка.
► Language (Язык). Идентификатор языка, ассоциированного с новой FidDb. В корневой папке обрабатываются только те файлы, для которых идентификатор
языка совпадает с указанным в этом поле. Значение
берется из окна сводки результатов импорта данного
файла, но может быть изменено с помощью кнопки
справа от поля.
► Common Symbols File (Файл хорошо известных символов). В это поле вводится имя файла, содержащего список
функций, которые должны быть исключены из процесса
импорта. В данном случае оно не используется.
После нажатия кнопки OK начинается процесс импорта.
По завершении мы увидим результаты заполнения FidDb
(рис. 13.16).

Рис. 13.16. Окно результатов заполнения базы данных UPX
Расширение взгляда на мир Ghidra

377

Создав новую базу FidDb, Ghidra может использовать ее для
идентификации функций в любом анализируемом двоичном
файле. Продемонстрируем это, загрузив другой 64-разрядный
ELF-файл для Linux, упакованный UPX, upx_demo2_x64_static.
upx, который автоматически проанализируем без помощи анализатора идентификаторов функций. В результирующем дереве символов на рис. 13.17 мы видим пять неидентифицированных функций, как и следовало ожидать.

Рис. 13.17. Дерево символов для файла upx_demo2_x64_static.upx
до применения анализатора идентификаторов функций
Запустив анализатор идентификаторов функций как однократный (AnalysisOne ShotFunction ID), мы увидим
дерево символов, показанное на рис. 13.18, в котором уже присутствуют имена функций UPX.

Рис. 13.18. Дерево символов для файла upx_demo2_x64_static.
upx после применения анализатора идентификаторов
функций
378

ГЛАВА 13

Анализатор также обновляет окно листинга, включая в него
новые имена и вводные комментарии, как для функции UPX_1
ниже. Вводный комментарий содержит ту информацию, которую мы предоставили при создании базы FidDb:
**************************************************************
* Library Function - Single Match *
* Name: UPX_1 *
* Library: UPX AMD64 021c8db *
**************************************************************
undefined UPX_1()
undefined
AL:1

UPX_1
XREF[1]:
UPX_2:00457c08(c)
00457b1a 48 8d 04 2f
LEA RAX,[RDI + RBP*0x1]
00457b1e 83 f9 05
CMP ECX,0x5

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

Пример применения плагина Function ID:
профилирование статической библиотеки
Подвергая обратной разработке статически скомпонованный
двоичный файл, мы хотим иметь базу FidDb, которая позволила бы Ghidra идентифицировать библиотечный код и избавить
нас от его анализа. В следующем примере рассматриваются два
важных вопроса: (1) как узнать, имеется ли такая FidDb, и (2)
что делать, если ее нет? Ответ на первый вопрос прост: в комп­
лект поставки Ghidra входит по меньшей мере дюжина FidDb
(в виде fidbf-файлов), относящихся к коду библиотек Visual
Studio. Если двоичный файл собран не для Windows и вы еще
не создали и не импортировали никаких FidDb, то придется
создавать собственную FidDb, применяя плагин Function ID
(и это, кстати, ответ на второй вопрос).
Расширение взгляда на мир Ghidra

379

Самое важное при заполнении новой FidDb – иметь источник функций, которые с высокой вероятностью будут встречаться в любом двоичном файле, к которому планируется применить FidDb. В примере UPX у нас был файл, содержащий
код, который, как подсказывала интуиция, мы вполне можем
встретить в будущем. В типичном случае статически скомпонованной программы имеется двоичный файл, и мы просто хотим
идентифицировать в нем как можно больше кода.
Существует много способов определить, что мы имеем дело со
статически скомпонованной программой. Находясь в Ghidra,
загляните в папку Imports в дереве символов. Она будет пуста
для полностью статически скомпонованного двоичного файла,
который не нуждается в импортированных функциях. Частично статически скомпонованный файл может что-то импортировать, поэтому поищите строки копирайта или версий хорошо
известных библиотек в окне определенных строк.
На уровне командной строки можно воспользоваться простыми утилитами типа file и strings:
$ file upx_demo2_x64_static_stripped
upx_demo2_x64_static_stripped: ELF 64-bit LSB executable, x86-64,
version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0,
BuildID[sha1]=54e3569c298166521438938cc2b7a4dda7ab7f5c, stripped
$ strings upx_demo2_x64_static_stripped | grep GCC
GCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0

Утилита file говорит, что файл статически скомпонован, зачищен от всех символов и собран для системы Linux. (Зачищенный двоичный файл не содержит знакомых имен, которые
могли бы подсказать, что делает функция.) Пропустив выход
strings через grep GCC, мы идентифицируем компилятор GCC
7.4.0 и дистрибутив Linux, Ubuntu 18.04.1, для которого был
собран файл. (Ту же самую информацию можно получить с помощью команды браузера кода SearchProgram Text, задав
поиск строки GCC.) Очень вероятно, что файл был скомпонован
с библиотекой libc.a1, поэтому возьмем копию libc.a из Ubuntu
1

380

Архив стандартных библиотечных функций C, libc.a, используется в статически скомпонованных программах для ОС типа Unix.

ГЛАВА 13

18.04.1 и воспользуемся ей в качестве отправной точки для восстановления символов в зачищенном файле. (Дополнительные
строки в двоичном файле могли бы привести к другим статическим библиотекам для анализа идентификаторов функций, но
мы ограничимся libc.a.)
Чтобы использовать libc.a для заполнения FidDb, Ghidra
должна идентифицировать составляющие ее команды и функции. Архив (отсюда расширение .a) представляет собой контейнер для других файлов, чаще всего объектных (с расширением .o), которые компилятор извлекает и компонует
в исполняемый файл. Ghidra импортирует файлы-контейнеры
не так, как одиночные двоичные файлы, поэтому, когда мы импортируем libc.a командой FileImport, как обычно, предлагаются другие режимы импорта, показанные на рис. 13.19 (эти
режимы имеются и в меню File).

Рис. 13.19. Импорт файла-контейнера
В режиме Single File (Одиночный файл) Ghidra импортирует
контейнер так, будто он является одиночным файлом. Поскольку контейнер – это неисполняемый файл, скорее всего, Ghidra
предположит формат Raw Binary и выполнит минимальный
объем автоматического анализа. В режиме File System Ghidra
открывает окно обозревателя файлов (рис. 13.20), в котором
отображается содержимое контейнера. В этом режиме можно
выбрать любое подмножество файлов для импорта в Ghidra,
воспользовавшись командами в контекстном меню.

Расширение взгляда на мир Ghidra

381

Рис. 13.20. Импорт в режиме файловой системы
В режиме Batch (Пакет) Ghidra автоматически импортирует файлы из контейнера, не прерываясь на показ информации о каждом отдельном файле. После начальной обработки
содержимого контейнера Ghidra открывает диалоговое окно
пакетного импорта, показанное на рис. 13.21. Перед тем как
нажать OK, можно просмотреть информацию о каждом импортируемом файле, добавить в пакет дополнительные файлы,
задать параметры импорта и выбрать конечную папку внутри
проекта Ghidra. На рис. 13.21 видно, что мы собираемся импортировать 1690 файлов из архива libc.a в корневой каталог
проекта CH13.

382

ГЛАВА 13

Рис. 13.21. Диалоговое окно пакетного импорта
Нажатие OK запускает процесс импорта (который может занять некоторое время). По завершении импорта мы сможем
просмотреть вновь импортированные файлы в окне проекта.
Поскольку libc.a – файл-контейнер, он будет выглядеть как
папка, в которую можно зайти и проанализировать любой находящийся там файл.
Теперь мы, наконец, можем записать цифровые отпечатки
всех функций из libc в базу данных FidDb и использовать ее
для анализа идентификаторов функций в нашем статически
скомпонованном двоичном файле. Делается это так же, как
в примере UPX: сначала создается пустая база FidDb, которая
затем заполняется из программ. В роли программ в данном
случае выступает все содержимое только что импортированной
папки libc.a. Но тут мы сталкиваемся с серьезной проблемой.
Выбирая файлы, которыми хотим заполнить нашу новую
базу FidDb, мы должны быть уверены, что каждый файл был
надлежащим образом проанализирован Ghidra, которая идентифицировала функции и принадлежащие им команды (это
входные данные для вычисления идентификатора функции).
Расширение взгляда на мир Ghidra

383

До сих пор мы видели, как Ghidra анализирует программы,
только когда открывали их в браузере кода, но в случае libc.a
перед нами стоит грандиозная задача проанализировать все
1690 файлов в архиве. Открывать и анализировать их по одному – непроизводительная трата времени. Но, даже открыв все
файлы после импорта и воспользовавшись командой Analyze
All Open, мы все равно должны будем обработать 1690 файлов
(и, вероятно, понадобится ручное вмешательство, чтобы настроить параметры инструментов и выделение ресурсов, чтобы наш
экземпляр Ghidra мог справиться с задачей такого размера).
Если эта задача кажется вам неподъемной, то вы абсолютно
правы. Это не та задача, которую следует решать вручную с помощью графического интерфейса Ghidra, а корректно поставленное повторяющееся задание, которое должно выполняться
без вмешательства человека. И в следующих трех главах мы
как раз и опишем методы автоматизации этой и других задач.
А когда доберемся до раздела «Автоматизированное создание
базы FidDb» главы 16, то вернемся к этой конкретной задаче
и продемонстрируем, как легко выполнить пакетную обработку в необслуживаемом режиме Ghidra.
Но каким бы способом мы ни обрабатывали архив libc.a, по
завершении этой работы плагин Function ID выдаст следующие результаты:
FidDb Populate Results
2905 total functions visited
2638 total functions added
267 total functions excluded
Breakdown of exclusions: FAILS_MINIMUM_SHORTHASH_LENGTH: 234
DUPLICATE_INFO: 9
FAILED_FUNCTION_FILTER: 0
IS_THUNK: 16
NO_DEFINED_SYMBOL: 8
MEMORY_ACCESS_EXCEPTION: 0
Most referenced functions by name:
749 __stack_chk_fail
431 free
304 malloc
...

384

ГЛАВА 13

Теперь наша база FidDb готова для использования, и анализатор идентификаторов функций сможет идентифицировать многие функции в файле upx_demo2_x64_static _stripped, что сущест­
венно сократит трудоемкость его обратной разработки.

РЕЗЮМЕ
В этой главе были продемонстрированы некоторые способы
расширения Ghidra: разбор исходных файлов на C, расширение моделей слов и сбор цифровых отпечатков функций с помощью плагина Function ID. Если двоичный файл содержит
статически скомпонованный код или код, который уже не раз
использовался в ранее проанализированных файлах, то сопоставление функций с базами данных FidDb может избавить вас
от ручного перелопачивания горы кода. Понятно, что статических библиотек так много, что в Ghidra невозможно включить
файлы FidDb для всех возможных случаев. Но есть возможность создавать собственные базы, поэтому вы можете собрать
коллекцию FidDb специально под свои нужды. В главах 14
и 15 мы познакомимся с мощными скриптовыми средствами
дальнейшего расширения функциональности Ghidra.

14

ОСНОВЫ НАПИСАНИЯ
СКРИПТОВ ДЛЯ GHIDRA

Никакое приложение не может удовлетворить
потребности любого пользователя. Просто невозможно предвидеть все возможные случаи. Модель открытого кода, принятая в Ghidra, допускает
подачу запросов на новую функциональность и ценные дополнения со стороны разработчиков. Но иногда проблему требуется решить незамедлительно, не дожидаясь, пока
кто-то реализует то, что нужно вам. Для поддержки непредвиденных сценариев и программного управления действиями
Ghidra в систему включены средства написания скриптов.
Областей применения скриптов не перечесть: от простых
однострочных поделок до полноценных программ, которые автоматизируют типичные задачи или выполняют сложный анализ. В этой главе мы раскажем о базовых средствах написания
скриптов, доступных через интерфейс браузера кода. Мы познакомимся с внутренней скриптовой средой, обсудим разработку скритов на Java и Python, а в главе 15 перейдем к другим
интегрированным возможностям.

ДИСПЕТЧЕР СКРИПТОВ
Диспетчер скриптов доступен из меню браузера кода. Команда
WindowScript Manager (ОкноДиспетчер скриптов) открывает окно, показанное на рис. 14.1. Его можно также открыть, щелкнув по значку диспетчера скриптов на панели инструментов (зеленый кружок со стрелкой внутри, показанный
также в левом верхнем углу окна диспетчера скриптов).

Рис. 14.1. Окно диспетчера скриптов

Окно диспетчера скриптов
В только что установленном дистрибутиве Ghidra диспетчер
скриптов загружает более 240 скриптов, организованных в виде
дерева категорий, показанного в левой части рис. 14.1. Некоторые папки содержат подпапки, уточняющие классификацию.
Папки можно раскрывать и сворачивать, чтобы видеть, как организованы скрипты. При выборе какой-либо папки или подпапки в правой части будут показаны находящиеся в ней скрипты.
Для заполнения этого окна Ghidra находит и индексирует все
скрипты в подкаталогах каталога ghidra_scripts дистрибутива.
Просматривается также каталог ghidra_scripts вашего домашнего каталога, и индексируются найденные там скрипты.
388

ГЛАВА 14

Набор скриптов, предлагаемый по умолчанию, охватывает
широкий спектр задач. Некоторые призваны продемонстрировать основы написания скриптов. В таблице скриптов имеется
дополнительная информация о назначении каждого скрипта.
Как и для большинства таблиц в Ghidra, мы можем управлять составом столбцов и порядком сортировки. По умолчанию
отобра­жаются все поля, кроме Created (Дата создания) и Path
(Путь). Шесть информационных столбцов дают следующую информацию о скрипте.
► Status (Состояние). Обозначает состояние скрипта. Обычно поле либо пустое, либо содержит красный значок, означающий, что в скрипте имеется ошибка. Если со скриптом
ассоциирован значок, то он будет показан в этом столбце.
► Name (Имя). Содержит имя файла скрипта вместе с расширением.
► Description (Описание). Описание берется из комментария внутри скрипта. Это поле может не поместиться
в столбце, но полный текст всегда можно прочитать, задержав над ним мышь. Подробнее мы обсудим его ниже
в разделе «Разработка скрипта».
► Key (Клавиша). Показывает, назначена ли скрипту какая-нибудь комбинация клавиш.
► Category (Категория). Задает путь к скрипту в иерархии
категорий. Это логическая иерархия, а не иерархия каталогов в файловой системе.
► Modified (Дата модификации). Дата, когда скрипт был
сохранен в последний раз. Для скриптов по умолчанию
это дата установки экземпляра Ghidra.
Поле Filter в левой части окна позволяет искать по категориям, а в правой – по именам и описаниям скриптов. Наконец,
в нижней части расположено дополнительное окно, поначалу
пустое. В нем отображаются метаданные выбранного скрипта
в удобном для обработки формате. Формат и семантика полей
метаданных обсуждаются в разделе «Написание скриптов на
Java (не JavaScript!)» ниже.
Хотя диспетчер скриптов дает немало информации, главная
сила окна в его панели инструментов, обзор которой приведен
на рис. 14.2.
Основы написания скриптов для Ghidra

389

Панель инструментов диспетчера
скриптов
У диспетчера скриптов нет меню, всем действиям соответствуют значки на панели инструментов (рис. 14.2).
Большинство операций понятно из приведенных на рисунке
описаний, но вот операции редактирования заслуживают дополнительного обсуждения. Операция Edit script with Eclipse (Редактировать скрипт в Eclipse) рассматривается в главе 15, потому
что считается продвинутым средством. Значок Edit Script (Редактировать скрипт) открывает окно простенького текстового редактора со своей собственной панелью инструментов, показаной
на рис. 14.3. Значки на этой панели реализуют базовую функцио­
нальность редактирования файлов. Располагая каким-никаким
редактором, мы можем приступить к написанию скриптов.

Выполнить скрипт

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

Повторно выполнить
последний скрипт

Выполняет скрипт, запущенный в последний раз. Эта операция
доступна также из окна браузера кода после выполнения скрипта

Редактировать
скрипт

Открывает выбранный скрипт в окне редактора

Редактировать
скрипт в Eclipse

Открывает выбранный скрипт в Eclipse. См. главу 15

Назначить клавишу

Позволяет назначить скрипту комбинацию клавиш

Удалить скрипт

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

Переименовать
скрипт

Переименовывает созданный пользователем скрипт. Переименовать таким способом системные скрипты невозможно

Создать новый
скрипт

Открывает окно с шаблоном скрипта в окне редактора

Обновить список
скриптов

Заново просматриват каталоги скриптов и перестраивает список
скриптов

Каталоги скриптов

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

Справка

Открывает документацию по классу GhidraScript. При первом
выполнении этой операции Ghidra автоматически генерирует
документацию



Рис. 14.2. Панель инструментов диспетчера скриптов
390

ГЛАВА 14

Обновить 

Обновляет текст скрипта. Это полезно, когда
используется внешний редактор

Сохранить

Сохраняет изменения в файле скрипта.
Сохранить изменения системного скрипта
невозможно

Сохранить как

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

Отмена/Повтор

Позволяет откатить и повторить изменения
(сохраняется 50 последних изменений)

Выполнить скрипт

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

Выбрать шрифт

Позволяет задать шрифт, размер и стиль

Рис. 14.3. Панель инструментов редактора скриптов

РАЗРАБОТКА СКРИПТОВ
В Ghidra поддерживается несколько способов разработки
скриптов. В этой главе мы будем рассматривать скрипты на
Java и Python, поскольку на этих языках написаны системные
скрипты, представленные в окне диспетчера скриптов. Большинство из более чем 240 системных скриптов написаны на
Java, поэтому с него и начнем.

Написание скриптов на Java
(не JavaScript!)
В Ghidra скрипт, написанный на Java, – это фактически полная
спецификация класса, который рассчитан на незаметную компиляцию, динамическую загрузку в работающий экземпляр
Ghidra, выполнение и, наконец, выгрузку. Этот класс должен
расширять класс Ghidra.app.script.GhidraScript, реализовывать метод run() и включать аннотации, содержащие метаданные скрипта в формате Javadoc. Мы покажем структуру файла
скрипта, опишем требования к метаданным, рассмотрим несколько системных скриптов, а затем перейдем к редактированию существующих скриптов и созданию своих собственных.
Основы написания скриптов для Ghidra

391

На рис. 14.4 показан редактор скриптов, который открывается при выполнении команды Create New Script (см. рис. 14.2).
Мы назвали новый скрипт CH14_NewScript.

Рис. 14.4. Новый пустой скрипт
В начале файла мы видим комментарии и теги для ввода ожидаемой документации в формате Javadoc, которая затем будет отображена в полях окна диспетчера скриптов (см.
рис. 14.1). Все комментарии, начинающиеся с //, перед объявлением класса, поля или метода становятся частью документации скрипта. Внутри кода могут быть дополнительные
комментарии, которые не включаются в описание. Кроме того,
поддерживаются следующие теги, содержащие метаданные:
392

ГЛАВА 14

► @author – информация об авторе скрипта. Сведения
предоставляются по желанию автора и могут содержать
любые детали (например, имя, контактные данные, дата
создания скрипта и т. д.);
► @category – в каком месте дерева категорий будет отобра­
жаться скрипт. Это единственный обязательный тег, он
должен присутствовать во всех скриптах Ghidra. Имена
категорий в составе пути разделяются точками (например, @category Ghidrabook.CH14);
► @keybinding – комбинация клавиш для быстрого доступа
к скрипту из окна браузера кода (например, @keybinding K);
► @menupath – путь к этому скрипту в меню, позволяющий запускать скрипт из меню браузера кода (например,
@menupath File.Run.ThisScript);
► @toolbar – значок скрипта. Отображается на панели инструментов в окне браузера кода и тоже служит для запуска скрипта. Если Ghidra не сможет найти указанное изображение в каталоге скриптов или в системном каталоге,
то будет показано изображение по умолчанию (например,
@toolbar myImage.png).
При освоении нового API (в частности, Ghidra API) поначалу
приходится постоянно обращаться к документации. Язык Java
особенно чувствителен к проблемам путей к классам и включению всех необходимых вспомогательных пакетов. Для экономии времени и нервов лучше редактировать существующую
программу, а не писать новую с нуля. Именно так мы и представим простой пример скрипта.

Пример редактированияскрипта:
поиск по регулярному выражению
Предположим, что требуется разработать скрипт, который будет принимать регулярное выражение от пользователя и выводить найденные строки на консоль. Этот скрипт должен
присутствовать в окне диспетчера скриптов для конкретного
проекта. И хотя Ghidra предлагает много способов решить эту
задачу, вас попросили написать именно скрипт. Чтобы найти скрипт с похожей функциональностью, который можно
Основы написания скриптов для Ghidra

393

было бы взять за основу, посмотрим, что имеется в категории
Strings and Search (Строки и поиск), а затем поищем по слову strings. Применение фильтров дает более полный список
относящихся к работе со строками скриптов. В данном случае
мы будем редактировать первый же скрипт в списке, который имеет что-то общее с тем, что нам предстоит сделать, –
CountAndSaveStrings.java.
Откройте скрипт в редакторе, чтобы убедиться, что это
и вправду хорошая отправная точка для реализации новой
функциональности. Для этого щелкните правой кнопкой
мыши по скрипту и выберите из контекстного меню команду
Edit script, после чего сохраните его копию под новым именем FindStringsByRegex.java, воспользовавшись командой
Save As (Сохранить как). Ghidra не разрешает редактировать
системные скрипты, входящие в состав дистрибутива, в окне
диспетчера скриптов (хотя в Eclipse и других редакторах это
можно делать). Можно было бы также отредактировать файл
до сохранения, потому что Ghidra не даст случайно перезаписать существующий файл CountAndSaveStrings.java.
В оригинальном файле CountAndSaveStrings.java имеются
следующие метаданные:
 /* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
 //Counts the number of defined strings in the current selection,
//or current program if no selection is made,
//and saves the results to a file.
 //@category CustomerSubmission.Strings

394

ГЛАВА 14

Мы можем оставить без изменения, модифицировать или удалить лицензионное соглашение  – ни на выполнение скрипта, ни на документацию это не повлияет. Описание скрипта 
мы изменим, чтобы текст, отображаемый в документации и в
диспетчере скриптов, правильно отражал назначение скрипта.
Автор скрипта включил только один из пяти возможных тегов , поэтому мы добавим остальные, не заполняя их. Новое
описание будет выглядеть так:
// Подсчитывает число строк, соответствующих регулярному выражению, в текущем
// выделении или в текущей программе, если ничего не выделено,
и отображает это
// число на консоли.
//
//@author Ghidrabook
//@category Ghidrabook.CH14
//@keybinding
//@menupath
//@toolbar

Тег категории Ghidrabook.CH14 будет добавлен в дерево в окне
диспетчера скриптов, как показано на рис. 14.5.
Далее в оригинальном скрипте идут предложения import.
Из длинного перечня предложений импорта, которые Ghidra
включает при создании нового скрипта (рис. 14.4), к поиску строк
относятся лишь показанные ниже, поэтому мы оставим тот же
список, что в оригинальном файле CountAndSaveStrings.java:
import
import
import
import

ghidra.app.script.GhidraScript;
ghidra.program.model.listing.*;
ghidra.program.util.ProgramSelection;
java.io.*;

Сохраним новый скрипт и выберем его в диспетчере скриптов – мы увидим картину, показанную на рис. 14.5. Наша новая
категория включена в дерево, а метаданные скрипта отображены в информационном окне и в таблице скриптов. Таблица содержит всего один скрипт, Ghidrabook.CH14, потому что в этой
категории других скриптов нет.
Основы написания скриптов для Ghidra

395

Рис. 14.5. Информация о новом скрипте, отображаемая в окне
диспетчера скриптов
Поскольку эта книга – не пособие по Java, мы просто подытожим внесенные в скрипт изменения, не вдаваясь в объяснения
синтаксиса и семантики Java. Ниже описано поведение скрипта CountAndSaveStrings.java.
1. Получить текст программы, в котором производится поиск.
2. Получить файл, в котором будут сохранены результаты.
3. Открыть файл.
4. Произвести поиск в тексте программы, подсчитать количест­
во подходящих строк и записать каждую из них в файл.
5. Закрыть файл.
6. Вывести количество найденных строк на консоль.
А вот чего мы хотим от модифицированного скрипта:
1. Получить текст программы, в котором производится поиск.
2. Запросить у пользователя регулярное выражение.
3. Произвести поиск в тексте программы, подсчитать количест­
во подходящих строк и вывести каждую из них на консоль.
4. Вывести количество найденных строк на консоль.
Наш новый скрипт будет значительно короче оригинального, поскольку нет нужды взаимодействовать с файловой системой и обрабатывать возможные ошибки. Ниже приведена
наша реализация.
396

ГЛАВА 14

public class FindStringsByRegex extends GhidraScript {
@Override
public void run() throws Exception {
String regex =
askString("Please enter the regex",
Please enter the regex you're looking to match:);
Listing listing = currentProgram.getListing();
DataIterator dataIt;
if (currentSelection != null) {
dataIt = listing.getDefinedData(currentSelection, true);
}
else {
dataIt = listing.getDefinedData(true);
}
Data data;
String type;
int counter = 0;
while (dataIt.hasNext() && !monitor.isCancelled()) {
data = dataIt.next();
type = data.getDataType().getName().toLowerCase();
if (type.contains("unicode") || type.contains("string")) {
String s = data.getDefaultValueRepresentation();
if (s.matches(regex)) {
counter++;
println(s);
}
}
}
println(counter + " matching strings were found");
}
}

Все Java-скрипты для Ghidra должны расширять (наследовать) существующий класс Ghidra.app.script.GhidraScript .
Сохранив окончательную версию скрипта, выберите его в диспетчере скриптов и выполните. Когда скрипт начнет работать,
мы увидим диалоговое окно, показанное на рис. 14.6. В поле
уже введено регулярное выражение, которое мы будем искать,
чтобы протестировать наш скрипт:

Основы написания скриптов для Ghidra

397

Рис. 14.6. Приглашение ввести регулярное выражение
По завершении нашего скрипта консоль браузера кода будет
выглядеть так:
FindStringsByRegex.java> Running...
FindStringsByRegex.java> «Fatal error: glibc detected an invalid stdio handle\n»
FindStringsByRegex.java> «Unknown error «
FindStringsByRegex.java> «internal error»
FindStringsByRegex.java> «relocation error»
FindStringsByRegex.java> «symbol lookup error»
FindStringsByRegex.java> «Fatal error: length accounting in _dl_exception_create_
format\n»
FindStringsByRegex.java> «Fatal error: invalid format in exception string\n»
FindStringsByRegex.java> «error while loading shared libraries»
FindStringsByRegex.java> «Unknown error»
FindStringsByRegex.java> «version lookup error»
FindStringsByRegex.java> «sdlerror.o»
FindStringsByRegex.java> «dl-error.o»
FindStringsByRegex.java> «fatal_error»
FindStringsByRegex.java> «strerror.o»
FindStringsByRegex.java> «strerror»
FindStringsByRegex.java> «__strerror_r»
FindStringsByRegex.java> «_dl_signal_error»
FindStringsByRegex.java> «__dlerror»
FindStringsByRegex.java> «_dlerror_run»
FindStringsByRegex.java> «_dl_catch_error»
FindStringsByRegex.java> 20 matching strings were found
FindStringsByRegex.java> Finished!

Этот простой пример показывает, как невысок барьер на
пути к развитым возможностям написания скриптов на Java
в Ghidra. Существующие скрипты легко модифицировать, да
и новые нетрудно писать с нуля в диспетчере скриптов. В главах 15 и 16 мы представим более сложные средства, но отметим, что Java – лишь один из языков написания скриптов, поддерживаемых Ghidra. Другим является Python.
398

ГЛАВА 14

Скрипты на Python
Из 240 с лишним системных скриптов лишь горстка написана на Python. Их легко найти, отобрав файлы с расширением
.py в диспетчере скриптов. Большая часть Python-скриптов
находится в категории Examples.Python, и все они содержат
предупреж­дение следующего вида:

Рис. 14.7. Пример Python-скрипта с предупреждением
(это повторение скрипта Ghidra, написанного на Java,
приводится только для примера. В производственной
среде используйте версию на Java)
Из всех примеров в этом каталоге следующие три являются
неплохой отправной точкой для тех, кто предпочитает Python:
► ghidra_basic.py. Включает примеры базовых скриптов
на Python в контексте Ghidra;
► python_basics.py. Элементарное введение во многие команды Python, которые могут оказаться полезными;
► jython_basic.py. Не столь элементарные команды для
демонстрации специфики Jython.
Средства Ghidra, продемонстрированные в этих примерах, –
лишь самая верхушка айсберга, состоящего из различных API
Ghidra. Скорее всего, вам придется потратить некоторое время
на ознакомление с библиотекой примеров на Java, прежде чем
вы сможете в полной мере задействовать Java API Ghidra из
своих скриптов на Python.
Основы написания скриптов для Ghidra

399

Помимо способности выполнять Python-скрипты, Ghidra
предоставляет интерпретатор Python, позволяющий использовать код на Python или Jython для прямого доступа к объектам
Java, относящимся к Ghidra (см. рис. 14.8).

Будущее Python в Ghidra
Python – популярный язык для создания скриптов благодаря
своей простоте и многочисленным библиотекам. Хотя большинство скриптов в выпускной версии Ghidra написаны на
Java, сообщество, сформировавшееся вокруг инструментов
обратной разработки с открытым исходным кодом, вероятно,
выберет Python как основной язык программирования скриптов в Ghidra. В своей поддержке Python Ghidra опирается на
Jython (его преимущество – прямой доступ к объектам Ghidra,
написанным на Java). Jython совместим с Python 2 (точнее, версией 2.7.1), но не с Python 3. Хотя жизненный цикл
Python 2 закончился в январе 2020 года, скрипты, написанные
на Python 2, по-прежнему будут работать в Ghidra, но все новые скрипты должны быть написаны так, чтобы их было легко
перенести на Python 3.

Рис. 14.8. Пример печати с помощью интерпретатора Python
Интерпретатор Python доступен из браузера кода с по­
мощью команды меню WindowsPython. Дополнительные
сведения о нем см. в справке по Ghidra Help. Для получе-

400

ГЛАВА 14

ния информации об API во время работы с Python и интерпретатором Python выберите пункт HelpGhidra API Help
в левом верхнем углу окна интерпретатора, показанного на
рис. 14.8, – откроется документация по классу GhidraScript.
В Python также встроена функция help(), модифицированная таким образом, что дает прямой доступ к документации
по Ghidra в формате Javadoc. Для ее использования введите в интерпретаторе help(object), как показано на рис. 14.9.
Например, help(currentProgram) отображает документацию по
API класса Ghidra ProgramDB.

Рис. 14.9. Пример получения справки из интерпретатора Python

Поддержка других языков
Наконец, Ghidra поддерживает скрипты на языках, отличных
от Java и Python, что позволяет включить существующие скрипты из вашего арсенала средств обратной разработки в технологический процесс на основе Ghidra. Эта функциональность
более подробно обсуждается в справке по Ghidra.

Основы написания скриптов для Ghidra

401

ВВЕДЕНИЕ В GHIDRA API
Сейчас вы располагаете всей информацией, необходимой для
написания и выполнения скриптов Ghidra. Пора воспользоваться Ghidra API, чтобы обогатить ваши умения и научиться
более непосредственно взаимодействовать с объектами Ghidra.
Ghidra раскрывает свой API двумя разными способами.
Program API определяет многоуровневую иерархию объектов с корнем в классе Program. Этот API может изменяться при
переходе от одной версии Ghidra к другой. Flat API «разравнивает» API Program, раскрывая все его уровни через один класс
FlatProgramAPI. Flat API часто оказывается более удобным способом доступа ко многим конструкциям Ghidra. И к тому же он
реже изменяется.
Далее в этой главе мы рассмотрим наиболее полезные части
Flat API. При необходимости мы будем также рассказывать
о классах, входящих в Program API. Для обсуждения выбран
язык Java, потому что это «родной» язык Ghidra.
Ghidra API состоит из большого числа пакетов, классов
и функций для интерфейса с проектами и связанными с ними
файлами. Все они документированы в стиле Javadoc, и для доступа к документации достаточно нажать красный значок плюс
в окне диспетчера скриптов. Эта документация в сочетании
с примерами скриптов, поставляемыми вместе в Ghidra, – ваш
основной источник информации об API и их использовании.
Самый простой способ понять, как сделать нечто, – просмот­
реть имена классов Ghidra, стараясь найти нечто похожее на
то, что вам нужно. По мере накопления опыта работы с Ghidra
вы станете лучше ориентироваться в соглашениях об именовании и организации файлов, это поможет находить подходящие
классы быстрее.
Ghidra придерживается архитектуры модель–делегат, принятой в Java Swing, т. е. значения и характеристики данных
хранятся в объектах модели и отображаются интерфейсными
объектами-делегатами: дерево, список, таблица и т. д. Делегаты обрабатывают события, например щелчки мышью, и обновляют данные и их представления. В подавляющем большинстве случаев ваши скрипты будут иметь дело с данными,
инкапсулированными в классах моделей, которые служат для
402

ГЛАВА 14

представления различных конструкций программы и обратной разработки.
Далее в этом разделе мы будем рассматривать наиболее употребительные классы моделей, их связи между собой и полезные API для взаимодействия с ними. Мы не станем пытаться охватить Ghidra API целиком, так что имейте в виду, что
в вашем распоряжении еще очень много функций и классов.
Авторитетным источником информации по всему Ghidra API
является документация в формате Javadoc, поставляемая
в комплекте с Ghidra, а окончательным арбитром – исходный
код Ghidra, написанный на Java.

Интерфейс Address
Интерфейс Address описывает модель адреса в адресном пространстве. Все адреса представлены смещениями длиной до
64 бит. Сегментированные адреса можно дополнительно квалифицировать значением сегмента. Во многих случаях смещение адреса эквивалентно виртуальному адресу в листинге программы. Метод getOffset выделяет значение смещения типа
long из объекта Address. Многие функции Ghidra API требуют
объектов Address в качестве аргументов или возвращают такие
объекты в качестве результата.

Интерфейс Symbol
Интерфейс Symbol определяет общие свойства всех символов.
Как минимум, символ состоит из имени и адреса. Эти атрибуты
можно получить с помощью следующих методов:
Address getAddress()
Возвращает адрес символа.
String getName()
Возвращает имя символа.

Интерфейс Reference
Интерфейс Reference моделирует перекрестную ссылку (см.
главу 9) между исходным и конечным адресами. Ссылка характеризуется своим типом. Из полезных методов Reference отметим следующие:
Основы написания скриптов для Ghidra

403

public Address getFromAddress()
Возвращает исходный адрес ссылки.
public Address getToAddress()
Возвращает конечный адрес ссылки.
public RefType getReferenceType()
Возвращает объект RefType, описывающий природу связи
между исходным и конечным адресами.

Класс GhidraScript
Этот класс не моделирует какой-то конкретный атрибут двоичного файла, но каждый скрипт должен быть подклассом
GhidraScript, который, в свою очередь, является подклассом
FlatProgramAPI. Благодаря этому скрипты могут мгновенно обращаться ко всему Flat API, а вам нужно только предоставить
реализацию метода
protected abstract void run() throws Exception;

который, надо полагать, заставляет ваш скрипт делать чтото интересное. Остальные части класса GhidraScript дают
доступ к различным общим ресурсам для взаимодействия
с пользователем Ghidra и анализируемой программой. В следующих разделах перечислены некоторые полезные функции
и данные-члены этого класса (включая унаследованные от
FlatProgramAPI).

Полезные данные-члены
Класс GhidraScript предоставляет удобный доступ к ряду объектов, к которым часто обращаются скрипты.
protected Program currentProgram;
Текущая открытая программа. Класс Program обсуждается
ниже. Это поле открывает доступ к более интересной информации, например спискам команд и символов.
protected Address currentAddress;
Адрес, ассоциированный с текущим положением курсора.
Класс Address обсуждается ниже.
404

ГЛАВА 14

protected ProgramLocation currentLocation;
Объект ProgramLocation представляет текущее положение
курсора, включая его адрес, строку, столбец и другую информацию.
protected ProgramSelection currentSelection;
Объект ProgramSelection представляет диапазон адресов,
выбранных в пользовательском интерфейсе Ghidra.
protected TaskMonitor monitor;
Класс TaskMonitor обновляет состояние долго работающих
задач и проверяет, не была ли задача отменена пользователем (monitor.isCancelled()). Любой написанный вами долго
работающий цикл должен включать обращения к функции
monitor.isCancelled в качестве дополнительного условия
завершения – только так можно распознать, что пользователь пытался снять скрипт.

Функции пользовательского интерфейса
Класс GhidraScript содержит вспомогательные функции для
простых операций пользовательского интерфейса, от простого
вывода сообщений до интерактивных элементов. Ниже описаны некоторые общеупотребительные функции.
public void println(String message)
Печатает сообщение message, сопровождаемое переводом
строки, в окне консоли Ghidra. Полезна для ненавязчивой
печати сообщений о состоянии или результатов скрипта.
public void printf(String message, Object... args)
Сообщение message выступает в роли форматной строки
Java, в которую подставляются аргументы args, и результат
выводится в окно консоли Ghidra.
public void popup(final String message)
Отображает сообщение message в диалоговом окне. Для
продолжения работы скрипта пользователь должен нажать
кнопку OK. Это более навязчивый способ показать пользователю сообщение о состоянии.

Основы написания скриптов для Ghidra

405

public String askString(String title, String message)
Одна из многих имеющихся функций семейства ask. Функция askString открывает диалоговое окно, в котором message
выступает в роли приглашения, и возвращает текст, введенный пользователем.
public boolean askYesNo(String title, String question)
Задает в диалоговом окне вопрос, требующий ответа «да» или
«нет». Возвращает true в случае «да» и false в случае «нет».
public Address askAddress(String title, String message)
Открывает диалоговое окно, в котором message выступает в роли приглашения, и преобразует данные, введенные
пользователем, в объект Address.
public int askInt(String title, String message)
Открывает диалоговое окно, в котором message выступает
в роли приглашения, и преобразует данные, введенные
пользователем, в число типа int.
public File askFile(final String title, final String
approveButtonText)
Отображает диалоговое окно выбора файла и возвращает объект File, представляющий файл, выбранный пользователем.
public File askDirectory(final String title, final String
approveButtonText)
Отображает диалоговое окно выбора файла и возвращает
объект File, представляющий каталог, выбранный пользователем.
public boolean goTo(Address address)
Позиционирует все связанные окна дизассемблера, так чтобы был виден адрес address. Перегруженные варианты этой
функции принимают в качестве аргумента объект типа
Symbol или Function и позиционируют окна на нем.

Функции для работы с адресами
Для процессора адрес – обычно просто число, указывающее
адрес в памяти. Ghidra моделирует адреса с помощью класса
Address. Класс GhidraScript предоставляет функцию-обертку
для преобразования числа в объект Address:
406

ГЛАВА 14

public Address toAddr(long offset)
Вспомогательная функция, создающая объект Address в адресном пространстве по умолчанию.

Чтение памяти программы
Класс Memory представляет непрерывный диапазон байтов,
например содержимое исполняемого файла, загруженного
в Ghidra. Внутри объекта Memory с каждым значением байта
ассоциирован адрес, хотя адреса могут быть помечены как не­
инициализированные, т. е. из них нельзя извлечь никакого значения. Ghidra возбуждает исключение MemoryAccessException
при попытке обратиться к элементу внутри объекта Memory с недействительным адресом. Полное описание всех функций API
см. в документации по классу Memory. Ниже перечислены функции, раскрывающие часть функциональности класса Memory
с помощью Flat API:
public byte getByte(Address addr)
Возвращает значение одного байта по адресу addr. Тип данных byte в Java соответствует байту со знаком, т. е. изменяется в диапазоне –128..127.
public byte[] getBytes(Address addr, int length)
Возвращает length байт из памяти, начиная с addr.
public int getInt(Address addr)
Возвращает 4-байтовое значение, начинающееся с addr,
в виде типа Java int. При сборке целого числа из байтов
учитывается порядок байтов и архитектура, для которой собран двоичный файл.
public long getLong(Address addr)
Возвращает 8-байтовое значение, начинающееся с addr,
в виде типа Java long. При сборке длинного целого числа
из байтов учитывается порядок байтов и архитектура, для
которой собран двоичный файл.

Функции поиска в программе
Средства поиска Ghidra разбросаны по разным классам Program
API в зависимости от типа искомого элемента. В классе Memory
находится функциональность поиска неформатированных байОсновы написания скриптов для Ghidra

407

тов. Поиск по элементам кода (например, Data и Instruction)
и тексту комментариев реализован в классе Listing, там же
находятся соответствующие итераторы. Поиск по символам
и меткам и ассоциированные итераторы включены в класс
SymbolTable. Ниже перечислены функции, раскрывающие часть
функциональности поиска с помощью Flat API:
public Data getFirstData()
Возвращает первый элемент данных в программе.
public Data getDataAfter(Data data)
Возвращает следующий после data элемент данных или
null, если такового не существует.
public Data getDataAt(Address address)
Возвращает элемент данных по адресу address или null,
если такового не существует.
public Instruction getFirstInstruction()
Возвращает первую команду в программе.
public Instruction getInstructionAfter(Instruction instruction)
Возвращает следующую после instruction команду или
null, если таковой не существует.
public Instruction getInstructionAt(Address address)
Возвращает команду по адресу address или null, если таковой не существует.
public Address find(String text)
Ищет строку text в окне листинга. Компоненты листинга
просматриваются в следующем порядке:
1) вводные комментарии;
2) предварительные комментарии;
3) метки;
4) мнемонические команды и операнды;
5) концевые комментарии;
6) повторяющиеся комментарии;
7) заключительные комментарии.
В случае успешного поиска возвращается совпавший адрес.
Заметим, что поскольку поиск производится в определенном
порядке, может быть возвращено не первое вхождение текста
в лис­тинг дизассемблера в смысле строгого возрастания адресов.
408

ГЛАВА 14

public Address find(Address start, byte[] values);
Ищет в памяти, начиная с адреса addr, заданную последовательность байтов values. Если addr равен null, то поиск
начинается с наименьшего действительного адреса в двоичном файле. В случае успешного поиска возвращается
адрес первого байта найденной последовательности.
public Address findBytes(Address start, String byteString)
Ищет в памяти, начиная с адреса addr, заданную строку
байтов byteString, которая может содержать регулярные
выражения. Если addr равен null, то поиск начинается
с наименьшего действительного адреса в двоичном файле. В случае успешного поиска возвращается адрес первого
байта найденной последовательности.

Манипулирование метками и символами
Необходимость манипулировать именованными адресами
в базе данных Ghidra возникает в скриптах довольно часто.
Для этого предоставляются следующие функции.
public Symbol getSymbolAt(Address address)
Возвращает символ Symbol, ассоциированный с данным
адресом, или null, если с адресом не связан никакой символ.
public Symbol createLabel(Address address, String name, boolean
makePrimary)
Сопоставляет имя name адресу address. Ghidra допускает сопоставление нескольких имен одному адресу. Если
makePrimary равно true, то новое имя становится основным
именем, связанным с данным адресом.
public List getSymbols(String name, Namespace namespace)
Возвращает список всех символов с именем name в пространстве имен namespace. Если namespace равно null, то производится поиск в глобальном пространстве имен. Если результат
пуст, значит, символа с таким именем не существует. Если результат содержит всего один элемент, значит, имя уникально.

Работа с функциями
Многие скрипты предназначены для анализа функций в программе. Для доступа к информации о функциях служат следующие функции.
Основы написания скриптов для Ghidra

409

public final Function getFirstFunction()
Возвращает первый объект Function в программе.
public Function getGlobalFunctions(String name)
Возвращает объект Function с заданным именем или null,
если такой функции не существует.
public Function getFunctionAt(Address entryPoint)
Возвращает объект Function, соответствующий функции по
адресу entryPoint, или null, если такой функции не сущест­
вует.
public Function getFunctionAfter(Function function)
Возвращает объект Function, соответствующий функции,
следующей за function, или null, если такой функции
не существует.
public Function getFunctionAfter(Address address)
Возвращает объект Function, соответствующий функции,
которая начинается после адреса address, или null, если
такой функции не существует.

Работа с перекрестными ссылками
Перекрестные ссылки рассматривались в главе 9. В Ghidra
Program API верхнеуровневый объект Program содержит объект ReferenceManager, управляющий ссылками в программе.
Как и для многих других программных конструкций, Flat API
предлагает функции для доступа к перекрестным ссылкам, некоторые из которых перечислены ниже.
public Reference[] getReferencesFrom(Address address)
Возвращает массив всех объектов Reference с исходным
адресом address.
public Reference[] getReferencesTo(Address address)
Возвращает массив всех объектов Reference с конечным
адресом address.

Функции манипулирования программой

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

ГЛАВА 14

public final void clearListing(Address address)
Удаляет команду или данные по указанному адресу.
public void removeFunctionAt(Address address)
Удаляет функцию по указанному адресу.
public boolean disassemble(Address address)
Выполняет дизассемблирование методом рекурсивного
спус­ка, начиная с указанного адреса. Возвращает true, если
операция была успешной.
public Data createByte(Address address)
Преобразует элемент по указанному адресу в байт данных. Имеются также функции createWord, createDword,
createQword и другие такого рода.
public boolean setEOLComment(Address address, String comment)
Добавляет концевой комментарий по заданному адресу.
С комментариями также связаны функции setPlateComment,
setPreComment и setPostComment.
public Function createFunction(Address entryPoint, String name)
Создает функцию с именем name по адресу entryPoint.
Ghidra пытается автоматически определить, где кончается
функция, отыскивая команду возврата.
public Data createAsciiString(Address address)
Создает завершающуюся нулем строку ASCII-символов по
адресу address.
public Data createAsciiString(Address address, int length)
Создает строку ASCII-символов длины length по адресу
address. Если length равно или меньше нуля, то Ghidra пытается самостоятельно найти нуль, завершающий строку.
public Data createUnicodeString(Address address)
Создает завершающуюся нулем строку символов Юникода
по адресу address.

Класс Program
Класс Program находится в корне иерархии Program API и на самом внешнем уровне модели данных двоичного файла. Обычно он используется (часто в виде объекта currentProgram) для
Основы написания скриптов для Ghidra

411

доступа к модели. К числу наиболее употребительных функций-членов класса Program относятся следующие.
public Listing getListing()
Возвращает объект Listing для текущей программы.
public FunctionManager getFunctionManager()
Возвращает объект FunctionManager, который предоставляет
доступ ко всем функциям, найденным в двоичном файле.
Этот класс позволяет сопоставлять адресу Address содержащую его функцию Function (Function getFunction Containing
(Address addr)). Кроме того, он предоставляет итератор
FunctionIterator, полезный, когда требуется обработать все
функции в программе.
public SymbolTable getSymbolTable()
Возвращает объект SymbolTable для программы. С помощью
этого объекта можно работать с отдельными символами или
обойти все символы в программе.
public Memory getMemory()
Возвращает объект Memory для программы, который позволяет работать с памятью программы на уровне неформатированных байтов.
public ReferenceManager getReferenceManager()
Возвращает объект ReferenceManager для программы, который можно использовать для добавления и удаления ссылок. Кроме того, он позволяет получить итераторы для многих типов ссылок.
public Address getMinAddress()
Возвращает наименьший действительный адрес, принадлежащий программе. Чаще всего это базовый адрес двоичного файла.
public Address getMaxAddress()
Возвращает наибольший действительный адрес, принадлежащий программе.
public LanguageID getLanguageID()
Возвращает спецификацию языка двоичного файла в виде
объекта. Затем саму спецификацию можно получить с помощью функции getIdAsString().
412

ГЛАВА 14

Интерфейс Function
Интерфейс Function определяет требуемое Program API поведение объектов функций. Методы предоставляют доступ к различным атрибутам функций.
public String getPrototypeString(boolean formalSignature,
boolean includeCallingConvention)
Возвращает прототип объекта Function в виде строки. Аргументы определяют формат прототипа.
public AddressSetView getBody()
Возвращает множество адресов, занятых телом функции.
Множество адресов состоит из одного или нескольких
диапазонов адресов, поскольку код функции может находиться в нескольких несмежных участках памяти. Для
посещения всех адресов множества следует получить
итератор AddressIterator, а для перебора диапазонов –
итератор AddressRangeIterator. Отметим, что для получения самих команд, составляющих тело функции, нужно использовать объект Listing (см. описание функции
getInstructions).
public StackFrame getStackFrame()
Возвращает кадр стека функции. Результат можно использовать для получения детальной информации о размещении локальных переменных функции и аргументов в стеке.

Интерфейс Instruction
Интерфейс Instruction определяет требуемое Program API поведение объектов команд. Функции-члены предоставляют доступ к различным атрибутам команд.
public String getMnemonicString()
Возвращает мнемоническое обозначение команды.
public String getComment(int commentType)
Возвращает комментарий типа commentType, связанный с командой, или null, если комментарий такого типа с командой
не связан. Аргумент commentType может принимать значения
EOL_COMMENT, PRE_COMMENT, POST_COMMENT и REPEATABLE_COMMENT.
Основы написания скриптов для Ghidra

413

public int getNumOperands()
Возвращает число операндов команды.
public int getOperandType(int opIndex)
Возвращает битовую маску, состоящую из флагов типов операндов, определенных в классе OperandType.
public String toString()
Возвращает представление команды в виде строки.

ПРИМЕРЫ СКРИПТОВ GHIDRA
Далее в этой главе мы представим несколько довольно типичных ситуаций, когда для ответа на вопрос о программе имеет
смысл использовать скрипт. Для краткости показано только
тело функции run каждого скрипта.

Пример 1: перечисление функций
Многие скрипты применяются к отдельным функциям. Примерами могут служить построение дерева вызовов с корнем
в конкретной функции, построение графа потока управления
функции и анализ кадров стека каждой функции в программе.
В листинге 14.1 мы перебираем все функции программы и печатаем основную информацию о каждой, а именно: начальный
и конечный адреса, суммарный размер аргументов функции
и суммарный размер ее локальных переменных. Вся информация выводится в окно консоли.
// ch14_1_flat.java
void run() throws Exception {
int ptrSize = currentProgram.getDefaultPointerSize();
 Function func = getFirstFunction();
while (func != null && !monitor.isCancelled()) {
String name = func.getName();
long addr = func.getBody().getMinAddress().getOffset();
long end = func.getBody().getMaxAddress().getOffset();
 StackFrame frame = func.getStackFrame();
 int locals = frame.getLocalSize();
 int args = frame.getParameterSize();
printf(«Функция: %s, начинается %x, заканчивается %x\n», name, addr, end);
printf(« Область локальных переменных занимает %d байт\n», locals);

414

ГЛАВА 14

printf(« Область аргументов занимает %d байт (%d аргументов)\n», args,

args / ptrSize);
 func = getFunctionAfter(func);
}
}

Листинг 14.1. Скрипт перечисления функций
В скрипте используется Flat API для обхода всех функций,
начиная с первой , и далее последовательно . Скрипт получает кадр стека каждой функции , а затем суммарный размер
локальных переменных  и аргументов . В конце каждой итерации печатается сводная информация о функции.

Пример 2: перечисление команд
Мы можем перечислить все команды, принадлежащие заданной функции. В листинге 14.2 вычисляется количество команд
в той функции, где сейчас находится курсор:
// ch14_2_flat.java
public void run() throws Exception {
Listing plist = currentProgram.getListing();
 Function func = getFunctionContaining(currentAddress);
if (func != null) {
 InstructionIterator iter = plist.getInstructions(func.getBody(), true);
int count = 0;
while (iter.hasNext() && !monitor.isCancelled()) {
count++;
Instruction ins = iter.next();
}
 popup(String.format(«%s содержит %d команд\n», func.getName(), count));
}
else {
popup(String.format(«По адресу %x нет функции», currentAddress.getOffset()));
}
}

Листинг 14.2. Скрипт перечисления команд
Сначала мы получаем ссылку на функцию, содержащую курсор . Если функция найдена, то далее мы используем объект
Listing, чтобы получить итератор InstructionIterator по командам функции . В цикле подсчитывается количество команд, и результат отображается в окне сообщения .
Основы написания скриптов для Ghidra

415

Пример 3: перечисление перекрестных
ссылок
Обход перекрестных ссылок – дело не простое из-за того, что
слишком уж много есть функций для доступа к ссылкам; к тому
же перекрестные ссылки двусторонние. Чтобы получить желаемое, нужно работать со ссылками подходящего типа.
В первом примере, показанном в листинге 14.3, мы получаем
список всех вызовов функций внутри текущей функции, перебирая все команды и анализируя, вызывает ли команда другую
функцию. Сделать это можно, например, разобрав строку, возвращенную функцией getMnemonicString, – нас интересуют команды call. Но это плохо переносимое и не особенно эффективное решение, потому что названия команд вызова для разных
процессоров разные и придется еще определять, какая именно
функция вызывается. Перекрестные ссылки избавлены от всех
этих сложностей, потому что не зависят от процессора и прямо
сообщают, куда ведет ссылка.
// ch14_3_flat.java
void run() throws Exception {
Listing plist = currentProgram.getListing();
 Function func = getFunctionContaining(currentAddress);
if (func != null) {
String fname = func.getName();
InstructionIterator iter = plist.getInstructions(func.getBody(), true);
 while (iter.hasNext() && !monitor.isCancelled()) {
Instruction ins = iter.next();
Address addr = ins.getMinAddress();
Reference refs[] = ins.getReferencesFrom();
 for (int i = 0; i < refs.length; i++) {
 if (refs[i].getReferenceType().isCall()) {
Address tgt = refs[i].getToAddress();
Symbol sym = getSymbolAt(tgt);
String sname = sym.getName();
long offset = addr.getOffset();
printf(«%s вызывает %s по адресу 0x%x\n», fname, sname, offset);
}
}
}
}
}

Листинг 14.3. Перечисление вызовов функций
416

ГЛАВА 14

Опасные функции
C-функции strcpy и sprintf считаются опасными, потому что допускают копирование в буфер без проверок. Обе можно сделать
безопасными, если программист будет проверять размеры исходного и конечного буферов, но программисты, не ведающие
об опасностях, часто опускают такие проверки. Например, strcpy
объявлена следующим образом:
char *strcpy(char *dest, const char *source);
Эта функция копирует все символы исходного буфера source до
первого нуля включительно в конечный буфер dest. Фундаментальная проблема заключается в том, что во время выполнения
нет никакой возможности узнать размер массива, и strcpy не может определить, достаточно ли в конечном буфере места для
хранения всех данных, копируемых из source. Такие неконтролируемые операции копирования – основная причина уязвимостей,
вызванных переполнением буфера.

Сначала мы получаем ссылку, в которой находится курсор .
Затем мы обходим все команды этой функции  и для каждой
команды обходим все исходящие из нее перекрестные ссылки .
Нас интересуют только ссылки, являющиеся вызовами других
функций, поэтому мы должны проверить значение, возвращенное
getReferenceType , и посмотреть, верно ли, что isCall равно true.

Пример 4: нахождение вызовов функции
Перекрестные ссылки полезны также, когда нужно найти все
команды, ссылающиеся на конкретный адрес. В листинге 14.4
перебираются все перекрестные ссылки, ведущие на указанный символ (в отличие от ссылок из, рассмотренных в предыдущем примере).
// ch14_4_flat.java
 public void list_calls(Function tgtfunc) {
String fname = tgtfunc.getName();
Address addr = tgtfunc.getEntryPoint();
Reference refs[] = getReferencesTo(addr);
 for (int i = 0; i < refs.length; i++) {
Основы написания скриптов для Ghidra

417

 if (refs[i].getReferenceType().isCall()) {
Address src = refs[i].getFromAddress();
 Function func = getFunctionContaining(src);
if (func.isThunk()) {
continue;
}
String caller = func.getName();
long offset = src.getOffset();
 printf("%s вызывается из 0x%x в %s\n", fname, offset, caller);
}
}
}
 public void getFunctions(String name, List list) {
SymbolTable symtab = currentProgram.getSymbolTable();
SymbolIterator si = symtab.getSymbolIterator();
while (si.hasNext()) {
Symbol s = si.next();
if (s.getSymbolType() != SymbolType.FUNCTION || s.isExternal()) {
continue;
}
if (s.getName().equals(name)) {
list.add(getFunctionAt(s.getAddress()));
}
}
}
public void run() throws Exception {
List funcs = new ArrayList();
getFunctions("strcpy", funcs);
getFunctions("sprintf", funcs);
funcs.forEach((f) -> list_calls(f));
}

Листинг 14.4. Перечисление функций, вызывающих данную
В этом примере мы написали вспомогательную функцию
getFunctions , которая собирает объекты Function, ассоциированные с интересующими нас функциями. Для каждой из таких функций мы дополнително вызываем функцию list_calls
, которая обрабатывает все перекрестные ссылки на нее .
Если обнаружена перекрестная ссылка типа вызова , то мы
находим вызывающую функцию  и показываем ее имя пользователю . Такой подход, в частности, можно использовать
для построения дешевого анализатора безопасности, который
предупреждает обо всех вызовах функций типа strcpy и sprintf.
418

ГЛАВА 14

Пример 5: эмуляция поведения языка
ассемблера
Существует много причин для написания скрипта, эмулирующего поведение анализируемой программы. Например, изучаемая
программа может быть самомодифицируемой, как многие вредоносные программы, или содержать закодированные данные, которые раскодируются во время выполнения. Если не запустить
эту программу и не вытащить модифицированные данные из памяти работающего процесса, то как разобраться в ее поведении?
Если процесс декодирования не особенно сложный, то можно
быстро написать скрипт, который выполняет те же действия,
что и работающая программа. Декодирование данных с по­
мощью скрипта избавляет от необходимости запускать программу, которая неизвестно что делает или предназначена для
платформы, к которой у вас нет доступа. Например, не имея
машины с процессором MIPS, вы не сможете выполнить написанную для него программу и наблюдать за процессом декодирования данных. Однако можно написать скрипт Ghidra,
который имитирует поведение двоичного файла и вносит необходимые изменения, не нуждаясь в платформе MIPS.
Следующий код для процессора x86 взят из двоичного файла, представленного на соревновании «Захвати флаг» в рамках
конференции DEFCON1:
08049ede MOV dword ptr [EBP + local_8],0x0
LAB_08049ee5
08049ee5 CMP dword ptr [EBP + local_8],0x3c1
08049eec JA LAB_08049f0d
08049eee MOV EDX,dword ptr [EBP + local_8]
08049ef1 ADD EDX,DAT_0804b880
08049ef7 MOV EAX,dword ptr [EBP + local_8]
08049efa ADD EAX,DAT_0804b880
08049eff MOV AL,byte ptr [EAX]=>DAT_0804b880
08049f01 XOR EAX,0x4b
08049f04 MOV byte ptr [EDX],AL=>DAT_0804b880
08049f06 LEA EAX=>local_8,[EBP + -0x4]
08049f09 INC dword ptr [EAX]=>local_8
08049f0b JMP LAB_08049ee5
1

Публикуется с разрешения Kenshoto, организаторов «Захвати флаг» на
DEFCON 15. Это ежегодное соревнование хакеров, проводимое в рамках
DEFCON (http://www.defcon.org/).
Основы написания скриптов для Ghidra

419

Этот код декодирует закрытый ключ, включенный в двоичный файл. С помощью скрипта на рис. 14.5 мы можем извлечь
закрытый ключ, не запуская программу.
// ch14_5_flat.java
public void run() throws Exception {
int local_8 = 0;
while (local_8 = 1) {
Object[] opObjects0 = inst.getOpObjects(0);
for (int ii = 0; ii < opObjects0.length; ii++) {
if (opObjects0[ii] instanceof Register) {
return true;
}
}
}
return false;
}
//***********************************************************************
// Этот метод решает, является ли команда "начальной" командой
// потенциального гаджета ROP
//***********************************************************************
private boolean isStartInstruction(Instruction inst) {
if (startInstr0Params.contains(inst.getMnemonicString())) {
return true;
}
if (startInstr1RegParam.contains(inst.getMnemonicString()) &&
inst.getNumOperands() >= 1) {
Object[] opObjects0 = inst.getOpObjects(0);
for (int ii = 0; ii < opObjects0.length; ii++) {
if (opObjects0[ii] instanceof Register) {
return true;
}
}
}
return false;
}

Ghidra вызывает метод анализатора added , чтобы начать
анализ. Наш алгоритм проверяет каждую команду  в двоичном файле, чтобы понять, годится она на роль «начальной»
или нет . Встретив подходящую начальную команду, анализатор вызывает функцию создания гаджета buildGadget .
Создание гаджета – это рекурсивный проход назад по списку
команд, который продолжается до тех пор, пока встретившие­
ся команды нам полезны . Наконец, каждый найденный
гаджет печатается, для чего перебираются входящие в его состав команды .

450

ГЛАВА 15

Шаг 4: протестировать анализатор в Eclipse
В процессе разработки код часто тестируется и модифицируется.
По мере построения анализатора мы можем тестировать его
функциональность в Eclipse, выполняя команду Run As и выбирая из списка Ghidra. В результате текущая версия модуля
временно устанавливается в Ghidra, и Ghidra запускается. Если
результаты не совпадают с ожидаемыми, то файл редактируется и тестируется еще раз. Если результат нас устраивает, переходим к шагу 5. Такой способ тестирования кода, не выходя из
Eclipse, может сэкономить много времени в процессе разработки.

Шаг 5: добавить анализатор в Ghidra
Чтобы добавить анализатор в установку Ghidra, нужно экспор­тировать модуль из Eclipse, а затем установить
его как расширение Ghidra. Для экспорта выполните команду GhidraDevExportGhidra Module Extension
(GhidraDevЭкспортМодуль расширения Ghidra), выберите
свой модуль и нажмите кнопку Next. В следующем окне выберите вариант Gradle Wrapper (Обертка Gradle), как показано на
рис. 15.23, если на вашей машине не установлен Gradle (отметим,
что необходимо подключение к интернету, чтобы обертка могла обратиться к сайту gradle.org). Нажмите Finish для завершения процедуры экспорта. Ели вы экспортируете модуль впервые, то в ваш
модуль в Eclipse будет добавлен каталог dist, в котором будет сохранен zip-файл, содержащий экспортированное содержимое.

Рис. 15.23. Диалоговое окно конфигурирования Gradle
Eclipse и GhidraDev

451

В окне проекта Ghidra добавьте новый анализатор, выполнив
команду FileInstall Extensions (Файл Установить расширения). Откроется окно, как на рис. 15.24, в котором будут показаны все существующие, но еще не установленные расширения.

Рис. 15.24. Окно установки расширений
Добавьте новый анализатор SimpleROP, щелкнув по значку + в правом верхнем углу и перейдя к только что созданному zip-файлу в каталоге dist. Когда ваш анализатор появится
в списке, вы сможете выбрать его и нажать кнопку OK (не показано). Перезапустите Ghidra, чтобы воспользоваться новой
функциональностью из меню Analysis.

Шаг 6: тестирование анализатора
в Ghidra
Мы ограничили план разработки и точно так же ограничим
план тестирования простой демонстрацией функциональности. Анализатор SimpleROP проходит приемочное тестирование, поскольку удовлетворяет следующим критериям.
1. (Проходит) SimpleROP появился в списке Analysis Options,
который открывается после выбора из меню пункта
CodeBrowserAnalysis.
2. (Проходит) Описание SimpleROP появляется в окне описания параметров анализа при выборе этого анализатора.

452

ГЛАВА 15

То, что тесты 1 и 2 проходят, видно на рис. 15.25. (Если бы мы
запрограммировали и зарегистрировали параметры анализатора на шаге 3-5, то они были бы показаны на панели Options
в правой части окна.)

Рис. 15.25. Окно параметров анализа
3. (Проходит) Анализатор SimpleROP выполняется, если его
выбрать.
В данном случае мы запускали SimpleROP для уже проанализированного файла и в качестве составной частиавтоматического анализа. Выполнение SimpleROP для непроанализированного файла не должно дать никаких результатов, т. к.
расширения типа INSTRUCTION_ANALYZER требуют, чтобы команды были предварительно идентифицированы (что по умолчанию делается во время автоматического анализа). Когда
SimpleROP выполняется как часть автоматического анализа,
его очередность устанавливается в соответствии с типом, заданным на шаге 3.2. На рис. 15.26 показана часть журнала Ghidra,
из которой видно, что анализатор SimpleROP запускался.

Eclipse и GhidraDev

453

Рис. 16.26. Окно журнала Ghidra, в котором видно, что
анализатор запускался
4. (Проходит) SimpleROP записывает каждый гаджет в файл
с именем fileZZZ_gadgets.txt, если анализируется файл fileZZZ.
Следующая выдержка из файла call_tree_x64_static_gadgets.
txt показывает, что многие гаджеты взяты из части листинга
call_tree_x64_static на рис. 15.27:
00400412;ADD RSP,0x8;RET;
004004ce;NOP;RET;
00400679;ADD RSP,0x8;POP RBX;POP RBP;POP R12;POP R13;POP R14;POP R15;RET;
0040067d;POP RBX;POP RBP;POP R12;POP R13;POP R14;POP R15;RET;
0040067e;POP RBP;POP R12;POP R13;POP R14;POP R15;RET;
0040067f;POP R12;POP R13;POP R14;POP R15;RET;
00400681;POP R13;POP R14;POP R15;RET;
00400683;POP R14;POP R15;RET;
00400685;POP R15;RET;
00400a8b;POP RBP;MOV EDI,0x6babd0;JMP RAX;
00400a8c;MOV EDI,0x6babd0;JMP RAX;
00400a98;POP RBP;RET;

454

ГЛАВА 15

Рис. 15.27. Листинг call_tree_x64_static в браузере кода

РЕЗЮМЕ
В главе 14 мы познакомились со скриптами как средством
расширения возможностей Ghidra. В этой главе мы продемонстрировали модули расширения Ghidra, а также интеграцию
с Eclipse. Хотя Eclipse – не единственный способ редактирования
расширений Ghidra, интеграция Ghidra с Eclipse IDE образует
невероятно мощную среду для разработчиков, занимающихся
расширением возможностей Ghidra. Мастера и шаблоны опус­
кают входной барьер, поскольку подсказывают кодировщику,
как нужно правильно модифицировать существующие и создавать новые расширения. В главе 16 мы рассмотрим необслуживаемый режим Ghidra, упомянутый на рис. 15.18. А в последующих главах будем опираться на интеграцию Ghidra с Eclipse
IDE для дальнейшего расширения возможностей Ghidra, что
поможет сделать Ghidra оптимальным инструментом в вашем
технологическом процессе обратной разработки.

16

НЕОБСЛУЖИВАЕМЫЙ РЕЖИМ
GHIDRA

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

ПРИСТУПАЯ К РАБОТЕ
Вспомним, как мы впервые подступились к Ghidra в главе 4.
Мы успешно выполнили следующие шаги:
1)
2)
3)
4)
5)
6)

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

Повторим эти шаги, воспользовавшись командным режимом необслуживаемого анализатора. Сам необслуживаемый
анализатор (analyzeHeadless или analyzeHeadless.bat), а также
полезный файл analyzeHeadlessREADME.html можно найти
в подкаталоге support установочного каталога Ghidra. Чтобы упростить пути к файлам, мы временно поместили файл
global_array_demo_x64 в тот же каталог. Сначала опишем команды и параметры, необходимые для выполнения отдельных
задач, а затем соберем все вместе и достигнем поставленной
цели. Хотя существенных отличий от предыдущих глав и нет,
при работе из командной строки между тремя платформами,
поддерживаемыми Ghidra, различий больше. В примерах ниже
мы будем работать с Windows, но отмечать важные отличия на
других платформах.

Прямая или обратная косая черта?
Важное различие между операционными системами, поддерживаемыми Ghidra, – способ записи путей в файловой системе.
В целом синтаксис похож, но символы-разделители компонентов
пути разные. В Windows используется обратная косая черта, а в
Linux и macOS – прямая. Путь в Windows выглядит так:
D:\GhidraProjects\ch16\demo_stackframe_32
А такой же путь в Linux и macOS – так:
/GhidraProjects/ch16/demo_stackframe_32

458

ГЛАВА 16

Путаница осложняется еще и тем, что в Windows для разделения
компонентов URL-адреса и для обозначения флагов в командной строке (в т. ч. в документации Ghidra) используется прямая
косая черта. Операционные системы знают об этой проблеме
и стараются принимать оба символа, но не всегда предсказуемым образом. В примерах из этой главы мы будем использовать
соглашения Windows, так что читатели могут радоваться обратной совместимости с DOS.

Шаг 1: запуск Ghidra
Этот шаг выполняется командой analyzeHeadless. Все дополнительные шаги определяются параметрами данной команды.
Если запустить analyzeHeadless без параметров, то будет выведено сообщение о порядке использования, в котором перечислены все параметры (рис. 16.1). Для запуска Ghidra необходимо указать некоторые из них.

Рис. 16.1. Порядок запуска необслуживаемого анализатора

Шаги 2 и 3: создать новый проект Ghidra
в указанном месте
В необслуживаемом режиме Ghidra создает проект, если он
еще не существует. Если в указанном месте уже есть проект,
то Ghidra открывает его. Поэтому необходимы два параметра:
Необслуживаемый режим Ghidra

459

местоположение и имя проекта. Следующая команда создает
проект CH16 в каталоге D:\GhidraProjects:
analyzeHeadless D:\GhidraProjects CH16

Это необходимый минимум – при таких параметрах Ghidra
откроет проект и больше ничего не сделает. На самом деле
Ghidra прямо так и скажет:
Nothing to do...must specify -import, -process, or prescript and/or postscript.

(Нечего делать... необходимо задать флаг -import, -process
или prescript и (или) postscript.)

Шаг 4: импортировать файл в проект
Для импорта файла необходимо задать флаг -import и имя
файла. Мы импортируем файл global_array_demo_x64, который уже использовали ранее. Как было отмечено выше, для
простоты мы поместили файл в каталог support. Но можно
было бы задать в командной строке полный путь к файлу.
analyzeHeadless D:\GhidraProjects CH16 -import global_array_demo_x64

Шаги 5 и 6: автоматический анализ
файла, сохранение и выход
В необслуживаемом режиме автоматический анализ и сохранение производятся по умолчанию, поэтому команда на шаге 4
делает все, что нам нужно. Параметр нужен, чтобы отменить
анализ файла (-noanalysis), существуют также параметры,
управляющие порядком сохранения проекта и связанных
с ним файлов.
Вот как выглядит полная команда для достижения всех шести
заявленных целей:
analyzeHeadless D:\GhidraProjects CH16 -import global_array_demo_x64

460

ГЛАВА 16

Как часто бывает с консольными командами, возникает вопрос: «Откуда мне знать, происходит ли что-нибудь?» Первый
признак успеха (или неудачи) – сообщения, отображаемые на
консоли. Информационные сообщения, начинающиеся словом
INFO, сообщают о ходе работы. Сообщения об ошибках начинаются словом ERROR. В листинге 16.1 показано подмножество сообщений, в т. ч. сообщение об ошибке:
 INFO HEADLESS Script Paths:
C:\Users\Ghidrabook\ghidra_scripts
 D:\ghidra_PUBLIC\Ghidra\Extensions\SimpleROP\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Features\Base\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Features\BytePatterns\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Features\Decompiler\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Features\FileFormats\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Features\FunctionID\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Features\GnuDemangler\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Features\Python\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Features\VersionTracking\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Processors\8051\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Processors\DATA\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Processors\PIC\ghidra_scripts(HeadlessAnalyzer)
INFO HEADLESS: execution starts (HeadlessAnalyzer)
INFO Opening existing project: D:\GhidraProjects\CH16
(HeadlessAnalyzer)
 ERROR Abort due to Headless analyzer error:
ghidra.framework.store.LockException:
Unable to lock project! D:\GhidraProjects\CH16 (HeadlessAnalyzer)
java.io.IOException: ghidra.framework.store.LockException:
Unable to lock project! D:\GhidraProjects\CH16
...

Рис. 16.1. Ошибка в работе необслуживаемого анализатора
Перечислены пути к скриптам, используемые в необслуживаемом режиме . Ниже в этой главе мы покажем, как применить дополнительные скрипты в необслуживаемых командах.
Созданное в предыдущей главе расширение SimpleROP включено в пути к скриптам , потому что любое расширение добавляет новый путь. Исключение LockException  – пожалуй,
самая частая ошибка при работе с необслуживаемым анализатором. Ошибка имеет место, когда мы пытаемся применить

Необслуживаемый режим Ghidra

461

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

Рис. 16.2. Вывод результатов необслуживаемого анализатора
на консоль
Чтобы проверить результаты в Ghidra GUI, откройте проект
и убедитесь, что файл был загружен, как показано на рис. 16.3,
а затем откройте файл в браузере кода и проверьте, что анализ
был выполнен.
462

ГЛАВА 16

Рис. 16.3. Подтверждение создания проекта и загрузки файла
в графическом интерфейсе Ghidra
Повторив сделанное ранее в необслуживаемом режиме
Ghidra, рассмотрим ситуации, когда необслуживаемый режим оказывается лучше GUI. Чтобы создать проект, загрузить
и проанализировать все файлы, показанные на рис. 16.4, с помощью Ghidra GUI, нам пришлось загружать файлы по одному или выбрать их перед выполнением операции пакетного
импорта, как было описано в разделе «Пакетный импорт» главы 11. В необслуживаемом режиме мы можем просто указать
каталог и проанализировать все находящиеся в нем файлы.

Необслуживаемый режим Ghidra

463

Рис. 16.4. Входной каталог с примерами для необслуживаемого
режима Ghidra
Следующая команда просит необслуживаемый анализатор открыть или создать проект с именем CH16 в каталоге
D:\GhidraProjects, а затем импортировать и проанализировать все файлы в каталоге D:\ch16:
analyzeHeadless D:\GhidraProjects CH16 -import D:\ch16

После того как команда завершится, мы можем открыть новый проект в Ghidra GUI и увидим там все проанализированные файлы, как показано на рис. 16.5. Ни подкаталог D:\ch16\
CH16_subdirectory, ни находящиеся в нем файлы в проекте
не показываются. Мы вернемся к этому вопросу, когда будем
обсуждать дополнительные флаги и параметры необслуживаемого анализатора в следующем разделе.

464

ГЛАВА 16

Рис. 16.5. Проект, получившийся после задания каталога
в необслуживаемом режиме

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

Флаги общего назначения
Ниже приведены и проиллюстрированы на простых примерах
краткие описания дополнительных флагов, которые позволяют управлять происходящим (перенесенные строки напечатаны с отступом). Когда уместно, обсуждаются типичные ошибки.
Редкие ошибки оставляем читателю в качестве упражнения на
освоение справки по Ghidra.
-log logfilepath
При выполнении команды многое может пойти не так (или
так). По счастью, плагины Ghidra непрерывно сообщают о том,
что происходит во время работы. В Ghidra GUI эта обратная

Необслуживаемый режим Ghidra

465

связь не так существенна (поскольку на экране мы и так видим, что творится), но в необслуживаемом режиме важна.
По умолчанию журнал записывается в файл .ghidra/.
ghidra__PUBLIC/application.log в домашнем каталоге пользователя. Можно указать другое место, задав флаг
-log в командной строке. Чтобы создать каталог CH16-logs
и записать журнал в файл CH16-logfile, введите такую команду:
analyzeHeadless D:\GhidraProjects CH16 -import global_array_demo_x64
-log D:\GhidraProjects\CH16-logs\CH16-Logfile

-noanalysis
Этот флаг говорит Ghidra, что не нужно анализировать
файлы, импортированные в командной строке. Если выполнить показанную ниже команду и открыть файл global_
array_demo_x64 в Ghidra GUI, то мы увидим загруженный,
но не проанализированный файл в проекте CH16:
analyzeHeadless D:\GhidraProjects CH16 -import global_array_demo_x64
-noanalysis

-overwrite
В листинге 16.1 мы видели ошибку, случившуюся, когда
Ghidra попыталась открыть уже открытый проект. Другая
типичная ошибка возникает, когда Ghidra пытается импортировать файл в проект, а файл уже был импортирован.
Чтобы импортировать новую версию файла или перезаписать существующий файл, несмотря ни на что, задайте флаг
-overwrite. Если этот флаг не задан, то выполнение следующей команды два раза подряд приведет к ошибке при втором запуске. А с флагом -overwrite мы можем запускать ее
сколько угодно раз.
analyzeHeadless D:\GhidraProjects CH16 -import global_array_demo_x64
-overwrite

466

ГЛАВА 16

-readOnly
Чтобы импортировать файл, но не сохранять его в проекте,
задайте флаг ‑readOnly. В таком случае флаг -overwrite игнорируется (если он задан). Этот флаг имеет смысл также
при задании вместе с флагом -process, а не ‑import . Флаг
-process будет рассмотрен ниже в этой главе.
analyzeHeadless D:\GhidraProjects CH16 -import global_array_demo_x64
-readOnly

-deleteProject
Этот флаг означает, что Ghidra не должна сохранять проект, созданный благодаря флагу –import. Его можно использовать вместе с любыми флагами, но предполагается, что
задан флаг -readOnly (даже если он опущен). Вновь созданный проект удаляется после завершения анализа. Сущест­
вующий проект с помощью этого флага не удаляется.
analyzeHeadless D:\GhidraProjects CH16 -import global_array_demo_x64
-deleteProject

-recursive
По умолчанию Ghidra не заглядывает в подкаталоги, когда
ее просят обработать целый каталог. Этот флаг означает,
что Ghidra должна рекурсивно обрабатывать каталог, т. е.
заходить во все встретившиеся по пути подкаталоги. Например, следующая команда требует обработать тот же каталог ch16, что и раньше, но теперь рекурсивно:
analyzeHeadless D:\GhidraProjects CH16 -import D:\ch16 -recursive

Открыв проект CH16 после завершения этой команды, мы
увидим структуру, показанную на рис. 16.6. В отличие от
рис. 16.5, подкаталог CH16_subdirectory теперь включен
в проект вместе с находящимся в нем файлом.

Необслуживаемый режим Ghidra

467

Рис. 16.6. Результат обработки проекта необслуживаемым
анализатором с флагом -recursive

Метасимволы!
Метасимволы – простой способ выбрать несколько файлов в необслуживаемом режиме, не перечисляя их по отдельности. Звездочка (*) сопоставляется с любой последовательностью символов,
а вопросительный знак (?) с одним любым символом. Чтобы загрузить и проанализировать только 32-разрядные двоичные файлы,
нужно воспользоваться метасимволами следующим образом:
analyzeHeadless D:\GhidraProjects CH16 -import D:\ch16\demo_
stackframe_32*
Эта команда создаст проект CH16, а затем загрузит в него и проанализирует все 32-разрядные файлы в каталоге ch16. Результат
показан на рис. 16.7. Подробнее об использовании метасимволов для задания файлов, подлежащих импорту и обработке, см.
файл analyzeHeadlessREADME.html. Далее мы еще столкнемся
с метасимволами в примерах необслуживаемых скриптов Ghidra.

468

ГЛАВА 16

Рис. 16.7. Файлы проекта, загруженные при задании demo_
stackframe_32* с метасимволами

-analysisTimeoutPerFile seconds
Когда вы анализировали файлы (или сидели и наблюдали
за тем, как это делает Ghidra), вы, возможно, обратили внимание на несколько факторов, влияющих на время анализа, как то: размер файла, скомпонован он статически или
динамически, параметры декомпилятора. Но каким бы ни
было содержимое файла и параметры, заранее невозможно
сказать, сколько времени займет анализ.
В необслуживаемом режиме Ghidra, особенно если вы обрабатываете сразу много файлов, можно задать флаг
analysisTimeoutPerFile, гарантирующий, что задача закончится за разумное время. Тайм-аут задается в секундах,
и по его истечении анализ прерывается. Например, команда
Ghidra, которую мы все время приводим в пример, в нашей
системе работает чуть дольше одной секунды (см. рис. 16.2).
Если бы мы действительно захотели ограничить время работы этого скрипта, то могли бы задать следующую команду,
которая прекратила бы анализ через одну секунду:
analyzeHeadless D:\GhidraProjects CH16 -import global_array_demo_x64
-analysisTimeoutPerFile 1

Необслуживаемый режим Ghidra

469

Тогда мы увидели бы на консоли картину, показанную на
рис. 16.8.

Рис. 16.8. Предупреждение на консоли о прерывании анализа по
тайм-ауту
-processor languageID и-cspec compilerSpecID
Как показано в предыдущих примерах, обычно Ghidra отлично справляется с определением информации о файле
и дает правильные рекомендации по импорту. Пример
окна с рекомендациями для конкретного файла показан на
рис. 16.9. Это окно отображается всегда при использовании
GUI для импорта файла в проект.

Рис. 16.9. Диалоговое окно подтверждения импорта в Ghidra GUI
Если вы полагаете, что обладаете дополнительной информацией о языке или компиляторе, то можете нажать кнопку
с многоточием справа от спецификации языка. Тогда откроется
окно, показанное на рис. 16.10, где вы сможете выбрать язык
и спецификацию компилятора.
470

ГЛАВА 16

Рис. 16.10. Окно выбора языка и спецификации компилятора
Чтобы сделать то же самое в необслуживаемом режиме Ghidra,
воспользуйтесь флагом -cspec и (или) -processor, как показано ниже. Задавать -cspec можно только вместе с -processor,
но -processor можно задавать и без -cspec – в таком случае
Ghidra выбирает компилятор, ассоциированный с данным процессором по умолчанию.
analyzeHeadless D:\GhidraProjects CH16 -import global_array_demo_x64
-processor "x86:LE:64:default" -cspec "gcc"

-loader loadername
Флаг -loader самый сложный из всех параметров необслуживаемого анализатора. Аргумент loadername задает имя
модуля загрузчика Ghidra (обсуждается в главе 17), который будет использоваться для импорта файла в поименованный проект. Вот несколько примеров имен загрузчиков:
PeLoader, ElfLoader, MachoLoader. Каждый модуль загрузчика понимает еще и свои собственные аргументы в командной строке. Эти дополнительные аргументы описаны в файле support/analyzeHeadlessREADME.html.
-max-cpu number
Этот флаг позволяет ограничить сверху количество процессорных ядер, задействованных для обработки необслуживаемой команды Ghidra. Флаг требует задания целого числа
в качестве аргумента. Если его значение меньше 1, то максимальное число ядер будет равно 1.
Необслуживаемый режим Ghidra

471

analyzeHeadless D:\GhidraProjects CH16 -import global_array_demo_x64
-max-cpu 5

Флаги при работе с сервером
Некоторые команды используются только при взаимодействии
с сервером Ghidra. Поскольку эта тема не является для нас основной, мы лишь кратко упомянем эти команды. Дополнительные
сведения можно найти в файле analyzeheadlessREADME.html.
ghidra://server[:port]/repository_name[/folder_path]
Во всех предыдущих примерах мы задавали местоположение или имя проекта. Этот параметр позволяет задать репозиторий на сервере Ghidra и необязательный путь к папке.
-p

При работе с сервером Ghidra этот флаг выводит на консоль
приглашение для ввода пароля.

-connect [userID]
Этот флаг позволяет задать идентификатор пользователя
userID для подключения к серверу Ghidra Server вместо
подразумеваемого по умолчанию.
-keystore path
Этот флаг позволяет задать файл с закрытым ключом при
аутентификации по протоколу PKI или SSH.
-commit ["comment"]
Если включен режим фиксации по умолчанию, то этот флаг
позволяет задать комментарий, связанный с данной фиксацией.

Флаги скриптов
Пожалуй, самые важные приложения необслуживаемого режима Ghidra связаны со скриптами. В главах 14 и 15 было показано, как создаются и используются скрипты в графическом
интерфейсе. Представив флаги скриптов, мы затем продемонстрируем, каким мощным инструментом может быть необслуживаемый режим Ghidra в скриптовом контексте.
472

ГЛАВА 16

-process [project_file]
Этот флаг означает, что выбранные файлы нужно обработать,
а не импортировать. Если файл не указан, то обрабатываются все
файлы в папке проекта. Все указанные файлы будут также проанализированы, если только не задан флаг ‑noanalysis. Ghidra
понимает два метасимвола (* и ?) в аргументе флага ‑process,
чтобы упростить задание нескольких файлов. В отличие от флага ‑import, в данном случае задаются имена импортированных
в проект файлов, а не файлов в локальной файловой системе,
поэтому имена, содержащие эти метасимволы, нужно заключать
в кавычки, чтобы оболочка не расширила их раньше времени.
-scriptPath "path1[;path2...]"
По умолчанию в необслуживаемом режиме рассматривается много путей к скриптам по умолчанию, а также к скриптам импортированных расширений, как показано на
рис. 16.1. Чтобы еще расширить список путей, по которым
Ghidra ищет скрипты, воспользуйтесь флагом –scriptPath,
задав в качестве аргумента заключенный в кавычки список
путей. В путях распознаются две специальные переменные:
$GHIDRA_HOME и $USER_HOME. Первая ссылается на установочный каталог Ghidra, вторая – на домашний каталог пользователя. Отметим, что это не переменные окружения, и, значит, ваша оболочка может потребовать, чтобы начальный
знак $ экранировался, иначе он не будет передан Ghidra.
В команде ниже в состав путей к скриптам добавляется каталог D:\GhidraScripts:
analyzeHeadless D:\GhidraProjects CH16 -import global_array_demo_x64
-scriptPath "D:\GhidraScripts"

После выполнения этой команды каталог D:\GhidraScripts
будет включен в состав путей к скриптам:
INFO HEADLESS Script Paths:
D:\GhidraScripts
C:\Users\Ghidrabook\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Extensions\SimpleROP\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Features\Base\ghidra_scripts
Необслуживаемый режим Ghidra

473

D:\ghidra_PUBLIC\Ghidra\Features\BytePatterns\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Features\Decompiler\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Features\FileFormats\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Features\FunctionID\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Features\GnuDemangler\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Features\Python\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Features\VersionTracking\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Processors\8051\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Processors\DATA\ghidra_scripts
D:\ghidra_PUBLIC\Ghidra\Processors\PIC\ghidra_scripts (HeadlessAnalyzer)
INFO HEADLESS: execution starts (HeadlessAnalyzer)

-preScript
Этот флаг задает имя скрипта, который нужно выполнить
перед анализом. Скрипт может содержать необязательный
список аргументов.
-postScript
Этот флаг задает имя скрипта, который нужно выполнить
после анализа. Скрипт может содержать необязательный
список аргументов.
-propertiesPath
Этот флаг задает путь к файлам свойств, ассоциированным
со скриптом. Файлы свойств передают входные данные
скриптам, работающим в необслуживаемом режиме. Примеры скритов и их файлов свойств имеются в документации
по необслуживаемому анализатору.
-okToDelete
Поскольку скрипт может делать все, что задумал его автор, он, в частности, может и удалять (или попробовать
удалить) файлы, являющиеся частью проекта Ghidra. Чтобы предотвратить этот нежелательный побочный эффект,
Ghidra при работе в необслуживаемом режиме не позволяет
удалять файлы из скрипта, если только не был задан флаг
-okToDelete. Примечание: этот флаг необязателен, если задан флаг -import.

474

ГЛАВА 16

НАПИСАНИЕ СКРИПТОВ
Теперь, когда вы понимаете основные компоненты необслуживаемой команды Ghidra, давайте напишем несколько скриптов, предназначенных для запуска из командной строки.

HeadlessSimpleROP
Вспомните анализатор SimpleROP, разработанный в главе 15.
Мы написали модуль в Eclipse IDE, а затем импортировали его
в Ghidra, чтобы его можно было применить к любому импортированному файлу. Теперь мы хотим задать каталог и потребовать, чтобы скрипт нашел гаджеты ROP во всех (или только
в избранных) файлах, находящихся в этом каталоге. И еще мы
хотим, чтобы скрипт не только записывал найденные гаджеты в выходной файл, свой для каждого двоичного файла, но
и формировал сводный файл, в котором указано, сколько гаджетов найдено в каждом файле.
В такой постановке запуск SimpleROP из Ghidra GUI потребовал бы много времени на открытие и закрытие браузера
кода, чтобы показать каждый файл в окне листинга, и другие подобные действия. Но нам вовсе не нужно видеть файлы
в окне браузера кода, чтобы решить поставленную задачу. Так
почему бы не написать скрипт, который будет искать гаджеты
независимо от GUI? Именно для таких случаев и предназначен необслуживаемый режим Ghidra.
Конечно, для достижения цели можно было бы модифицировать SimpleROP, но мы не хотим жертвовать существующим
расширением Ghidra, которое, быть может, полезно другим
пользователям. (Мы понимаем, что написали его не далее, как
в предыдущей главе… но, кто знает, вдруг оно уже стало вирусным?) Так что лучше напишем на основе SimpleROP новый
скрипт HeadlessSimpleROP, который будет искать все гаджеты
ROP в файле , записывать их в файл _
gadgets.txt, а затем дописывать строку, содержащую путь
/ и количество найденных гаджетов в конец
сводного файла gadget_summary.txt. Вся прочая функциональность (разбор каталогов, файлов и т. д.) будет предоставлена
самой Ghidra благодаря рассмотренным выше флагам.
Необслуживаемый режим Ghidra

475

Чтобы упростить разработку, мы создадим новый скрипт
с помощью плагина Eclipse GhidraDev, описанного в главе 15, а затем скопируем в новый шаблон скрипта исходный
код SimpleROPAnalyzer.java и внесем необходимые изменения. Наконец, мы запустим этот скрипт, указав его в аргументе флага -postScript, чтобы он вызывался после этапа анализа
для каждого открытого файла.

Создание шаблона скрипта HeadlessSimpleROP
Начнем с создания шаблона. Из меню GhidraDev выберите
пункт NewGhidraScript и введите необходимую информацию в диа­логовом окне, показанном на рис. 16.11. Скрипт можно
было бы поместить в любую папку, но мы выберем папку ghidra_
scripts внутри уже существующего модуля SimpleROP в Eclipse.

Рис. 16.11. Диалоговое окно создания скрипта Ghidra
Нажмите кнопку Finish, чтобы увидеть новый шаблон
скрипта со всеми метаданными (рис. 16.12). Тег задачи в строке 14 показывает, с чего начать.

476

ГЛАВА 16

Рис. 16.12. Новый шаблон скрипта HeadlessSimpleROP
Чтобы преобразовать анализатор SimpleROP в скрипт
HeadlessSimpleROP, необходимо сделать следующее:
1) удалить лишние предложения import;
2) удалить открытые методы анализатора;
3) продублировать функциональность метода added, который вызывается при запуске анализатора SimpleROPAnalyzer, в методе run, вызываемом при запуске скрипта HeadlessSimpleROP;
4) добавить дописывание имени файла и числа найденных
в нем гаджетов в сводный файл gadget_summary.txt.
Мы поместим скрипт HeadlessSimpleROP в каталог D:\
GhidraScripts и воспользуемся необслуживаемым анализатором для демонстрации его функциональности. В следующих разделах мы прогоним серию тестов, запуская скрипт
HeadlessSimpleROP для файлов и каталогов, показанных на
рис. 16.6. Заодно продемонстрируем некоторые флаги необслуживаемого анализатора в действии.

Тест 1: загрузить, проанализировать
и обработать один файл
Следующая команда импортирует, анализирует и вызывает
наш скрипт для формирования отчета о гаджетах в одном файле (символ ^ означает продолжение строки в командной оболочке Windows):
Необслуживаемый режим Ghidra

477

analyzeHeadless D:\GhidraProjects CH16_ROP ^
-import D:\ch16\demo_stackframe_32 ^
-scriptPath D:\GhidraScripts ^
-postScript HeadlessSimpleROP.java

Во время выполнения необслуживаемый анализатор Ghidra
создает проект CH16_ROP в каталоге GhidraProjects, а затем импортирует, загружает и анализирует файл demo_stackframe_32.
Каталог для скрипта мы задали с помощью флага scriptPath.
По завершении анализа к файлу применяется наш скрипт.
Когда команда закончится, проверим содержимое файлов
gadget_summary.txt и demo_stackframe_32_gadgets.txt, чтобы
убедиться, что наш скрипт отработал правильно. В файле demo_
stackframe_32_gadgets.txt оказалось 16 потенциальных гаджетов ROP:
080482c6;ADD ESP,0x8;POP EBX;RET;
080482c9;POP EBX;RET;
08048343;MOV EBX,dword ptr [ESP];RET;
08048360;MOV EBX,dword ptr [ESP];RET;
08048518;SUB ESP,0x4;PUSH EBP;PUSH dword ptr [ESP + 0x2c];PUSH dword ptr
[ESP + 0x2c]; CALL dword ptr [EBX + EDI*0x4 + 0xffffff0c];
0804851b;PUSH EBP;PUSH dword ptr [ESP + 0x2c];PUSH dword ptr [ESP +
0x2c];
CALL dword ptr [EBX + EDI*0x4 + 0xffffff0c];
0804851c;PUSH dword ptr [ESP + 0x2c];PUSH dword ptr [ESP + 0x2c];
CALL dword ptr [EBX + EDI*0x4 + 0xffffff0c];
08048520;PUSH dword ptr [ESP + 0x2c];CALL dword ptr [EBX + EDI*0x4 +
0xffffff0c];
08048535;ADD ESP,0xc;POP EBX;POP ESI;POP EDI;POP EBP;RET;
08048538;POP EBX;POP ESI;POP EDI;POP EBP;RET;
08048539;POP ESI;POP EDI;POP EBP;RET;
0804853a;POP EDI;POP EBP;RET;
0804853b;POP EBP;RET;
0804854d;ADD EBX,0x1ab3;ADD ESP,0x8;POP EBX;RET;
08048553;ADD ESP,0x8;POP EBX;RET;
08048556;POP EBX;RET;

А вот как выглядит файл gadget_summary.txt:
demo_stackframe_32: Найдено потенциальных гаджетов: 16

478

ГЛАВА 16

Тест 2: загрузить, проанализировать
и обработать все файлы в каталоге
В этом тесте мы импортируем весь каталог, а не один файл:
analyzeHeadless D:\GhidraProjects CH16_ROP ^
-import D:\ch16 ^
-scriptPath D:\GhidraScripts ^
-postScript HeadlessSimpleROP.java

По завершении необслуживаемого анализатора в файле
gadget_summary.txt будут находиться следующие строчки:
demo_stackframe_32: Найдено потенциальных гаджетов: 16
demo_stackframe_32_canary: Найдено потенциальных гаджетов: 16
demo_stackframe_32_stripped: Найдено потенциальных гаджетов: 16
demo_stackframe_64: Найдено потенциальных гаджетов: 24
demo_stackframe_64_canary: Найдено потенциальных гаджетов: 24
demo_stackframe_64_stripped: Найдено потенциальных гаджетов: 24

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

Тест 3: загрузить, проанализировать
и обработать все файлы в каталоге рекурсивно
В этом тесте мы добавим флаг -recursive. В данном случае рекурсивно посещаются все файлы во всех подкаталогах каталога ch16:
analyzeHeadless D:\GhidraProjects CH16_ROP ^
-import D:\ch16 ^
-scriptPath D:\GhidraScripts ^
-postScript HeadlessSimpleROP.java ^
-recursive

По завершении необслуживаемого анализатора в файле
gadget_summary.txt будут находиться следующие строки, причем первым идет файл, находящийся в подкаталоге:
Необслуживаемый режим Ghidra

479

demo_stackframe_32_sub: Найдено потенциальных гаджетов: 16
demo_stackframe_32: Найдено потенциальных гаджетов: 16
demo_stackframe_32_canary: Найдено потенциальных гаджетов: 16
demo_stackframe_32_stripped: Найдено потенциальных гаджетов: 16
demo_stackframe_64: Найдено потенциальных гаджетов: 24
demo_stackframe_64_canary: Найдено потенциальных гаджетов: 24
demo_stackframe_64_stripped: Найдено потенциальных гаджетов: 24

Тест 4: загрузить, проанализировать
и обработать все 32-разрядные файлы в каталоге
В этом тесте мы используем * в качестве метасимвола оболочки, чтобы импортировать только файлы, содержащие признак
32-разрядности:
analyzeHeadless D:\GhidraProjects CH16ROP ^
-import D:\ch16\demo_stackframe_32* ^
-recursive ^
-postScript HeadlessSimpleROP.java ^
-scriptPath D:\GhidraScripts

Файл gadget_summary file содержит следующие строки:
demo_stackframe_32: Найдено потенциальных гаджетов: 16
demo_stackframe_32_canary: Найдено потенциальных гаджетов: 16
demo_stackframe_32_stripped: Найдено потенциальных гаджетов: 16

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

Автоматизированное создание базы
данных FidDb
В главе 13 мы начали создавать базу данных об идентификаторах функций (FidDb), в которой хранятся цифровые отпечатки
функций из статической версии библиотеки libc. Используя режим пакетного импорта в графическом интерфейсе Ghidra, мы
480

ГЛАВА 16

импортировали 1690 объектных файлов из архива libc.a. Однако когда дело дошло до анализа, мы столкнулись с препятствием, потому что в GUI имеется лишь минимальная поддержка
пакетного анализа. Но теперь мы знакомы с необслуживаемым
режимом Ghidra и можем завершить создание FidDb.

Пакетный импорт и анализ
Импорт и анализ 1690 файлов из архива когда-то казался нам
неподъемной задачей, но предыдущие примеры содержат все,
что нужно знать для ее практического решения. Мы рассмотрим
два случая и для каждого приведем примеры командной строки.
Если архив libc.a еще не был импортирован в проект Ghidra,
то извлечем его содержимое в каталог, а затем воспользуемся необслуживаемым режимом Ghidra для обработки всего каталога:
$ mkdir libc.a && cd libc.a
$ ar x path\to\archive && cd ..
$ analyzeHeadless D:\GhidraProjects CH16 –import libc.a ^
-processor x86:LE:64:default –cspec gcc –loader ElfLoader ^
-recursive

Команда выводит тысячи строк, потому что Ghidra сообщает
о каждом из 1690 обрабатываемых файлов, но после ее завершения в нашем проекте будет создан новый каталог libc.a, содержащий 1690 проанализированных файлов.
Если мы с помощью GUI импортировали libc.a, но не обработали ни одного файла, то для анализа можно выполнить следующую команду:
$ analyzeHeadless D:\GhidraProjects CH16\libc.a –process

Когда весь статический архив будет импортирован и проанализирован, мы сможем воспользоваться плагином Function ID
для создания и заполнения базы FidDb, как описано в главе 13.

Необслуживаемый режим Ghidra

481

РЕЗЮМЕ
Хотя GUI остается самой простой и полнофункциональной версией Ghidra, запуск в необслуживаемом режиме предлагает
чрезвычайно гибкие возможности для создания сложных инструментов на базе автоматического анализа Ghidra. На данный момент мы рассмотрели все наиболее употребительные
средства и изучили, как заставить Ghidra поработать для нас.
Пришло время перейти к более продвинутым средствам.
В следующих нескольких главах мы рассмотрим подходы
к некоторым более трудным проблемам, возникающим в процессе обратной разработки двоичных файлов, в т. ч. обработке
неизвестных форматов файлов и процессорных архитектур путем создания изощренных расширений Ghidra. Мы также поговорим о декомпиляторе Ghidra и обсудим некоторые различия
в походах компиляторов к генерации кода; это поможет вам бегло читать листинги дизассемблера.

Часть IV

ДОПОЛНИТЕЛЬНЫЕ
ТЕМЫ

17

ЗАГРУЗЧИКИ GHIDRA

Если не считать краткой демонстрации загрузчика Raw Binary в главе 4, то Ghidra успешно
определила тип, загрузила и проанализировала
все файлы, которые мы ей подсовывали. Однако
так бывает не всегда. Рано или поздно вы, вероятно, столкнетесь с диалоговым окном, показанным на рис. 17.1.
(Этот конкретный файл содержит шелл-код, который Ghidra
не смогла распознать, потому что у него нет ни какой-то определенной структуры, ни осмысленного расширения имени,
ни магического числа.)

Рис. 17.1. Пример загрузчика Raw Binary
Так что же случилось при попытке импортировать этот файл?
Начнем с обзора процесса загрузки файла в Ghidra.
1. В окне проекта пользователь указывает файл, который хочет загрузить. Импортер Ghidra опрашивает все загрузчики,
и каждый пытается идентифицировать файл. Те, кто может
загрузить файл, возвращают список спецификаций, которые
будут отображаться в диалоговом окне импорта (или пустой
список, означающий «Я не могу загрузить этот файл»).
2. Импортер собирает ответы всех загрузчиков, строит список
тех, что распознали файл, и показывает заполненное окно
импорта пользователю.
3. Пользователь выбирает загрузчик и связанную с ним информацию.
4. Импортер вызывает выбранный пользователем загрузчик,
который загружает файл.
В случае файла на рис. 17.1 ни один загрузчик конкретного
формата не ответил «да». В результате задача была передана
единственному загрузчику, готовому в любой момент загрузить любой файл, – Raw Binary. Этот загрузчик почти ничего
не делает сам, перекладывая всю ответственность на плечи инженера, выполняющего обратную разработку. Если вы оказались в ситуации, когда нужно проанализировать такие файлы

486

ГЛАВА 17

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

АНАЛИЗ НЕИЗВЕСТНОГО ФАЙЛА
Ghidra включает модули загрузчиков для распознавания
многих распространенных форматов исполняемых и архивных файлов, но совершенно невозможно научить ее всем
форматам хранения исполняемого кода, число которых постоянно растет. Двоичные образы могут содержать исполняемые файлы в формате специализированных операционных
систем, образы ПЗУ, экспортированные из встраиваемых систем, обновления прошивок различных устройств, да и прос­
то неформатированные куски машинного кода, быть может,
извлеченные из сетевых пакетов. Формат образа может быть
продиктован операционной системой (исполняемые файлы),
целевым процессором и архитектурой системы (образы ПЗУ),
а может вообще отсутствовать (шелл-код эксплойта, внедряемый в приложение).
В предположении, что имеется процессорный модуль, который дизассемблирует код, содержащийся в неизвестном двоичном файле, на вас ложится задача правильно организовать
образ памяти, а затем проинформировать Ghidra о том, какие
части двоичного файла представляют код, а какие – данные.
Для большинства типов процессоров результат загрузки файла без форматирования – это просто все содержимое, сваленное
в один сегмент, начинающийся с нулевого адреса, как показано в листинге 17.1.

Загрузчики Ghidra

487

00000000
00000001
00000002
00000003
00000004
00000005
00000006
00000007

4d ??
5a ??
90 ??
00 ??
03 ??
00 ??
00 ??
00 ??

4Dh M
5Ah Z
90h
00h
03h
00h
00h
00h

Листинг 17.1. Начальные строки непроанализированного
PE-файла, загруженного загрузчиком Raw Binary
Иногда, если выбранный процессорный модуль достаточно
изощренный, может быть произведено частичное дизассемблирование. Например, выбранный процессор для встраиваемого
микроконтроллера может делать определенные предположения
о структуре памяти образа ПЗУ, а анализатор, знающий о типичных последовательностях кода для конкретного процессора, может оптимистически отформатировать совпадения с ними как код.
Столкнувшись с нераспознанным файлом, соберите столько
информации о файле, сколько сможете. Пригодятся сведения
о том, как и где был получен файл, любые упоминания о процессоре и операционной системе, проектная документация по
системе и любая информация о структуре памяти, полученная
путем отладки или аппаратного анализа (например, с применением анализаторов логики).
В следующем разделе мы для примера предположим, что
Ghidra не распознает формат Windows PE. На самом деле PE –
хорошо известный формат файлов, и многие читатели с ним
наверняка знакомы. И что особенно важно, подробная документация этого формата широко доступна, поэтому анализ
произвольного PE-файла – сравнительно простая задача.

ЗАГРУЗКА PE-ФАЙЛА WINDOWS
ВРУЧНУЮ
Если удается найти документацию по формату конкретного
файла, то жизнь становится заметно проще, и ваши попытки
воспользоваться Ghidra, чтобы извлечь смысл из двоичного фай488

ГЛАВА 17

ла, скорее, принесут успех. В листинге 17.1 показаны первые
несколько строк непроанализированного PE-файла, который
был загружен в Ghidra загрузчиком Raw Binary с применением
спецификации языка и процессора x86:LE:32:default:windows1.
В документации о формате PE сказано, что корректный PEфайл должен начинаться заголовком MS-DOS, в котором первые два символа – 2-байтовая сигнатура 4Dh 5Ah (MZ); именно ее
мы видим в первых двух строчках листинга 17.12. 4-байтовое
значение, расположенное со смещением 0x3C от начала файла,
содержит смещение следующего интересующего нас заголовка,
а именно заголовка PE.
Есть две стратегии разбора полей заголовка MS-DOS: (1)
выделить значения из каждого поля заголовка, правильно
указав размеры, и (2) воспользоваться диспетчером типов
данных Ghidra, чтобы определить и применить структуру
IMAGE_DOS_HEADER в соответствии со спецификацией формата
PE. Мы рассмотрим трудности, связанные с первым вариантом, ниже в этой главе. В данном случае вариант 2 требует
куда меньше усилий.
Если используется загрузчик Raw Binary, то Ghidra не загружает в диспетчер типов данных типы данных Windows, так
что мы можем загрузить архив, содержащий типы MS-DOS,
windows_vs12_32.gdt, самостоятельно. Найдите IMAGE_DOS_
HEADER непосредственно в архиве, либо нажмите Ctrl-F, чтобы
найти его в окне диспетчера типов данных. Затем перетащите
заголовок на начало файла. Можно также поместить курсор на
первый адрес в листинге, выбрать пункт DataChoose Data
Type из контекстного меню (или нажать клавишу T), а потом
ввести или найти тип данных в появившемся диалоговом окне
выбора типа данных. При любом способе будет создан следующий листинг с содержательными концевыми комментариями
к каждому полю:

1

2

Если в качестве компилятора была выбрана Visual Studio, то в спецификации языка и компилятора появится слово windows. Для большинства
других компиляторов отображаемое имя и имя в спецификации различаются не так сильно.
См. https://docs.microsoft.com/en-us/windows/win32/debug/pe-format.
Загрузчики Ghidra

489

00000000
00000002
00000004
00000006
00000008
0000000a
0000000c
0000000e
00000010
00000012
00000014
00000016
00000018
0000001a
0000001c

00000024
00000026
00000028

0000003c

4d
90
03
00
04
00
ff
00
b8
00
00
00
40
00
00
00
00
00
00
00
00
00
d8

5a
00
00
00
00
00
ff
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00

WORD 5A4Dh e_magic
WORD 90h e_cblp
WORD 3h e_cp
WORD 0h e_crlc
WORD 4h e_cparhdr
WORD 0h e_minalloc
WORD FFFFh e_maxalloc
WORD 0h e_ss
WORD B8h e_sp
WORD 0h e_csum
WORD 0h e_ip
WORD 0h e_cs
WORD 40h e_lfarlc
WORD 0h e_ovno
00 WORD[4] e_res
00
WORD 0h e_oemid
WORD 0h e_oeminfo
00 WORD[10] e_res2
00
00
00 LONG D8h e_lfanew

Поле e_lfanew в последней строке предыдущего листинга
имеет значение D8h, это означает, что заголовок PE следует искать по смещению D8h (216 байт) от начала двоичного файла.
Здесь должно находиться магическое число заголовка PE, 50h
45h (PE), и это означает, что мы должны наложить на двоичный
файл со смещения D8h структуру IMAGE_NT_HEADERS. Ниже приведена часть получающегося в результате листинга Ghidra:
000000d8 IMAGE_NT_HEADERS
000000d8 DWORD 4550h Signature
000000dc IMAGE_FILE_HEADER FileHeader
000000dc WORD 14Ch Machine 
000000de WORD 5h NumberOfSections 
000000e0 DWORD 40FDFD TimeDateStamp
000000e4 DWORD 0h PointerToSymbolTable
000000e8 DWORD 0h NumberOfSymbols
000000ec WORD E0h SizeOfOptionalHeader
000000ee WORD 10Fh Characteristics
000000f0 IMAGE_OPTIONAL_HEADER32 OptionalHeader
000000f0 WORD 10Bh Magic

490

ГЛАВА 17













000000f2
000000f3
000000f4
000000f8
000000fc
00000100
00000104
00000108
0000010c
00000110
00000114













BYTE '\u0006' MajorLinkerVersion
BYTE '\0' MinorLinkerVersion
DWORD 21000h SizeOfCode
DWORD A000h SizeOfInitializedData
DWORD 0h SizeOfUninitializedData
DWORD 14E0h AddressOfEntryPoint 
DWORD 1000h BaseOfCode
DWORD 1000h BaseOfData
DWORD 400000h ImageBase 
DWORD 1000h SectionAlignment 
DWORD 1000h FileAlignment 

И уже здесь мы видим ряд интересных фактов, которые помогут нам уточнить структуру двоичного файла. Во-первых, поле
Machine  в заголовке PE говорит о типе процессора, для которого
собран файл. Значение 14Ch указывает, что файл предназначен
для процессоров типа x86. Если бы тип процессора бы другим,
например 1C0h (ARM), то нужно было бы закрыть браузер кода,
щелкнуть правой кнопкой мыши по файлу в окне проекта и с
помощью команды Set Language (Задать язык) выбрать правильный язык.
Поле ImageBase  содержит базовый виртуальный адрес загруженного из файла образа. Зная эту информацию, мы можем
включить информацию о виртуальном адресе в браузер кода.
Выполнив команду WindowMemory Map (ОкноКарта
памяти), мы увидим список блоков памяти (рис 7.2) текущей
программы. В данном случае имеется единственныйблок
памяти, содержащий всю программу. Загрузчик Raw Binary
не знает, как определять адреса различных участков программы, поэтому помещает все содержимое в один блок, начинающийся с адреса 0.

Рис. 17.2. Окно карты памяти
Загрузчики Ghidra

491

На панели инструментов окна карты памяти рсположены
значки, показанные на рис. 7.3, они позволяют манипулировать блоками памяти. Чтобы правильно отобразить образ на
память, мы прежде всего должны задать базовый адрес, указанный в заголовке PE.

Добавить
блок

Отображает диалоговое окно Add Memory, в котором
можно добавить информацию, необходимую
для создания нового блока

Переместить
блок

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

Разбить блок

Если выбран блок, то эта кнопка позволяет разбить
блок на два

Расширить
вверх

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

Расширить
вниз

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

Объединить
блоки

Если выбраны два или более блоков памяти, то эта
кнопка объединяет их в один

Удалить блок

Удаляет все выбранные блоки памяти

Задать
базовый
адрес образа

Позволяет указать (или изменить) базовый адрес
программы

Рис. 17.3. Панель инструментов в окне карты памяти
Поле ImageBase  сообщает, что базовый адрес этого двоичного файла равен 00400000. С помощью значка Set Image Base
изменим базовый адрес по умолчанию на этот. После нажатия
OK все окна Ghidra будут обновлены, отражая новое расположение программы в памяти, как показано на рис. 17.4. (Будьте
осторожны, используя эту команду; после того как несколько
блоков уже определено, она сдвинет все блоки на то же расстоя­
ние, что и базовый.)

492

ГЛАВА 17

Рис. 17.4. Карта памяти после задания базового адреса образа
Поле AddressOfEntryPoint  задает относительный виртуальный адрес (ОВА, англ. RVA) точки входа в программу. Согласно спецификации формата PE, ОВА – это относительное
смещение от базового виртуального адреса программы, а точка входа в программу – адрес первой выполняемой команды.
В данном случае ОВА точки входа 14E0h означает, что выполнение программы начнется с виртуального адреса 4014E0h
(400000h + 14E0h). Это первое указание на то, где нужно искать
код программы. Но прежде мы должны правильно отобразить
остальную часть программы на виртуальные адреса.
В формате PE для описания отображения содержимого файла
на участки памяти используются секции. Разобрав заголовки
всех секций, мы сможем полностью определить расположение
программы в виртуальной памяти. Поле NumberOfSections  содержит количество секций в PE-файле (в данном случае – пять).
Согласно спецификации, массив структур, описывающих заголовки, расположен сразу после структуры IMAGE_NT_HEADERS.
Его элементами являются структуры типа IMAGE_SECTION_
HEADER, который мы определим в редакторе структур Ghidra
и применим (в данном случае – пять раз) к байтам, следующим
за IMAGE_NT_HEADERS. Можно вместо этого выбрать первый байт
заголовка первой секции и установить его тип равным IMAGE_
SECTION_HEADER[n], где n в данном случае равно 5, – тогда весь
массив свернется в одну отображаемую строку.
Поля FileAlignment  и SectionAlignment  показывают, как
данные в каждой секции выровнены в файле и как те же самые данные будут выровнены после отображения в память.
В нашем примере оба поля говорят о выравнивании на граЗагрузчики Ghidra

493

ницу 1000h1. В формате PE не требуется, чтобы оба числа были
равны. Но тот факт, что они равны, упрощает нам жизнь, поскольку означает, что смещения в дисковом файле равны смещениям в загруженном в память образе файла. Понимать, как
выровнены секции, важно, чтобы избежать ошибок при ручном
создании секций в программе.
Разобравшись со структурой заголовков секций, мы располагаем достаточной информацией для создания дополнительных
сегментов в программе. Наложив шаблон IMAGE_SECTION_HEADER
на байты, следующие сразу за структурой IMAGE_NT_HEADERS, мы
получим заголовок первой секции в листинге Ghidra:
004001d0 IMAGE_SECTION_HEADER
004001d0 BYTE[8] ".text" Name
004001d8 _union_226 Misc
004001d8 DWORD 20A80h PhysicalAddress
004001d8 DWORD 20A80h VirtualSize
004001dc DWORD 1000h VirtualAddress
004001e0 DWORD 21000h SizeOfRawData
004001e4 DWORD 1000h PointerToRawData
004001e8 DWORD 0h PointerToRelocations
004001ec DWORD 0h PointerToLinenumbers
004001f0 WORD 0h NumberOfRelocations
004001f2 WORD 0h NumberOfLinenumbers

Поле Name  говорит, что этот заголовок описывает секцию
.text. Все остальные поля потенциально полезны для форматирования листинга, но мы сосредоточимся на тех трех, что описывают расположение секции в памяти. Поле PointerToRawData 
(1000h) содержит смещение содержимого секции относительно
начала файла. Заметим, что оно кратно величине выравнивания в файле, 1000h. Секции расположены в PE-файле в порядке возрастания смещений (и виртуальных адресов). Поскольку
эта секция начинается со смещения 1000h, то первые 1000h байт
файла содержат данные заголовка и заполнение (если длина
заголовка в байтах менее 1000h, то его нужно дополнить до гра1

494

Выравнивание описывает начальный адрес или смещение блока данных.
Адрес или смещение должны быть кратны величине выравнивания. Например, данные, выровненные на границу 200h (512) байт, должны начинаться по адресу (или смещению), который делится на 200h.

ГЛАВА 17

ницы 1000h байт). Поэтому, хотя байты заголовка, строго говоря, не являются секцией, мы можем выразить их логическую
связанность, сгруппировав в блок памяти в листинге Ghidra.
Ghidra предлагает два способа создания новых блоков памяти – оба в окне карты памяти на рис. 17.2. Значок Add Block
(см. рис. 17.3) открывает диалоговое окно, показанное на
рис. 17.5, которое используется для добавления новых блоков
памяти, не перекрывающихся ни с одним из существующих.
От вас требуется ввести имя нового блока, начальный адрес
и длину. Блок можно инициализировать постоянным значением (например, заполнить нулями), содержимым текущего
файла (вы указываете смещение, начиная с которого брать
данные) или оставить неинициализированным.
Второй способ – разбить существующий блок на две части.
Для этого нужно сначала выбрать блок в окне карты памяти,
а затем щелкнуть по значку Split Block (рис. 17.3), чтобы открыть диалоговое окно, показанное на рис. 17.6. Мы еще только приступаем к делу, поэтому существует всего один блок,
который можно разбить. Для начала разобьем его по границе секции .text, чтобы вырезать заголовки программы из существующего блока. После ввода длины (1000h) отделяемого
блока (секции заголовков) Ghidra автоматически вычисляет
остальные поля адреса и длины. Нам остается только назвать
новый блок, созданный в точке разбиения. Мы выбрали имя,
указанное в заголовке первой секции: .text.

Загрузчики Ghidra

495

Рис. 17.5. Диалоговое окно добавления блока памяти

Рис. 17.6. Диалоговое окно разбиения блока
496

ГЛАВА 17

Теперь в нашей карте памяти два блока. Первый содержит
заголовки программы, его размер установлен правильно. Второй содержит правильно названную секцию .text, но его размер пока неправильный. Эта ситуация показана на рис. 17.7 –
мы видим, что размер секции .text равен 0x29000 байт.

Рис. 17.7. Карта памяти после разбиения блока
Возвращаясь к заголовку секции .text, мы видим, что в поле
Virtual Address  (1000h) находится ОВА, задающий смещение
в памяти (от ImageBase), с которого начинается содержимое секции, и что поле SizeOfRawData  (21000h) показывает, сколько
байтов данных хранится в файле. Иными словами, этот конкретный заголовок сообщает нам, что секция .text создана путем отображения 21000h байт в диапазоне 1000h-21FFFh на виртуальные адреса 401000h-421FFFh.
Поскольку мы разбили исходный блок памяти в начале секции .text, вновь созданная секция .text временно содержит
все остальные секции, т. к. ее текущий размер 0x29000 больше
правильного размера 0x21000. Заглядывая в заголовки остальных секций и повторно разбивая последний блок памяти,
мы в конце концов придем к правильной карте памяти программы. Но, дойдя до следующей пары заголовков секций, мы
столк­немся с проблемой:
00400220 IMAGE_SECTION_HEADER
00400220 BYTE[8] ".data" Name
00400228 _union_226 Misc
00400228 DWORD 5624h PhysicalAddress
00400228 DWORD 5624h VirtualSize
0040022c DWORD 24000h VirtualAddress
00400230 DWORD 4000h SizeOfRawData
00400234 DWORD 24000h PointerToRawData
00400238 DWORD 0h PointerToRelocations
0040023c DWORD 0h PointerToLinenumbers
Загрузчики Ghidra

497

00400240 WORD 0h NumberOfRelocations
00400242 WORD 0h NumberOfLinenumbers
00400244 DWORD C0000040hCharacteristics
00400248 IMAGE_SECTION_HEADER
00400248 BYTE[8] ".idata" Name
00400250 _union_226 Misc
00400250 DWORD 75Ch PhysicalAddress
00400250 DWORD 75Ch VirtualSize
00400254 DWORD 2A000h VirtualAddress
00400258 DWORD 1000h SizeOfRawData
0040025c DWORD 28000h PointerToRawData
00400260 DWORD 0h PointerToRelocations
00400264 DWORD 0h PointerToLinenumbers
00400268 WORD 0h NumberOfRelocations
0040026a WORD 0h NumberOfLinenumbers
0040026c DWORD C0000040h Characteristics

Виртуальный размер секции .data  больше размера в файле . Что это значит и как отражается на карте памяти? Компилятор пришел к выводу, что программе нужно 5624h байт
статической памяти во время выполнения, но отвел только
4000h байт для инициализации этих данных. Остальные 1624h
байт не инициализируются содержимым исполняемого файла,
поскольку предназначены для неинициализированных глобальных переменных. (Такие переменные нередко доводится
видеть в специальной секции программы под названием .bss.)
Чтобы закончить построение карты памяти, мы должны выбрать правильный размер секции .data и убедиться, что последующие секции тоже отображены корректно. Секция .data
отображает 4000h байт файла, начиная со смещения 24000h, на
адрес в памяти 424000h  (ImageBase + VirtualAddress). Следующая секция (.idata) отображает 1000h байт файла, начиная со
смещения 28000h , на адрес в памяти 42A000h . Внимательный
читатель, возможно, заметил, что секция .data, по-видимому,
занимает 6000h байт в памяти (42A000h–424000h), – и так оно
и есть. Дело в том, что для секции .data требуется 5624h байт, но
это число не делится на 1000h, поэтому секция будет дополнена
до 6000h байт, чтобы секция .idata удовлетворяла требованию
о выравнивании, заданном в заголовке PE-файла. Для завершения работы с картой памяти мы должны выполнить следующие действия:
498

ГЛАВА 17

1) разбить секцию .data на границе 4000h. Образовавшаяся
в результате секция .idata на данный момент начинается
по адресу 428000h;
2) переместить секцию .idata по адресу 42A000h, щелкнув
по значку Move Block (рис. 17.3) и задав начальный адрес
42A000h;
3) отделить и, если необходимо, переместить остальные секции, придя в результате к окончательному расположению
программы в памяти;
4) если понадобится, расширить секции, которые в виртуальной памяти должны быть выровнены на большую границу,
чем в файле. В нашем примере виртуальный размер секции
.data, 5624h, выровнен на границу 6000h, тогда как ее размер в файле равен 4000h и выровнен на границу 4000h. Переместив секцию .idata туда, где ей положено находиться,
и тем самым освободив место, мы расширим секцию .data
с 4000h до 6000h байт.
Для расширения секции .data выделите ее в окне карты памяти и щелкните по значку Expand Down (рис. 17.3), чтобы
изменить конечный адрес (или длину) секции. Откроется диалоговое окно расширения блока вниз, показанное на рис. 17.8
(эта операция добавляет суффикс .exp в имя секции).

Рис. 17.8. Диалоговое окно расширения блока вниз
Окончательная карта памяти, полученная в результате серии перемещений, разбиений и расширений блоков, показана
на рис. 17.9. Помимо имени, начального и конечного адресов
и длины, для каждой секции присутствуют флажки разрешения чтения (R), записи (W) и выполнения (X). В PE-файлах эти
значения задаются битами поля Characteristics в заголовке
Загрузчики Ghidra

499

секции. О том, как разбирать это поле и правильно устанавливать разрешения секций, см. спецификацию формата PE.

Рис. 17.9. Окончательный вид окна карты памяти после
создания всех секций
После того как все секции программы правильно отображены, мы должны найти байты, для которых велика вероятность оказаться кодом. Поле AddressOfEntryPoint (ОВА 14E0h,
или виртуальный адрес 4014E0h) отправляет нас к точке входа
в программу, которая заведомо содержит код. По этому адресу
мы видим такие неформатированные байты:
004014e0 ?? 55h U
004014e1 ?? 8Bh
004014e2 ?? ECh
...

Воспользовавшись контекстным меню для дизассемблирования (или нажав горячую клавишу D) с адреса 004014e0, мы запустим процесс рекурсивного спуска (наблюдать за его ходом
можно в правом нижнем углу браузера кода), в результате чего
показанные выше байты будут отформатированы как команды:
FUN_004014e0
004014e0 PUSH EBP
004014e1 MOV EBP,ESP
004014e3 PUSH -0x1
004014e5 PUSH DAT_004221b8
004014ea PUSH LAB_004065f0
004014ef MOV EAX,FS:[0x0]
004014f5 PUSH EAX

500

ГЛАВА 17

В этот момент есть надежда, что информации достаточно,
чтобы провести полный анализ двоичного файла. Если бы
у нас было меньше подсказок о расположении программы в памяти или о разделении данных и кода в файле, то пришлось
бы полагаться на другие источники информации. Перечислим
некоторые возможные подходы к определению расположения
программы в памяти и нахождению кода:
► использовать справочные руководства по процессору, чтобы понять, где находятся векторы сброса;
► поискать в двоичном файле строки, которые могут навести
на мысль об архитектуре, операционной системе или компиляторе, которым была собрана программа;
► поискать типичные последовательности кода, например
прологи функций, характерные для процессора, для которого предназначена программа;
► выполнить статистический анализ частей двоичного файла с целью найти участки, статистически похожие на известные двоичные файлы;
► поискать повторяющиеся последовательности данных,
которые могут быть таблицами адресов (например, во
многих нетривиальных 32-разрядных числах старшие
12 бит одинаковы)1. Это могут быть указатели, которые
дадут полезную информацию о расположении программы в памяти.
Завершая обсуждение загрузки неизвестных двоичных файлов, отметим, что вы должны будете повторить все рассмотренные в этом разделе шаги при каждом открытии файла одного
и того же формата, который так и остается неизвестным Ghidra.
По ходу дела вы можете автоматизировать некоторые действия, написав скрипты, которые разбирают заголовки и создают сегменты. Но именно в этом и заключается задача модуля
загрузчика Ghidra! В следующем разделе мы напишем простой
модуль загрузчика, чтобы познакомиться с архитектурой за1

В тривиальных числах очень мало значащих битов; к ним относятся –1, 0
и другие небольшие целые числа. В интересных числах значащих битов
обычно много, порядка разрядности архитектуры, поэтому результаты поиска по ним, вероятно, будут более релевантны.
Загрузчики Ghidra

501

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

ПРИМЕР 1: МОДУЛЬ ЗАГРУЗЧИКА
SIMPLESHELLCODE
В начале этой главы мы пытались загрузить в Ghidra файл,
содержащий шелл-код, что привело нас к загрузчику Raw
Binary. В главе 15 мы воспользовались комбинацией Eclipse
и GhidraDev, чтобы создать модуль анализатора, а затем добавили его в Ghidra как расширение. Напомним, что в качестве
одного из вариантов модулей Ghidra предлагала нам создать
модуль загрузчика. В этой главе мы разработаем простой модуль загрузчика в качестве расширения Ghidra для загрузки
шелл-кода. Как и в главе 15, упростим процесс разработки, поскольку это всего лишь демонстрационный проект. Наш процесс будет состоять из следующих шагов:
1)
2)
3)
4)
5)

поставить задачу;
создать модуль в Eclipse;
разработать загрузчик;
добавить загрузчик в Ghidra;
протестировать загрузчик в Ghidra.

Что такое шелл-код, и почему нам
это интересно?
Строго говоря, шелл-код – это чистый (raw) машинный код, единственное назначение которого состоит в том, чтобы запустить
процесс оболочки (англ. shell) от имени пользователя (например, /bin/sh). Чаще всего это делается путем прямого взаимодействия с ядром операционной системы посредством системных
вызовов. Использование системных вызовов устраняет зависимости от библиотек, работающих в адресном пространстве пользователя, например libc. Слово raw в этом контексте не следует
путать с тем же словом в контексте загрузчика Raw Binary. Под
чистым машинным кодом понимается код без всякой внешней
упаковки в виде заголовков файла; он компактнее откомпилированного исполняемого кода, выполняющего те же действия.

502

ГЛАВА 17

Компактный шелл-код для процессора x86-64 в Linux может занимать всего 30 байт, тогда как откомпилированная версия следующей программы на C, которая тоже запускает оболочку, «весит»
более 6000 байт даже после зачистки файла:
#include
int main(int argc, char **argv, char **envp) {
execve("/bin/sh", NULL, NULL);
}
Недостаток шелл-кода в том, что его невозможно выполнить
прямо из командной строки. Обычно он внедряется в сущест­
вующий процесс, и производятся определенные действия, чтобы передать ему управление. Противник может попытаться
поместить шелл-код в память процесса, замаскировав его под
обычные входные данные, а затем воспользоваться уязвимостью в программе, чтобы перенаправить поток выполнения на
внедренный шелл-код. Поскольку шелл-код часто поступает под
видом входных данных процесса, его можно наблюдать в сетевом трафике, адресованном уязвимому серверному процессу,
или в файле, который должно открыть уязвимое приложение,
обрабатывающее такие файлы.
Со временем термином шелл-код стали обозначать любой чис­
тый машинный код, включенный в эксплойт, даже если его выполнение не приводит к запуску оболочки в атакуемой системе.

Шаг 0: шаг назад
Еще перед тем как ставить задачу, мы должны понять (a) что
сейчас Ghidra делает с файлом, содержащим шелл-код, (b) чего
мы хотим от Ghidra в этом случае. Вообще-то, мы хотели бы загрузить и проанализировать файл с шелл-кодом как неформатированный двоичный код, а затем использовать полученную
информацию при разработке загрузчика шелл-кода (и, быть
может, его анализатора). По счастью, по сложности шелл-код
в большинстве случаев даже близко не стоит с PE-файлом. Так
что сделаем глубокий вдох и погрузимся в мир шелл-кодов.
Начнем с анализа файла шелл-кода, который мы пытались
загрузить в начале этой главы. В качестве единственного варианта нам был предложен загрузчик Raw Binary, как покаЗагрузчики Ghidra

503

зано на рис. 17.1. Рекомендаций относительно языка не было,
т. к. загрузчик Raw Binary просто получил наш файл «по наследству», потому что все остальные от него отказались. Давайте выберем сравнительно распространенную спецификацию языка и процессора x86:LE:32:default:gcc, показанную
на рис. 17.10.

Рис. 17.10. Диалоговое окно импорта со спецификацией языка
и процессора
Нажав OK, мы получим окно сводки результатов импорта,
показанное на рис. 17.11.

Рис. 17.11. Сводка результатов экспорта для файла шелл-кода
Из увеличенного блока в сводке мы знаем, что в файле
имеется всего 78 байт в одном блоке памяти, и это, по сути
504

ГЛАВА 17

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

Рис. 17.12. Окно браузера кода после загрузки (или анализа)
файла шелл-кода
Щелкните правой кнопкой мыши по первому адресу в файле
и выберите из контекстного меню команду Disassemble (или
нажмите клавишу D). Теперь в окне листинга мы увидим чтото, с чем можно работать, – список команд! В листинге 17.2 показаны команды после дизассемблирования и нашего анализа
файла. Концевые комментарии документируют результаты
анализа этого короткого файла.
Загрузчики Ghidra

505

0000002b INC EBX
0000002c MOV AL,0x66 ; 0x66 – вызов sys_socket в Linux
0000002e INT 0x80 ; передает управление ядру для
; выполнения системного вызова
00000030 XCHG EAX,EBX
00000031 POP ECX
LAB_00000032 XREF[1]: 00000038(j)
00000032 PUSH 0x3f ; 0x3f – вызов sys_dup2 в Linux
00000034 POP EAX
00000035 INT 0x80 ; передает управление ядру для
; выполнения системного вызова
00000037 DEC ECX
00000038 JNS LAB_00000
0000003a PUSH 0x68732f2f ; 0x68732f2f - это "//sh"
0000003f PUSH 0x6e69622f ; 0x6e69622f - это "/bin"
00000044 MOV EBX,ESP
00000046 PUSH EAX
00000047 PUSH EBX
00000048 MOV ECX,ESP
0000004a MOV AL,0xb ; 0xb – вызов sys_execve в Linux,
; выполняет указанную программу
0000004c INT 0x80 ; передает управление ядру для
; выполнения системного вызова

Листинг 17.2. Дизассемблированный 32-разрядный шелл-код
для Linux
Из нашего анализа следует, что шелл-код обращается к системному вызову Linux execve (по адресу 0000004c), чтобы запус­
тить программу /bin/sh (имя которой было помещено в стек
командами по адресам 0000003a и 000003f). Тот факт, что это
вполне осмысленный код, означает, что мы, скорее всего, на
правильном пути.
Теперь мы знаем достаточно о процессе загрузки, чтобы
определить свой загрузчик. (Нам также хватило бы информации для разработки простого анализатора шелл-кода, но эту
задачу мы отложим на потом.)

Шаг 1: поставить задачу
Мы хотим спроектировать и реализовать простой загрузчик, который будет загружать шелл-код в окно листинга и устанавливать точку входа, что упростит автоматический анализ. Загруз506

ГЛАВА 17

чик нужно будет добавить в Ghidra и сделать доступным. Кроме
того, он должен правильно отвечать на запросы импортера
Ghidra – так же, как это делает загрузчик Raw Binary. Это означает, что наш загрузчик тоже будет согласен на все. Попутно отметим, что во всех примерах используется FlatProgramAPI. Хотя
FlatProgramAPI в общем случае не применяется для построе­ния
расширений, в данном случае это решение позволяет закрепить
идеи, представленные в главе 14, которые, вероятно, окажутся
вам полезны при разработке скриптов Ghidra на Java.

Шаг 2: создать модуль в Eclipse
Как было описано в главе 15, воспользуемся командой
GhidraDev4NewGhidra Module Project, чтобы создать
модуль SimpleShellcode по шаблону модуля загрузчика. В результате будет создан файл SimpleShellcodeLoader.java в папке src/main/java внутри модуля SimpleShellcode. Иерархия
этой папки показана на рис. 17.13.

Рис. 17.13. Иерархия модуля SimpleShellcode

Шаг 3: разработать загрузчик
На рис. 17.14 показана часть шаблона загрузчика
SimpleShellcodeLoader.java. Функции свернуты, чтобы были
видны все методы, предоставляемые шаблоном. Напомним,
что Eclipse будет рекомендовать импорт, если какой-то пакет
потребуется по ходу разработки, поэтому можете сразу приступать к кодированию и принимать рекомендации Eclipse.

Загрузчики Ghidra

507

Рис. 17.14. Шаблон SimpleShellcodeLoader
В шаблоне загрузчика на рис. 17.14 имеется шесть тегов задач
в левом поле, которые подсказывают, с чего начать разработку.
Мы будем раскрывать секции по мере разработки и включим
код, относящийся к каждой задаче, чтобы вы поняли, как модифицировать шаблон. (Иногда сгенерированный код переносится на следующую строку или переформатируется для удобства
восприятия, а комментарии сворачиваются для экономии места
на странице.) В отличие от модуля анализатора, написанного
в главе 15, этот модуль не нуждается в очевидных переменныхчленах класса, поэтому можно сразу переходить к задачам.

Шаг 3-1: документировать класс
Раскрыв тег первой задачи, мы увидим следующее описание:
/**
* TODO: Provide class-level documentation that describes what this
* loader does.
*/

В этой задаче требуется заменить существующий комментарий TODO описанием назначения загрузчика:
/*
* Этот загрузчик загружает двоичный шелл-код в Ghidra
* и устанавливает точку входа
*/

508

ГЛАВА 17

Шаг 3-2: назвать и описать загрузчик
Раскрыв следующий тег, мы увидим комментарий TODO и строку, подлежащую редактированию. Из нее легко понять, с чего
начинать.
public String getName() {
// TODO: Name the loader. This name must match the name
// of the loader in the .opinion files
return "My loader";
}

Замените строку  осмысленным именем. О совпадении
с именем в .opinion-файлах не думайте, потому что они не относятся к загрузчикам, принимающим любой файл. С .opinionфайлами мы столкнемся в третьем примере. Игнорируя комментарий насчет .opinion-файлов, получаем такой код:
public String getName() {
return "Simple Shellcode Loader";
}

Шаг 3-3: определить, может ли загрузчик загрузить данный файл
Второй шаг процесса загрузки, описанный в начале главы, касается опроса со стороны импортера. Требуется решить, может
ли загрузчик загрузить файл, и дать импортеру ответ в виде
возвращаемого методом значения.
public Collection findSupportedLoadSpecs(ByteProvider provider)
throws IOException {
List loadSpecs = new ArrayList();
// TODO: проверить байты, которые поставляет 'provider', и решить,
может ли
// данный загрузчик загрузить их. Если может, вернуть соответствующие
// спецификации.
return loadSpecs;
}

Загрузчики Ghidra

509

Обычно загрузчики решают эту задачу, просматривая содержимое файла в поисках магического числа или структуры заголовка. Параметр ByteProvider – это предоставляемая
Ghidra обертка, позволяющая читать (но не записывать) входной поток байтов из файла. Мы упростим себе задачу и позаимствуем список LoadSpec из кода загрузчика Raw Binary,
который игнорирует содержимое файла и просто перечисляет
все возможные спецификации LoadSpec. Кроме того, мы назначим своему загрузчику более низкий приоритет, чем у Raw
Binary; тогда, если существует более специфичный загрузчик,
он автоматически будет иметь более высокий приоритет в диалоговом окне импорта.
public Collection findSupportedLoadSpecs(ByteProvider provider)
throws IOException {
// Список спецификаций загрузки, поддерживаемых данным загрузчиком
List loadSpecs = new ArrayList();
List languageDescriptions =
getLanguageService().getLanguageDescriptions(false);
for (LanguageDescription languageDescription : languageDescriptions) {
Collection compilerSpecDescriptions =
languageDescription.getCompatibleCompilerSpecDescriptions();
for (CompilerSpecDescription compilerSpecDescription :
compilerSpecDescriptions) {
LanguageCompilerSpecPair lcs =
new LanguageCompilerSpecPair(languageDescription.getLanguageID(),
compilerSpecDescription.getCompilerSpecID());
loadSpecs.add(new LoadSpec(this, 0, lcs, false));
}
}
return loadSpecs;
}

С каждым загрузчиком ассоциирован ярус и приоритет внут­
ри яруса. Ghidra определяет четыре яруса загрузчиков, от высокоспециализированных (ярус 0) до безразличных к формату
(ярус 3). Если несколько загрузчиков готовы принять файл, то
Ghidra сортирует их список, предъявляемый пользователю, в порядке возрастания ярусов. Загрузчики, принадлежащие одному
ярусу, сортируются в порядке возрастания приоритетов (т. е. за510

ГЛАВА 17

грузчик с приоритетом 10 находится в списке раньше, чем загрузчик с приоритетом 20, при условии что их ярусы одинаковы).
Например, оба загрузчика PE и Raw Binary готовы загружать PE-файл, но загрузчик PE для этой цели подходит лучше
(его ярус равен 1), поэтому он будет находиться в списке раньше загрузчика Raw Binary (ярус 3, приоритет 100). Мы присвоим загрузчику Simple Shellcode ярус 3 (LoaderTier.UNTARGETED_
LOADER) и приоритет 101, т. е. при заполнении импортером окна
импорта этот загрузчик окажется в самом низу. Для этого добавим в загрузчик два метода:
@Override
public LoaderTier getTier() {
return LoaderTier.UNTARGETED_LOADER;
}
@Override
public int getTierPriority() {
return 101;
}

Шаг 3-4: загрузить байты
Следующий метод (показаны две версии – до и после редактирования) производит основную работу по загрузке содержимого импортируемого файла в проект Ghidra (в данном случае он
загружает шелл-код):
protected void load(ByteProvider provider, LoadSpec loadSpec,
List options, Program program, TaskMonitor monitor,
MessageLog log) throws CancelledException, IOException {
// TODO: Load the bytes from 'provider' into the 'program'.
}

protected void load(ByteProvider provider, LoadSpec loadSpec,
List options, Program program, TaskMonitor monitor,
MessageLog log) throws CancelledException, IOException {
 FlatProgramAPI flatAPI = new FlatProgramAPI(program);
try {
monitor.setMessage("Simple Shellcode: загрузка началась");
// создать блок памяти, в который мы собираемся загрузить шелл-код
Загрузчики Ghidra

511











}


}
}

Address start_addr = flatAPI.toAddr(0x0);
MemoryBlock block = flatAPI.createMemoryBlock("SHELLCODE",
start_addr, provider.readBytes(0, provider.length()), false);
// сделать этот блок памяти допускающим чтение и выполнение, но не запись
block.setRead(true);
block.setWrite(false);
block.setExecute(true);
// установить точку входа в шелл-код равной начальному адресу
flatAPI.addEntryPoint(start_addr);
monitor.setMessage( "Simple Shellcode: загрузка завершилась" );
catch (Exception e) {
e.printStackTrace();
throw new IOException("Не удалось загрузить шелл-код");

Заметим, что, в отличие от скриптов в главах 14 и 15, которые наследуют классу GhidraScript (и в конечном итоге
FlatProgramAPI), наш класс загрузчика не имеет прямого доступа к Flat API. Поэтому, чтобы упростить доступ к некоторым часто используемым классам API, мы сами создаем объект
FlatProgramAPI . Затем мы создаем блок памяти MemoryBlock
с именем SHELLCODE по нулевому адресу  и заполняем его содержимым входного файла. Мы задаем разумные разрешения
 для этой области памяти, а затем устанавливаем точку входа
, сообщая Ghidra, откуда начинать дизассемблирование.
Добавление точки входа – очень важный шаг загрузчика.
Именно благодаря наличию точек входа Ghidra находит адреса,
где заведомо располагается код (а не данные). Загрузчик, который так или иначе разбирает входной файл, – идеальное место
для обнаружения точек входа и информирования о них Ghidra.

Шаг 3-5: регистрация параметров загрузчика
Некоторые загрузчики предлагают пользователям возможность
изменить различные параметры процесса загрузки. Мы можем переопределить метод getDefaultOptions, чтобы передать
Ghidra список параметров, доступных нашему загрузчику:

512

ГЛАВА 17

public List getDefaultOptions(ByteProvider provider, LoadSpec loadSpec,
DomainObject domainObject, boolean isLoadIntoProgram) {
List list = super.getDefaultOptions(provider, loadSpec, domainObject,
isLoadIntoProgram);
// TODO: если у загрузчика есть параметры, добавить их в список ‘list’
list.add(new Option(«Здесь должно быть имя параметра»,

Здесь должно быть значение параметра по умолчанию));
return list;
}

Поскольку этот загрузчик предназначен только для демонстрации, мы не будем добавлять никаких параметров. Но,
в принципе, можно было бы задать смещения от начала файла, с которого начинать чтение, и установить базовый адрес,
с которого загружать файл. Чтобы просмотреть параметры любого загрузчика, нажмите кнопку Options … в правом нижнем
углу диалогового окна импорта (см. рис. 17.1).
public List getDefaultOptions(ByteProvider provider, LoadSpec loadSpec,
DomainObject domainObject, boolean isLoadIntoProgram)
{
// параметров нет
List list = new ArrayList();
return list;
}

Шаг 3-6: проверить правильность параметров
Следующий шаг – проверить правильность параметров:
public String validateOptions(ByteProvider provider, LoadSpec loadSpec,
List options, Program program) {
// TODO: если у этого загрузчика есть параметры, здесь нужно проверить их
// правильность. Не все параметры нуждаются в проверке.
return super.validateOptions(provider, loadSpec, options, program);
}

Поскольку у нашего загрузчика параметров нет, просто возвращаем null:

Загрузчики Ghidra

513

public String validateOptions(ByteProvider provider, LoadSpec loadSpec,
List options, Program program) {
// Параметров нет, нечего проверять
return null;
}

Тестирование модулей в ECLIPSE
Если вы из тех программистов, кто не всегда пишет код правильно с первой попытки, то можете избежать нескольких циклов
«экспортировать, запустить Ghidra, импортировать расширение,
добавить расширение в список импорта, выбрать расширение,
перезапустить Ghidra, протестировать расширение», если будете
запускать новый код из Eclipse. После выбора команды RunRun
As из меню Eclipse вам будет предложено выполнить как Ghidra
(или как Ghidra Headless). При этом будет запущена Ghidra, и вы
сможете импортировать файл в текущий проект. Ваш загрузчик
будет включен как один из вариантов импорта, а все сообщения
будут выводиться на консоль Eclipse. С файлом можно будет взаимодействовать в Ghidra, как с любым другим файлом. Потом
вы сможете выйти из проекта Ghidra без сохранения и либо (1)
подправить код, либо (2) «экспортировать, запустить Ghidra, импортировать расширение, добавить расширение в список импорта, выбрать расширение, перезапустить Ghidra, протес­тировать
расширение» только один раз.

Шаг 4: добавить загрузчик в Ghidra
Убедившись, что модуль работает правильно, экспортируйте его
из Eclipse, а затем установите расширение в Ghidra точно так же,
как мы поступали с модулем SimpleROPAnalyzer в главе 15. Для
экспорта выберите из меню пункт GhidraDevExportGhidra
Module Extension, затем выберите модуль SimpleShellcode
и повторите те же действия, что в главе 15.
Для импорта расширения в Ghidra выберите команду
FileInstall Extensions в окне проекта. Добавьте новый загрузчик в список и выберите его. После перезапуска Ghidra
новый загрузчик должен появиться в списке, но на всякий случай надо проверить.
514

ГЛАВА 17

Шаг 5: протестировать загрузчик
в Ghidra
Наш упрощенный план тестирования призван всего лишь продемонстрировать функциональность. SimpleShellcode проходит
приемочный тест, а точнее отвечает следующим критериям.
1. (Проходит) SimpleShellcode входит в список загрузчиков
и имеет в нем меньший приоритет, чем Raw Binary.
2. (Проходит) SimpleShellcode загружает файл и устанавливает
точку входа.
То, что тест 1 проходит, видно по рис. 17.15. Второе подтверждение показано на рис. 17.16, где загружается PE-файл,
проанализированный выше в этой главе. В обоих случаях мы
видим, что загрузчик Simple Shellcode Loader является последним в списке Format.

Рис. 17.15. Окно импорта: в списке присутствует новый
загрузчик

Загрузчики Ghidra

515

Рис. 17.16. Окно импорта: новый загрузчик присутствует
в списке для PE-файла
Выбирайте спецификацию языка в зависимости от имеющейся информации о двоичном файле и от способа его получения.
Предположим, что шелл-код был извлечен из сетевых пакетов,
адресованных компьютеру с процессором x86. В таком случае
в качестве отправной точки, наверное, лучше всего будет выбрать
спецификацию языка и компилятора x86:LE:32:default:gcc.
После выбора языка и нажатия кнопки OK в окне на
рис. 17.15 двоичный файл импортируется в проект. Затем можно открыть программу в браузере кода, и Ghidra предложит
проанализировать файл. Если мы примем это предложение, то
увидим следующий листинг:
undefined FUN_00000000()
undefined AL:1
undefined4 Stack[-0x10]:4 local_10 XREF[1]: 00000022(W)
FUN_00000000 XREF[1]: Entry Point(*)
00000000 31 db XOR EBX,EBX
00000002 f7 e3 MUL EBX
00000004 53 PUSH EBX
00000005 43 INC EBX
00000006 53 PUSH EBX
00000007 6a 02 PUSH 0x2
00000009 89 e1 MOV ECX,ESP
0000000b b0 66 MOV AL,0x66

516

ГЛАВА 17

0000000d
0000000f
00000010
00000011
00000012

cd 80
5b
5e
52
68 02 00 11

INT 0x80
POP EBX
POP ESI
PUSH EDX
5c PUSH 0x5c110002

Точка входа  идентифицирована, поэтому Ghidra показывает дизассемблированный код, с которого можно начать анализ.
Загрузчик SimpleShellcodeLoader – тривиальный пример,
потому что шелл-код обычно встраивается в состав других данных. Для демонстрации мы воспользуемся нашим модулем как
основой для создания модуля загрузчика, который извлекает
шелл-код из исходных файлов на C и загружает его для анализа. Это, например, позволит нам строить сигнатуры шелл-кода,
которые Ghidra сможет распознать в других двоичных файлах.
Мы не будем расписывать каждый шаг во всех подробностях,
потому что всего лишь дополняем возможности уже существующего загрузчика шелл-кода.

ПРИМЕР 2: ПРОСТОЙ ЗАГРУЗЧИК
ШЕЛЛ-КОДА ИЗ ИСХОДНЫХ
ФАЙЛОВ
Поскольку модули – это способ организации кода, а созданный нами модуль SimpleShellcode обладает всем необходимым для создания загрузчика, нам нет нужды создавать
новый модуль. Просто выберите из меню Eclipse команду
FileNewFile (ФайлСоздатьФайл) и добавьте новый
файл (SimpleShellcodeSourceLoader.java) в папку src/main/
java модуля SimpleShellcode. В результате все ваши новые загрузчики будут включены в расширение Ghidra.
Чтобы упростить себе жизнь, скопируйте содержимое сущест­
вующего файла SimpleShellcodeLoader.java в новый файл и исправьте комментарии, в которых написано, что делает загрузчик. При описании следующих шагов мы останавливаемся на
тех частях существующего загрузчика, которые нужно изменить. В основном мы будем дописывать новый код.

Загрузчики Ghidra

517

Обновление 1: изменить ответ на опрос
импортера
Простой загрузчик исходного кода принимает решения исключительно на основе расширения файла. Если имя файла не оканчивается на .c, то загрузчик вернет пустой список
loadSpecs. А если оканчивается, то тот же список, что и предыдущий загрузчик. Для реализации этой идеи добавьте в метод
findSupportLoadSpecs следующую проверку:
// Список спецификаций загрузки, поддерживаемых этим загрузчиком
List loadSpecs = new ArrayList();
// Активировать загрузчик, если имя файла оканчивается на .c
if (!provider.getName().endsWith(".c")) {
return loadSpecs;
}

Мы также решили, что наш загрузчик заслуживает более
высокого приоритета, чем Raw Binary, т. к. готов принимать
только файлы определенного типа и для них работает лучше.
Поэтому мы вернем более высокий приоритет (меньшее значение) из метода getTierPriority:
public int getTierPriority() {
// приоритет этого загрузчика
return 99;
}

Обновление 2: найти шелл-код в исходном
коде
Напомним, что шелл-код – это чистый машинный код, который
делает нечто полезное. Отдельные байты шелл-кода лежат
в диапазоне от 0 до 255, и многие из них не являются символами ASCII, имеющими графическое представление. Поэтому
шелл-код, встроенный в исходный файл, скорее всего, будет
представлен управляющими последовательностями вида \xFF.

518

ГЛАВА 17

Строки такого вида хорошо различимы, и можно написать регулярное выражение, которое будет их выделять. Следующая
переменная экземпляра описывает регулярное выражение, которым могут пользоваться все функции нашего загрузчика для
поиска байтов шелл-кода в C-файле:
private String pattern = "\\\\x[0-9a-fA-F]{1,2}";

В методе load загрузчик ищет в файле строки, соответствующие регулярному выражению, чтобы можно было вычислить
объем памяти, необходимый Ghidra при загрузке файла. Поскольку шелл-код часто не является непрерывным, загрузчик
должен просмотреть файл целиком и найти в нем отдельные
участки шелл-кода.
// инициализировать сопоставитель с регулярным выражением
CharSequence provider_char_seq =
new String(provider.readBytes(0, provider.length()), "UTF-8");
Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(provider_char_seq);
// Подсчитать, сколько было найдено соответствий (байтов шелл-кода),
чтобы
// выделить достаточно памяти, а затем сбросить сопоставитель
int match_count = 0;
while (m.find()) {
 match_count++;
}
m.reset();

После загрузки всего содержимого входного файла  мы
подсчитываем количество соответствий  регулярному выражению .

Обновление 3: преобразовать шелл-код
в байтовые значения
Далее метод load() должен преобразовать шестнадцатеричные управляющие последовательности в байтовые значения
и поместить их в массив байтов:

Загрузчики Ghidra

519

byte[] shellcode = new byte[match_count];
// преобразовать шестнадцатеричное представление байтов в исходном коде
// в фактические байтовые значения в двоичном коде
int ii = 0;
while (m.find()) {
// вырезать \x
String hex_digits = m.group().replaceAll("[^0-9a-fA-F]+", "");
// оставшееся преобразовать в целое число, а затем привести к типу
// byte и записать результат в текущую позицию байтового массива
shellcode[ii++] = (byte)Integer.parseInt(hex_digits, 16);
}

Из каждой подошедшей строки выделяются шестнадцатеричные цифры , преобразуются в байтовые значения , которые помещаются в массив, содержащий шелл-код .

Обновление 4: загрузить
преобразованный байтовый массив
Наконец, поскольку шелл-код находится в байтовом массиве,
метод load() должен скопировать его из этого массива в память
программы. Это и есть собственно загрузка – последний шаг на
пути к достижению загрузчиком поставленной цели:
// создать блок памяти и скопировать в него шелл-код
Address start_addr = flatAPI.toAddr(0x0);
MemoryBlock block =
flatAPI.createMemoryBlock("SHELLCODE", start_addr, shellcode, false);

Результаты
Чтобы протестировать новый загрузчик, создадим исходный
файл на C, содержащий следующее шестнадцатеричное представление шелл-кода для x86:
unsigned char buf[] =
"\x31\xdb\xf7\xe3\x53\x43\x53\x6a\x02\x89\xe1\xb0\x66\xcd\x80"
"\x5b\x5e\x52\x68\x02\x00\x11\x5c\x6a\x10\x51\x50\x89\xe1\x6a"
"\x66\x58\xcd\x80\x89\x41\x04\xb3\x04\xb0\x66\xcd\x80\x43\xb0"
"\x66\xcd\x80\x93\x59\x6a\x3f\x58\xcd\x80\x49\x79\xf8\x68\x2f"
"\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0"
"\x0b\xcd\x80";

520

ГЛАВА 17

Поскольку имя исходного файла оканчивается на .c, наш загрузчик появляется в списке, где занимает первую позицию,
поскольку его приоритет выше, чем у загрузчиков Raw Binary
и Simple Shellcode (см. рис. 17.17).

Рис. 17.17. Диалоговое окно импорта исходного файла,
содержащего шелл-код
Если мы выберем этот загрузчик и ту же самую предлагаемую по умолчанию спецификацию языка и компилятора, что
в предыдущем примере (x86:LE:32:default:gcc), а затем позволим Ghidra автоматически проанализировать файл, то в лис­
тинге дизассемблера увидим следующую функцию:
**************************************************************
* FUNCTION
*
**************************************************************
undefined FUN_00000000()
undefined AL:1
undefined4 Stack[-0x10]:4 local_10
FUN_00000000 XREF[1]: Entry Point(*)
00000000 XOR EBX,EBX
00000002 MUL EBX
00000004 PUSH EBX
00000005 INC EBX
00000006 PUSH EBX

Загрузчики Ghidra

521

Прокрутив листинг вниз, мы увидим уже знакомый код (см.
листинг 17.2), который приводим здесь, добавив для ясности
комментарии:
LAB_00000032
00000032 PUSH 0x3f
00000034 POP EAX
00000035 INT 0x80
00000037 DEC ECX
00000038 JNS LAB_00000
0000003a PUSH 0x68732f2f ; 0x68732f2f преобразуется в "//sh"
0000003f PUSH 0x6e69622f ; 0x6e69622f преобразуется в "/bin"

В фокусе внимания обратной разработки чаще всего находятся двоичные файлы. В данном случае мы отошли от шаблона и использовали Ghidra как для загрузки шелл-кода с целью
анализа, так и для извлечения его из исходных файлов на C.
Нашей целью было продемонстрировать гибкость и простоту
создания загрузчиков для Ghidra. А теперь вернемся к шаб­
лону и создадим загрузчик для файла со структурированным
форматом.
Предположим, что искомый шелл-код находится внутри двоичного файла в формате ELF и что Ghidra не распознает этот
формат (хотя в действительности это, конечно, не так). Предположим еще, что никто из нас слыхом не слыхивал о том, что
такое ELF. Приключение начинается!

Пример 3: простой загрузчик шелл-кода
в формате ELF
Примите поздравления! Вы теперь местный специалист по
обратной разработке шелл-кода, и коллеги сообщают, что заподозрили наличие шелл-кода в двоичных файлах, а Ghidra
отсылает их к загрузчику Raw Binary. Проблема не выглядит
как одноразовая, и вы полагаете, что есть веские причины ожидать появления других двоичных файлов с похожими характеристиками, поэтому решаете создать загрузчик, который будет
обрабатывать файлы такого типа. Как было сказано в главе 13,
для сбора информации о файле можно использовать как внут­
ренние средства Ghidra, так и внешние инструменты. Если вы
522

ГЛАВА 17

снова повернетесь лицом к командной строке, то команда file
предоставит ценную информацию о том, с чего начинать разработкузагрузчика.
$ file elf_shellcode_min
elf_shellcode_min: ELF 32-bit LSB executable, Intel 80386, version 1
(SYSV),
statically linked, corrupted section header size
$

Команда file сообщает о формате, про который вы раньше никогда не слышали, ELF. Первый шаг – поискать любую информацию о двоичных файлах такого типа. Ваш добрый приятель
Google радостно выдает несколько ссылок на формат ELF, по
которым можно найти сведения, необходимые для создания загрузчика. Годится все, что содержит достаточно точную информацию, полезную для решения задачи1.
Поскольку эта задачка посложнее, чем два предыдущих примера, мы представим ее решение в нескольких разделах, ориентируясь на отдельные файлы в модуле, которые предстоит
создать, изменить или удалить по ходу разработки загрузчика
SimpleELFShellcodeLoader. Начнем с простых организационных действий.

Организационные мероприятия
Первым делом нужно создать файл SimpleELFShellcodeLoader.
java в модуле SimpleShellcode в Eclipse. Поскольку мы не хотим
начинать с нуля, сделаем копию файла SimpleShellcodeLoader.
java, сохранив его под новым именем. После этого нужно будет
внести в новый файл несколько мелких изменений, а уже потом заняться задачей как таковой.
► Переименовать класс в SimpleELFShellcodeLoader.
► Изменить значение, возвращаемое методом getTier
с UNTARGETED_LOADER, на GENERIC_TARGET_LOADER.
1

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

523

► Удалить метод getTierPriority.
► Изменить метод getName, так чтобы он возвращал "Simple
ELF Shellcode Loader".
Сделав все это, воспользуемся найденной информацией
о формате заголовка.

Формат заголовков ELF
В процессе изысканий вы выяснили, что в формате ELF есть
три типа заголовков: заголовок файла (или заголовок ELF),
заголовки программы и заголовки секций. Начнем с заголовка ELF. С каждым полем в заголовке ELF связано смещение,
а также другая информация о поле. Поскольку нам нужны
лишь немногие поля и мы не собираемся изменять смещения, объявим следующие константы как переменные экземп­
ляра класса загрузчика, это поможет правильно разобрать
заголовок.
private final byte[] ELF_MAGIC = {0x7f, 0x45, 0x4c, 0x46};
private final long EH_MAGIC_OFFSET = 0x00;
private final long EH_MAGIC_LEN = 4;
private final long EH_CLASS_OFFSET = 0x04;
private final byte EH_CLASS_32BIT = 0x01;
private final long EH_DATA_OFFSET = 0x05;
private final byte EH_DATA_LITTLE_ENDIAN = 0x01;
private final long EH_ETYPE_OFFSET = 0x10;
private final long EH_ETYPE_LEN = 0x02;
private final short EH_ETYPE_EXEC = 0x02;
private final long EH_EMACHINE_OFFSET = 0x12;
private final long EH_EMACHINE_LEN = 0x02;
private final short EH_EMACHINE_X86 = 0x03;
private final long EH_EFLAGS_OFFSET = 0x24;
private final long EN_EFLAGS_LEN = 4;
private final long EH_EEHSIZE_OFFSET = 0x28;
private final long EH_PHENTSIZE_OFFSET = 0x2A;
private final long EH_PHNUM_OFFSET = 0x2C;

524

ГЛАВА 17

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

Определение поддерживаемых
спецификаций загрузки
Загрузчик не может загрузить файл в неизвестном ему формате, а чтобы отвергнуть файл, должен вернуть пустой список
loadSpecs. В методе findSupportedLoadSpecs() мы сразу исключаем
двоичные файлы, в которых нет ожидаемого магического числа:
byte[] magic = provider.readBytes(EH_MAGIC_OFFSET, EH_MAGIC_LEN);
if (!Arrays.equals(magic, ELF_MAGIC)) {
// формат двоичного файла не ELF
return loadSpecs;
}

После того как нежеланные файлы исключены, загрузчик
может проверить разрядность и порядок байтов, чтобы узнать,
соответствует ли архитектура формату ELF. Для демонстрации ограничимся только 32-разрядными двоичными файлами
с прямым порядком байтов:
byte ei_class = provider.readByte(EH_CLASS_OFFSET);
byte ei_data = provider.readByte(EH_DATA_OFFSET);
if ((ei_class != EH_CLASS_32BIT) || (ei_data != EH_DATA_LITTLE_ENDIAN)) {
// такой ELF мы принимать не готовы
return loadSpecs;
}

И чтобы завершить проверку, убедимся, что данный ELFфайл исполняемый (а не разделяемая библиотека) и рассчитан на архитектуру процессора x86:
Загрузчики Ghidra

525

byte[] etyp = provider.readBytes(EH_ETYPE_OFFSET, EH_ETYPE_LEN);
short e_type =
ByteBuffer.wrap(etyp).order(ByteOrder.LITTLE_ENDIAN).getShort();
byte[] emach = provider.readBytes(EH_EMACHINE_OFFSET, EH_EMACHINE_LEN);
short e_machine =
ByteBuffer.wrap(emach).order(ByteOrder.LITTLE_ENDIAN).getShort();
if ((e_type != EH_ETYPE_EXEC) || (e_machine != EH_EMACHINE_X86)) {
// такой ELF мы принимать не готовы
return loadSpecs;
}

Ограничив типы файлов, мы можем запросить у экспертной службы (opinion service) спецификации подходящего языка и компилятора. Идея в том, что мы отправляем экспертной
службе значения, извлеченные из загружаемого файла (например, поле e_machine из заголовка ELF), и получаем в ответ список спецификаций языка и компилятора, которые готов принимать наш загрузчик. (Что происходит «за кулисами» экспертной
службы, подробнее описывается в следующих разделах.)
byte[] eflag = provider.readBytes(EH_EFLAGS_OFFSET, EN_EFLAGS_LEN);
int e_flags = ByteBuffer.wrap(eflag).order(ByteOrder.LITTLE_ENDIAN).
getInt();
List results =
QueryOpinionService.query(getName(), Short.toString(e_machine),

Integer.toString(e_flags));

Предположим, что экспертная служба может возвращать
больше результатов, чем мы хотим обрабатывать в загрузчике. Список можно укоротить, исключив некоторые результаты,
исходя из атрибутов в спецификациях языка и компилятора.
Следующий код отфильтровывает некоторые комбинации компилятора и процессора:
for (QueryResult result : results) {
CompilerSpecID cspec = result.pair.getCompilerSpec().
getCompilerSpecID();
if (cspec.toString().equals("borlanddelphi")) {
// игнорировать все, созданное компилятором Delphi
continue;
}

526

ГЛАВА 17

String variant = result.pair.getLanguageDescription().getVariant();
if (variant.equals("System Management Mode")) {
// игнорировать все с вариантом "System Management Mode"
continue;
}
// допустимая спецификация загрузки, добавить ее в список
 loadSpecs.add(new LoadSpec(this, 0, result));
}
return loadSpecs;

В этом примере (который вы можете включить в свой загрузчик) мы явно исключаем компилятор Delphi  и режим
управления системой x86. При желании можете исключить
еще что-нибудь. Все спецификации, которые вы решили оставить, нужно добавить в список loadSpecs.

Загрузить содержимое файла в Ghidra
Метод load() нашего упрощенного загрузчика предполагает,
что файл состоит из минимального заголовка ELF и короткого
заголовка программы, за которым следует шелл-код в секции
.text. Нам нужно определить полную длину заголовка, чтобы
выделить для него достаточно памяти. В следующем коде для
вычисления размера используются поля EH_EEHSIZE_OFFSET,
EH_PHENTSIZE_OFFSET и EH_PHNUM_OFFSET из заголовка ELF:
// Взять из заголовка некоторые поля, необходимые для процесса загрузки
//
// Каков размер заголовка ELF?
byte[] ehsz = provider.readBytes(EH_EEHSIZE_OFFSET, 2);
e_ehsize = ByteBuffer.wrap(ehsz).order(ByteOrder.LITTLE_ENDIAN).
getShort();
// Каков размер одного заголовка программы?
byte[] phsz = provider.readBytes(EH_PHENTSIZE_OFFSET, 2);
e_phentsize =
ByteBuffer.wrap(phsz).order(ByteOrder.LITTLE_ENDIAN).getShort();
// Сколько всего имеется заголовков программы?
byte[] phnum = provider.readBytes(EH_PHNUM_OFFSET, 2);
e_phnum = ByteBuffer.wrap(phunm).order(ByteOrder.LITTLE_ENDIAN).
getShort();

Загрузчики Ghidra

527

// Каков полный размер заголовка в нашем упрощенном формате ELF?
// (включается заголовок ELF и заголовки программы.)
long hdr_size = e_ehsize + e_phentsize * e_phnum;

Теперь, зная размер, можно создать и заполнить блоки памяти для секции заголовка ELF и секции .text:
// Создать блок памяти для заголовка ELF
long LOAD_BASE = 0x10000000;
Address hdr_start_adr = flatAPI.toAddr(LOAD_BASE);
MemoryBlock hdr_block =
flatAPI.createMemoryBlock(".elf_header", hdr_start_adr,

provider.readBytes(0, hdr_size), false);
// Сделать этот блок доступным только для чтения
hdr_block.setRead(true);
hdr_block.setWrite(false);
hdr_block.setExecute(false);
// Создать блок памяти для секции .text из упрощенного ELF-файла
Address txt_start_adr = flatAPI.toAddr(LOAD_BASE + hdr_size);
MemoryBlock txt_block =
flatAPI.createMemoryBlock(".text", txt_start_adr,
provider.readBytes(hdr_size, provider.length() – hdr_size),
false);
// Сделать этот блок памяти доступным для чтения и выполнения
txt_block.setRead(true);
txt_block.setWrite(false);
txt_block.setExecute(true);

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

528

ГЛАВА 17







// Наложить структуру на заголовок ELF
flatAPI.createData(hdr_start_adr, new ElfDataType());
// Добавить метку и точку входа в начало шелл-кода
flatAPI.createLabel(txt_start_adr, "shellcode", true);
flatAPI.addEntryPoint(txt_start_adr);

// Добавить перекрестную ссылку с заголовка ELF на точку входа
Data d = flatAPI.getDataAt(hdr_start_adr).getComponent(0).
getComponent(9);
 flatAPI.createMemoryReference(d, txt_start_adr, RefType.DATA);

Во-первых, тип данных заголовка ELF, включенный в Ghidra,
накладывается на начало заголовков EL1. Во-вторых, для
шелл-кода создаются метка  и точка входа . И наконец, мы
создаем перекрестную ссылку между полем точки входа в заголовке ELF и началом шелл-кода .
Поздравляем! Вы написали Java-код загрузчика, но нужно
прояснить два вопроса, чтобы вы хорошо понимали зависимости между новым загрузчиком и некоторыми важными файлами, – без этого загрузчик не будет работать как положено.
В этом примере предполагалась существующая архитектура
процессора (x86), и за кулисами была проделана определенная работа, чтобы загрузчик работал правильно. Напомним,
что импортер опрашивал загрузчики и волшебным образом
порождал спецификации языка и компилятора. В следующих
двух файлах хранится информация, критически важная для
загрузчика. Первый, x86.ldefs, содержит определения языка
x86 и является частью процессорного модуля x86.

Файлы определений языков
С каждым процессором связан файл определения языка. Этот
файл в формате XML включает всю информацию, необходимую
для генерирования спецификаций языка и компилятора для
данного процессора. В следующем листинге приведены взятые
из файла x86.ldefs определения, соответствующие 32-разрядному двоичному файлу в формате ELF:
1

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

529


Intel/AMD 32-bit x86






Intel/AMD 32-bit x86 System Management Mode



Этот файл используется, для того чтобы сгенерировать специ­
фикации языка и компилятора, представляемые в виде вариантов импорта. В данном случае имеется пять рекомендуемых
спецификаций (каждая начинается тегом compiler), которые
будут возвращены, основываясь на информации о двоичном
ELF-файле, но наш загрузчик исключает две из них, относящиеся к конкретному компилятору  и варианту .

Opinion-файлы
Еще один тип вспомогательных файлов – opinion-файлы. Этот
файл в формате XML содержит ограничения, ассоциированные с загрузчиком. Чтобы загрузчик распознавался экспертной службой, для него должна быть запись в opinion-файле.
В листинге ниже приведена такая запись для только что созданного загрузчика:
530

ГЛАВА 17








В этой записи знакомо все, кроме, быть может, поля primary .
Это поле содержит первичный ключ поиска, который идентифицирует компьютер так же, как в заголовке ELF. В заголовке ELF
значение 0x03 в поле e_machine означает процессор x86, а 0x3E
в том же поле – процессор amd64. Тег  определяет
связь между первичным ключом ("3"/x86) и остальными атрибутами. Эта информация используется экспертной службой для
поиска соответствующих записей в файлах определений языков.
Нам осталось только поместить экспертные данные в место,
где Ghidra сможет их найти. Все opinion-файлы, входящие
в комплект поставки Ghidra, находятся в подкаталоге data/
languages процессорного модуля Ghidra. Мы, конечно, могли
бы вставить свои экспертные данные в существующий opinionфайл, но лучше не вносить никаких изменений в поставляемые файлы, потому что придется повторять этот процесс после
каждого перехода на новую версию Ghidra.
Вместо этого создадим новый opinion-файл, содержащий
наши экспертные данные. Назвать его можно как угодно, но
имя SimpleShellcode.opinion выглядит особенно уместным.
Наш шаблон модуля загрузчика в Eclipse содержит свой подкаталог data. Сохраните в нем opinion-файл, чтобы ассоциировать его со своим модулем загрузчика. Ghidra найдет его, когда
будет искать opinion-файлы, и никакое обновление Ghidra на
ваш файл не повлияет.
Теперь, когда вы понимаете, что происходит за кулисами,
настало время протестировать загрузчик и посмотреть, отвечает ли он нашим ожиданиям.

Загрузчики Ghidra

531

Результаты
Чтобы продемонстрировать работоспособность нового упрощенного загрузчика ELF (с одним заголовком программы и без
секций), выполним процесс загрузки и посмотрим, как ведет
себя загрузчик на каждом шаге.
В окне проекта импортируйте файл. Импортер опросит все
загрузчики Ghidra, включая и наш, чтобы понять, какие из них
готовы загрузить данный файл. Напомним, что наш загрузчик
ожидает, что файл обладает следующими свойствами:
► магическое число ELF в начале файла;
► 32-разрядный с прямым порядком байтов;
► исполняемый ELF-файл для архитектуры x86;
► не был создан компилятором Delphi;
► вариант не совпадает с «System Management Mode».
Если вы загружаете файл с такими свойствами, то диалоговое окно импорта должно выглядеть, как на рис. 17.18, где
показан отсортированный по приоритетам список загрузчиков,
готовых обработать файл.

Рис. 17.18. Параметры импорта для файла elf_shellcode_min
Наивысший приоритет у загрузчика ELF, встроенного
в Ghidra. Сравним спецификации языка и компилятора, которые он готов принять (рис. 17.19 вверху), с теми, что готов
принять наш загрузчик (там же внизу).
532

ГЛАВА 17

Рис. 17.19. Спецификации языка и компилятора для двух разных
загрузчиков
Компилятор Delphi и вариант System Management Mode
принимаются включенным в дистрибутив загрузчиком ELF,
но не принимаются нашим. Выбрав свой загрузчик для файла
elf_shellcode_min, вы увидите сводку, показанную на рис. 17.20.

Рис. 17.20. Окно сводки результатов импорта для нового
загрузчика шелл-кода в формате ELF
Загрузчики Ghidra

533

Открыв файл в браузере кода и разрешив Ghidra автоматически проанализировать его, вы должны увидеть следующее
определение заголовка ELF в начале файла:
10000000
10000001
10000004
10000005
10000006
10000007
10000010
10000012
10000014
10000018
1000001c
10000020
10000024
10000028

7f db
45 4c
01 db
01 db
01 db
00 00
00 00
02 00
03 00
01 00
54 00
34 00
00 00
00 00
34 00

7Fh e_ident_magic_num
46 ds "ELF" e_ident_magic_str
1h e_ident_class
1h e_ident_data
1h e_ident_version
00 00 00 db[9] e_ident_pad
00 00
dw 2h e_type
dw 3h e_machine
00 00 ddw 1h e_version
00 10 ddw shellcode e_entry
00 00 ddw 34h e_phoff
00 00 ddw 0h e_shoff
00 00 ddw 0h e_flags
dw 34h e_ehsize

В этом листинге метка shellcode , очевидно, связана с точкой входа. Двойной щелчок по ней ведет на функцию с именем
shellcode, которая содержит тот же самый шелл-код, который
мы уже видели в двух предыдущих примерах:
1000008c
1000008e
10000093
10000098
1000009a

JNS
PUSH
PUSH
MOV
PUSH

LAB_10000086
"//sh"
"/bin"
EBX,ESP
EAX

Убедившись, что новый загрузчик работает, вы можете добавить его в Ghidra в качестве расширения и поделиться с коллегами, которые с нетерпением ждут этой функциональности.

534

ГЛАВА 17

РЕЗЮМЕ
В этой главе мы обсуждали проблемы, возникающие в связи
с нераспознанными двоичными файлами. Мы проштудировали несколько примеров загрузки и анализа файлов, которые
могут выручить нас в таких трудных случаях обратной разработки. Наконец, мы расширили свои навыки, научившись создавать загрузчики Ghidra.
Разработанные нами загрузчики были тривиальны, но тем
не менее на этих примерах мы познакомились со всеми компонентами, необходимыми для написания более сложных модулей загрузчиков. В следующей главе мы завершим обсуждение
модулей Ghidra, рассмотрев процессорные модули – компоненты, на которые ложится основная ответственность за форматирование листинга дизассемблера.

18

ПРОЦЕССОРНЫЕ МОДУЛИ
В GHIDRA

Процессорные модули, самые сложные из всех
типов модулей, отвечают за все операции диз­
ассемблирования, выполняемые Ghidra. Помимо решения очевидной задачи – преобразования
машинных кодов операций в эквиваленты на языке
ассемблера, процессорные модули также поддерживают создание функций, перекрестных ссылок и кадров стека.
Хотя количество процессоров, поддерживаемых Ghidra, впечатляет и увеличивается с выходом каждой новой версии, иног­
да требуется разработать новый процессорный модуль. Ясно,
что такая необходимость возникает, когда обратной разработке подвергается двоичный файл, рассчитанный на исполнение
процессором, для которого в Ghidra нет модуля. Но есть другие ситуации, например когда двоичный файл содержит прошивку встраиваемого микроконтроллера или исполняемый
образ, снятый с портативного устройства или устройства для
интернета вещей (IoT). Не столь очевидное применение процессорного модуля – дизассемблирование команд нестандарт-

ной виртуальной машины, встроенной в обфусцированный
исполняемый файл для x86. В таких случаях существующий
процессорный модуль x86 поможет разобраться только в самой
виртуальной машине, но не в исполняемом ей байтовом коде.
На случай если вы возьметесь за эту непростую задачу, мы
хотим снабдить вас надежной опорой, которая поможет совершить подвиг. Во всех предыдущих примерах модулей (анализатора и загрузчика) нужно было модифицировать только один
Java-файл. Если бы мы создавали эти модули в среде Eclipse
GhidraDev, шаблон модуля и содержащиеся в нем теги помогли бы справиться с задачей. Процессорные модули сложнее,
и, если мы хотим, чтобы модуль работал правильно, придется
иметь в виду связи между различными файлами. Мы не станем
в этой главе создавать процессорный модуль с чистого листа, но
заложим прочный фундамент, чтобы вы поняли, как устроены
такие модули, и продемонстрируем создание и модификацию
их компонентов.

Кто дополняет Ghidra?
На основе не вполне научного исследования мы сильно подозреваем, что имеются следующие категории:
Категория 1. Небольшой процент пользователей Ghidra модифицирует существующие или пишет новые скрипты, чтобы настроить под себя или автоматизировать некоторую относящуюся
к Ghidra функциональность.
Категория 2. Из тех, кто входит в категорию 1, небольшой процент предпочитает модифицировать или разработать плагин для
настройки некоторой функциональности Ghidra.
Категория 3. Из тех, кто входит в категорию 2, еще меньший процент готов модифицировать или написать новый анализатор для
расширения возможностей Ghidra.
Категория 4. Из тех, кто входит в категорию 3, немногие пользователи готовы модифицировать или написать загрузчик для нового формата файла.
Категория 5. Очень небольшая часть людей из категории 4 выбирают модификацию или написание процессорного модуля Ghidra,
потому что количество систем команд, нуждающихся в декодировании, гораздо меньше, чем количество форматов файлов, в ко-

538

ГЛАВА 18

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

ЗНАКОМСТВО С ПРОЦЕССОРНЫМ
МОДУЛЕМ GHIDRA
Создание процессорного модуля для реальной архитектуры –
узкоспециальное и весьма трудоемкое дело, далеко выходящее
за рамки этой книги. Но базовые знания о том, как процессоры
и их системы команд представлены в Ghidra, поможет вам понять, куда смотреть, чтобы все было наготове в тот момент, когда информация о процессорном модуле вам таки понадобится.

Процессорные модули в Eclipse
Начнем с уже знакомой территории. Когда для создания процессорного модуля используется меню EclipseGhidraDev,
создается по сути такая же структура папок, что и для модуля любого другого типа (рис. 18.1), но в папке src/main/java
нет исходного файла на Java, содержащего комментарии, теги
и список TODO.

Процессорные модули в Ghidra

539

Рис. 18.1. Состав процессорного модуля
Вместо этого папка data (на рисунке раскрыта) содержит гораздо больше, чем краткий файл README.txt, находящийся в этой
папке для модулей других типов. Кратко опишем все девять
файлов, содержащихся в папке data, с акцентом на расширения
имен. (Префикс skel напоминает, что мы работаем с заготовкой.)
skel.cspec. Это файл спецификаций компилятора в формате
XML, поначалу обескураживающий своей сложностью.
► skel.ldefs. Это файл определения языка в формате XML.
Заготовка содержит закомментированный шаблон определения языка.
► skel.opinion. Это opinion-файл импортера в формате
XML. Заготовка содержит закомментированный шаблон
определения спецификации языка и компилятора.
540

ГЛАВА 18

► skel.pspec. Это файл спецификации процессора в формате XML.
► skel.sinc. Это SLEIGH-файл с описанием команд языка1.
► skel.slaspec. Это файл спецификации на языке SLEIGH.
► buildLanguage.xml. Это XML-файл, в котором описан
процесс построения файлов в каталоге data/languages.
► README.txt. Это такой же файл, как во всех прочих модулях, но теперь он наконец-то содержательный, потому
что содержит информацию о содержимом каталога data/.
► sleighArgs.txt. Содержит параметры компилятора SLEIGH.
Файлы с расширениями .ldefs и .opinion использовались
в главе 17 при построении загрузчика шелл-кода, внедренного в ELF-файл. Остальные расширения мы рассмотрим в контексте по мере проработки примеров. Вы научитесь работать
с этими файлами для модификации процессорного модуля, но
прежде обсудим новый термин – SLEIGH.

SLEIGH
SLEIGH – это язык, который применяется в Ghidra для описания систем команд микропроцессоров, он нужен для поддержки процессов дизассемблирования и декомпиляции в Ghidra2.
Файлы в каталоге languages (см. рис. 18.1) либо написаны на
SLEIGH, либо представлены в формате XML, поэтому некоторое знакомство с языком SLEIGH необходимо для создания
или модификации процессорного модуля.
Описание того, как команды кодируются и интерпретируются процессором, находится в файле с расширением .slaspec
(он играет примерно такую же роль, как c-файл). Если в семействе процессоров имеется несколько вариантов, то для каждого
должен существовать свой slaspec-файл, но общие для разных
вариантов особенности поведения можно вынести в отдельные
sinc-файлы (аналог h-файлов), которые включаются в несколь1

2

Для больших систем команд, как, например, в случае процессора x86, sincфайл может быть разбит на несколько sinc-файлов. Тогда некоторые из
них могут использоваться как заголовочные, содержащие определения
и включаемые в другие файлы.
Подробную информацию о языке SLEIGH можно найти в файле docs/
languages/html/sleigh.html в установочном каталоге Ghidra.
Процессорные модули в Ghidra

541

ко slaspec-файлов. Процессорный модуль ARM в Ghidra дает
прекрасный пример этого подхода; он содержит больше десятка
slaspec-файлов, каждый из которых ссылается на один или несколько из пяти sinc-файлов. Эти файлы составляют исходный
SLEIGH-код для процессорного модуля, а задача компилятора
SLEIGH – преобразовать их в один sla-файл, понятный Ghidra.
Вместо того чтобы давать подробное теоретическое описание языка SLEIGH, мы будем знакомиться с различными его
час­тями по мере возникновения в них надобности, но сначала
посмотрим, какого рода информация о командах содержится
в SLEIGH-файле.
Чтобы увидеть дополнительные сведения о команде в лис­
тинге браузера кода, щелкните правой кнопкой мыши и выберите из контекстного меню пункт Instruction Info. Отображаемая информация берется из спецификаций в SLEIGH-файле
для конкретной команды. На рис. 18.2 показано окно информации о команде PUSH процессора x86-64.

Рис. 18.2. Окно информации о команде PUSH процессора x86-64
542

ГЛАВА 18

В окне информации о команде объединено все, что известно
о команде PUSH из SLEIGH-файла, с деталями ее конкретного
использования по адресу 00100736. Ниже в этой главе мы будем
работать с определениями команд на языке SLEIGH и вернемся к этому окну в контексте рассматриваемых команд.

Руководства по процессорам
Документация, поставляемая производителем процессора, –
важный ресурс для получения информации о системе команд.
Эти защищенные авторским правом материалы нельзя включить в дистрибутив Ghidra, но вы можете сделать это самостоятельно, воспользовавшись контекстным меню в окне листинга.
Щелкнув правой кнопкой мыши по команде и выбрав из меню
пункт Processor Manual (Руководство по процессору), вы, скорее всего, увидите сообщение типа показанного на рис. 18.3,
информирующее о том, что руководство для текущего процессора не найдено в ожидаемом месте.

Рис. 18.3. Диалоговое окно с сообщением об отсутствующем
руководстве по процессору
Заодно Ghidra говорит, что нужно сделать для решения
проб­лемы. В данном случае вы должны сначала найти в сети
руководство по x86, а затем сохранить его в указанном месте
под указанным именем.
ПРИМЕЧАНИЕ

Для процессора x86 есть много руководств. Для нахождения нужного ориентируйтесь на идентификатор в конце руководства: 325383-060US.
Процессорные модули в Ghidra

543

После того как руководство правильно установлено, выбор
пункта Processor Manual приведет к его отображению. Поскольку руководства по процессору велики (это конкретное
занимает почти 2200 страниц), Ghidra любезно включает возможность использовать файлы указателей, которые сопоставляют команде страницу в руководстве. По счастью, указатель
для этого конкретного руководства по x86 уже создан.
Руководство по процессору должно быть помещено в каталог
Ghidra/Processors//data/manuals, соответствующий процессору. Файлы указателей должны находиться в том же каталоге, что и само руководство. Формат файла указателя довольно
прост. Первые несколько строк файла x86.idx приведены в лис­
тинге ниже.
@Intel64_IA32_SoftwareDevelopersManual.pdf [Intel 64 and IA-32 Architectures
Software Developer's Manual Volume 2 (2A, 2B, 2C & 2D): Instruction Set
Reference, A-Z, Sep 2016 (325383-060US)]
AAA, 120
AAD, 122
BLENDPS, 123
AAM, 124

Первая строка файла (она размещена в трех строках листинга) сопоставляет локальному имени файла руководства описание, которое показывается пользователю, когда руководства
нет в системе. Формат строки следующий:
@FilenameInGhidraManualDirectory [Описание файла руководства]

Все последующие строки имеют вид КОМАНДА, страница.
Команда должна быть записана заглавными буквами, а страницы нумеруются, начиная с первой страницы pdf-файла. (Это
необязательно номер страницы, отображаемый на соответствующей странице документа.)
Один idx-файл может содержать указатели для нескольких
руководств. Нужно лишь вставить дополнительные директивы @, разделяющие указатели. Дополнительные сведения о составленных вручную файлах указателей для руководств по
544

ГЛАВА 18

процессорам можно найти в файле docs/languages/manual_
index.txt в установочном каталоге Ghidra.
После того как руководство сохранено и вручную проиндексировано, выбор пункта Processor Manual для любой команды
в окне листинга должен открывать соответствующую страницу
руководства. Если ничего не произошло, то, возможно, следует выбрать из меню пункт EditTools OptionsProcessor
Manuals и настроить подходящую программу просмотра руководства. На рис. 18.4 показано, как настроить просмотр руководства в веб-браузере Firefox.

Рис. 18.4. Параметры просмотра руководств по процессорам
Познакомившись с основами терминологии, самое время окунуться в пучину внутреннего устройства процессорных модулей.

МОДИФИКАЦИЯ ПРОЦЕССОРНОГО
МОДУЛЯ GHIDRA
Создание процессорного модуля с нуля – предприятие не для
слабых духом. Поэтому, вместо того чтобы прыгать без оглядки, мы, как и в предыдущих примерах, начнем с модификации
существующего модуля. Поскольку мы хотим продемонстрировать вещи, встречающиеся в реальных задачах, рассмотрим
для начала гипотетическую проблему, касающуюся процессорного модуля x86 в Ghidra. Мы по шагам разберем несколько
примеров на эту тему, а затем воспользуемся тем, что узна­ли,
чтобы представить общую картину того, как различные компоненты работают совместно, образуя в результате процессорный модуль.

Процессорные модули в Ghidra

545

Редактор SLEIGH в Ghidra
Чтобы помочь в модификации и создании процессорных модулей, Ghidra включает редактор SLEIGH, который легко интегрируется со средой Eclipse. Инструкции по установке редактора –
часть файла readme, о котором упоминалось в предыдущем
разделе. Установка требует всего нескольких шагов. Редактор
поддерживает следующие специальные функции.


Синтаксическая подсветка. Части кода, имеющие специальный смысл (например, комментарии, токены, строки,
переменные и т. д.), выделяются цветом.



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



Быстрое исправление. Даются рекомендации по разрешению проблем, замеченных редактором (это аналог вариантов быстрого исправления в случае отсутствия предложений import, с которым мы встречались в главе 15).



Наведение мыши. Для многих конструкций предоставляется дополнительная информация при наведении курсора
мыши.



Навигация. Предоставляются средства навигации, специ­
фичные для SLEIGH (например, подконструкторы, лексемы, регистры, p-коды и т. д.).



Поиск ссылок. Быстрый поиск всех случаев использования переменной.



Переименование. Вместо традиционного поиска и замены строк этот механизм переименовывает переменную
в данном файле и связанных с ним sinc- и slaspec-файлах.



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

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

546

ГЛАВА 18

Постановка задачи
Беглый просмотр каталога Ghidra/Processors в локальной
установке показывает, что в процессорном модуле x86 имеется
много команд, но отсутствует гипотетическая команда из расширения для виртуальных машин (virtual machine extensions –
VMX) для архитектур IA32 и IA641. Эта команда (которую мы
придумали специально для данного примера) называется
VMXPLODE. Она похожа на команду VMXOFF, которую Ghidra таки
поддерживает. Если команда VMXOFF заставляет процессор вый­
ти из режима VMX, то VMXPLODE выходит эффектно! Мы покажем все шаги добавления этой очень важной команды в существующий процессорный модуль x86, чтобы познакомиться
с некоторыми понятиями, связанными с созданием и модификацией процессорного модуля.

Пример 1: добавление команды
в процессорный модуль
Нашей первой целью является определение файлов, которые необходимо модифицировать для поддержки команды
VMXPLODE. В каталоге Ghidra/Processors имеются подкаталоги
для всех процессоров, поддерживаемых Ghidra, в т. ч. для x86.
Мы можем открыть процессорный модуль x86 (или любой другой) прямо в Eclipse, воспользовавшись командой FileOpen
Projects from File System or Archive и задав путь к папке процессора (Ghidra/Processors/x86). При этом будет установлена связь между Eclipse и процессорным модулем x86
в Ghidra, т. е. изменения, произведенные в Eclipse, будут сразу
отражаться в Ghidra.
На рис. 18.5 показана частично раскрытая версия модуля
x86 в Eclipse, которая точно отражает структуру каталогов
в Ghidra. Скачанное вами руководство по процессору и файл
указателя x86.idx тоже присутствуют.

1

В разделе 30-1 следующего документа описываются существующие команды управления VMCS: https://www.intel.com/content/dam/www/public/
us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-3cpart-3-manual.pdf.
Процессорные модули в Ghidra

547

Рис. 18.5. Процессорный модуль x86 в обозревателе пакетов Eclipse
Папка x86 содержит папку data, такую же, как мы видели в процессорном модуле, созданном с помощью EclipseGhidraDev.
Внутри нее имеется папка languages, содержащая более 40 файлов, в т. ч. 19 sinc-файлов, определяющих команды языка. Поскольку система команд x86 довольно велика, ее описание
разбито на несколько файлов, в каждом из которых находятся
родственные команды. Вместо того чтобы создавать новый sincфайл для нашей команды, мы добавим ее в существующий.
Если бы мы добавляли целую группу команд (например, для
расширения x86 SGX), то, наверное, создали бы sinc-файл, чтобы
сгруппировать их в одном месте. (На самом деле команды, входящие в состав SGX, помещены в отдельный файл sgx.sinc. Так
что о назначении одного из многих sinc-файлов мы уже знаем!)
Просматривая sinc-файлы, мы обнаружим, что ia.sinc содержит определения команд из существующего расширения VMX.
Мы воспользуемся определением команды VMXOFF в файле ia.
sinc как образцом для определения VMXPLODE. VMXOFF встречается в двух секциях ia.sinc. Первая – определения аппаратных
команд виртуализации на платформе Intel IA:
# MFL: definitions for Intel IA hardware assisted virtualization instructions
define pcodeop invept; # Invalidate Translations Derived from extended page
# tables (EPT); opcode 66 0f 38 80
# -----ЧАСТЬ ТЕКСТА ОПУЩЕНА----define pcodeop vmread; # Read field from virtual-machine control structure;
# opcode 0f 78
define pcodeop vmwrite; # Write field to virtual-machine control

548

ГЛАВА 18

structure;
# opcode 0f 79
define pcodeop vmxoff; # Leave VMX operation; opcode 0f 01 c4
define pcodeop vmxon; # Enter VMX operation; opcode f3 0f C7 /6

В каждой записи определена операция p-кода (pcodeop) – новая операция микрокода для архитектуры x86.
Определение включает имя и в данном случае комментарий,
содержащий описание и код операции. Нам нужно написать
комментарий для новой команды. Быстрый поиск в альтернативной реальности подтверждает, что код операции 0f 01
c5 уже давно зарезервирован для команды VMXPLODE. Теперь
у нас достаточно информации для добавления новой команды
в файл. Ниже показано новое определение в контексте:
define pcodeop vmxoff; # Leave VMX operation; opcode 0f 01 c4
define pcodeop vmxplode; # Операция взрыва (фиктивная); opcode 0f 01 c5
define pcodeop vmxon;
# Enter VMX operation; opcode f3 0f C7 /6

Второй раз VMXOFF встречается в файле ia.sinc в секции определения кода операции (именно туда мы вставим нашу новую команду). (Мы опустили часть этой секции для большей
ясности и расположили некоторые определения в нескольких
строках для удобочитаемости.) Мы не собираемся полностью
разбирать 8000+ строк кода в файле ia.sinc, но некоторые интересные моменты в показанном ниже листинге все же отметим.
# Intel hardware assisted virtualization opcodes
# -----ЧАСТЬ ТЕКСТА ОПУЩЕНА----# TODO: invokes a VM function specified in EAX
:VMFUNC EAX is vexMode=0 & byte=0x0f; byte=0x01; byte=0xd4 & EAX { vmfunc(EAX); }
# TODO: this launches the VM managed by the current VMCS. How is the
#
VMCS expressed for the emulator? For Ghidra analysis?
:VMLAUNCH is vexMode=0 & byte=0x0f; byte=0x01; byte=0xc2
{ vmlaunch(); }
# TODO: this resumes the VM managed by the current VMCS. How is the
#
VMCS expressed for the emulator? For Ghidra analysis?
:VMRESUME is vexMode=0 & byte=0x0f; byte=0x01; byte=0xc3
{ vmresume(); }
# -----ЧАСТЬ ТЕКСТА ОПУЩЕНА----:VMWRITE Reg32, rm32 is vexMode=0 & opsize=1 & byte=0x0f; byte=0x79;
rm32 & Reg32 ... & check_Reg32_dest ... { vmwrite(rm32,Reg32); build
check_Reg32_dest; }

Процессорные модули в Ghidra

549

@ifdef IA64
:VMWRITE Reg64, rm64 is vexMode=0 & opsize=2 & byte=0x0f; byte=0x79;
rm64 & Reg64 ... { vmwrite(rm64,Reg64); }
@endif
:VMXOFF is vexMode=0 & byte=0x0f; byte=0x01; byte=0xc4
{ vmxoff(); }
:VMXPLODE is vexMode=0 & byte=0x0f; byte=0x01; byte=0xc5
{ vmxplode(); }
# -----ЧАСТЬ ТЕКСТА ОПУЩЕНА----#END of changes for VMX opcodes

Комментарии TODO , встречающиеся во многих файлах
Ghidra, описывают задачи, которые еще только предстоит решить. Поиск по слову TODO в файлах Ghidra – прекрасный способ понять, чем лично вы можете помочь проекту.
Далее мы видим команду VMWRITE для 32-разрядной 
и 64-разрядной архитектур. 64-разрядная команда окружена проверкой , гарантирующей, что она включается только
в 64-разрядный sla-файл. Хотя 32-разрядные команды действительны и в 64-разрядном мире (например, EAX – младшие
32 бита регистра RAX), обратное неверно. Условное предложение гарантирует, что команды, работающие с 64-разрядными
регистрами, включены только для 64-разрядных сборок.
Команда VMXOFF  напрямую с регистрами не работает, поэтому различать ее 32- и 64-разрядные версии необязательно.
Конструктор новой команды VMXPLODE  вкупе с новым кодом
операции очень похож на конструктор VMXOFF. Разберем отдельные компоненты, составляющие эту строку.
:VMXPLODE
Это код определяемой команды, который отображается
в листинге дизассемблера.
is vexMode=0 & byte=0x0f; byte=0x01; byte=0xc5
Это битовые комбинации, ассоциированные с командой
и определяющие ограничения для нее. Символ & представляет операцию ЛОГИЧЕСКОЕ И (AND). Точки с запятой
играют двоякую роль: конкатенация и ЛОГИЧЕСКОЕ И.
Эта часть читается так: «Если мы не находимся в режиме
VEX и код операции состоит из этих трех байтов в таком порядке, то это ограничение удовлетворяется»1.
1

550

Схема кодирования VEX описана в разделе 2.3 документа https://www.intel.
com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architecturessoftware-developer-instruction-set-reference-manual-325383.pdf.

ГЛАВА 18

{ vmxplode(); }
В фигурные скобки заключена секция семантических действий. Компилятор SLEIGH транслирует эти действия во
внутреннюю форму Ghidra, называемую p-кодом (обсуждается ниже в этой главе). Для определения команды необходимо понимать операторы и синтаксис языка SLEIGH.
Эта часть конструктора, где производится основная работа
большинства команд, может быстро превратиться в запутанную последовательность нескольких предложений, разделенных точками с запятой. В данном случае, поскольку
мы определили VMXPLODE как новую операцию p-кода (define
pcodeop vmxplode;), мы можем вызвать здесь эту команду.
В последующих примерах мы добавим в эту секцию семантические действия SLEIGH.
Самый большой sinc-файл x86 – ia.sinc, потому что в нем определено много команд (в т. ч. и наша команда VMXPLODE), а также
различные характеристики процессора x86 (например, порядок
байтов, регистры, токены, переменные и многое другое). Большая часть относящегося к x86 материала в ia.sinc не повторяется
в других sinc-файлах в этом каталоге, потому что все sinc-файлы
включаются в файл спецификации SLEIGH (slaspec-файл).
Оба slaspec-файла для x86, x86.slaspec и x86-64.slaspec, содержат предложения include для включения необходимых
sinc-файлов. (Отметим, что можно было бы обойтись и без sincфайлов, а вставить их содержимое непосредственно в slaspecфайл, и для процессоров с небольшой системой команд это,
быть может, и разумно.) Содержимое файла x86-64.slaspec показано в следующем листинге:
@define IA64 "IA64" # только в
 @include "ia.sinc"
@include "avx.sinc"
@include "avx_manual.sinc"
@include "avx2.sinc"
@include "avx2_manual.sinc"
@include "rdrand.sinc" # только
@include "rdseed.sinc" # только
@include "sgx.sinc" # только в
@include "adx.sinc"

x86-64.slaspec

в x86-64.slaspec
в x86-64.slaspec
x86-64.slaspec

Процессорные модули в Ghidra

551

@include
@include
@include
@include
@include
@include
@include
@include
@include
@include

"clwb.sinc"
"pclmulqdq.sinc"
"mpx.sinc"
"lzcnt.sinc"
"bmi1.sinc"
"bmi2.sinc"
"sha.sinc"
"smx.sinc"
"cet.sinc"
"fma.sinc" # только в x86-64.slaspec

Мы добавили концевые комментарии, чтобы отметить
строки, присутствующие только в файле x86-64.slaspec. (x86.
slaspec – подмножество x86-64.slaspec.) В состав включаемых файлов входит ia.sinc , в котором определена команда
VMXPLODE, так что добавлять нам ничего не нужно. Если вы
создадите новый sinc-файл, то нужно будет добавить предложение include в оба файла x86.slaspec и x86-64.slaspec, чтобы
команда распознавалась в 32- и в 64-разрядных двоичных
файлах.
Чтобы проверить, распознает ли Ghidra новую команду
в двоичном файле, создадим тестовый файл. В нем мы сначала проверим, что команда VMXOFF по-прежнему распознается,
а затем – что VMXPLODE успешно добавлена. Исходный файл на
C для тестирования VMXOFF содержит такой код:
#include
// Следующая функция объявляет блок на языке ассемблера и говорит
// компилятору, что он должен выполнить этот код, не перемещая и не
изменяя.
void do_vmx(int v) {
asm volatile (
"vmxon %0;" // разрешить гипервизор
"vmxoff;" // запретить гипервизор
"nop;" // небольшая серия nop для развития примеров
"nop;"
"nop;"
"nop;"
"nop;"
"nop;"
"nop;"

552

ГЛАВА 18

"vmxoff;" // запретить гипервизор
:
:"m"(v) // место для входной переменной
:
);
}
int main() {
int x;
printf("Введите int: ");
scanf("%d", &x);
printf("После ввода, x=%d\n", x);
do_vmx(x);
printf("После do_vmx, x=%d\n", x);
return 0;
}

Загрузив откомпилированный двоичный файл в Ghidra, мы
увидим следующее тело функции do_vmx в окне листинга:
0010071a
0010071b
0010071e
00100721
 00100726
00100729
0010072a
0010072b
0010072c
0010072d
0010072e
0010072f
00100730
00100733
00100734
00100735

55 PUSH RBP
48 89 e5 MOV RBP,RSP
89 7d fc MOV dword ptr [RBP + local_c],EDI
f3 0f c7 VMXON qword ptr [RBP + local_c]
75 fc
0f 01 c4 VMXOFF
90 NOP
90 NOP
90 NOP
90 NOP
90 NOP
90 NOP
90 NOP
0f 01 c4 VMXOFF
90 NOP
5d POP RBP
c3 RET

Байты кода операции (0f 01 c4) в обеих командах VMXOFF 
совпадают с тем, что мы видели в файле ia.sinc для этой команды. Следующий листинг, скопированный из окна декомпилятора, тоже согласуется с тем, что мы знаем об исходном коде
и соответствующем результате дизассемблирования:

Процессорные модули в Ghidra

553

void do_vmx(undefined4 param_1)
{
undefined4 unaff_EBP;




}

vmxon(CONCAT44(unaff_EBP,param_1));
vmxoff();
vmxoff();
return;

Чтобы проверить, распознает ли Ghidra команду VMXPLODE, заменим первое вхождение VMXOFF в функции do_vmx на VMXPLODE.
Однако команда VMXPLODE отсутствует не только в нашем определении процессора в Ghidra, но и в базе знаний компилятора.
Чтобы ассемблер принял наш код, мы вручную вставили код
операции с помощью объявления данных, не используя мнемоническое обозначение команды:
//"vmxoff;"
".byte 0x0f, 0x01, 0xc5;"

// заменить эту строку
// этой, ассемблированной вручную

Загрузив обновленный двоичный файл, мы увидим следующий код в окне листинга:
0010071a
0010071b
0010071e
00100721
 00100726
00100729
0010072a
0010072b
0010072c
0010072d
0010072e
0010072f
00100730
00100733
00100734
00100735

55 PUSH RBP
48 89 e5 MOV RBP,RSP
89 7d fc MOV dword ptr [RBP + local_c],EDI
f3 0f c7 VMXON qword ptr [RBP + local_c]
75 fc
0f 01 c5 VMXPLODE
90 NOP
90 NOP
90 NOP
90 NOP
90 NOP
90 NOP
90 NOP
0f 01 c4 VMXOFF
90 NOP
5d POP RBP
c3 RET

Наша новая команда  присутствует вместе с назначенным
ей кодом операции (0f 01 c5). В окне декомпилятора тоже показана новая команда:
554

ГЛАВА 18

void do_vmx(undefined4 param_1)
{
undefined4 unaff_EBP;
vmxon(CONCAT44(unaff_EBP,param_1));
vmxplode();
vmxoff();
return;
}

Итак, какую работу проделала Ghidra, чтобы добавить нашу
новую команду в свое представление о системе команд процессора x86? После перезапуска (необходимого, чтобы изменения
вступили в силу) Ghidra обнаруживает, что sinc-файл изменился, и в нужный момент генерирует новый sla-файл.
В данном случае, когда мы загрузили первоначальныйоткомпилированный 64-разрядный файл, Ghidra обнаружила
изменение в файле ia.sinc и отобразила окно, показанное на
рис. 18.6, на то время, пока перекомпилировала этот файл. (Заметим, что перекомпиляция производится только по мере необходимости, а не автоматически при перезапуске.) Поскольку
мы загрузили 64-разрядный файл, перекомпилируется только
файл x86-64.sla, но не x86.sla. Затем, когда мы загрузили обновленный файл, содержащий команду VMXPLODE, Ghidra ничего не перекомпилировала, потому что с момента предыдущей
загрузки исходные SLEIGH-файла не изменились.

Рис. 18.6. Окно, отображаемое Ghidra во время перекомпиляции
файла спецификации языка
Ниже кратко перечислены шаги добавления новой команды
в процессорный модуль.
Процессорные модули в Ghidra

555

1. Найти каталог languages для целевого процессора (например, Ghidra/Processor//data/languages).
2. Добавить команду в sinc-файл выбранного процессора или
создать новый sinc-файл (например, Ghidra/Processor/
/data/languages/ .sinc).
3. Если был создан новый sinc-файл, не забудьте включить его в slaspec-файл (например, Ghidra/Processor/
/data/languages/ .slaspec).

Пример 2: модификация команды
в процессорном модуле
Мы успешно добавили команду в процессорный модуль x86, но
пока не достигли цели – сделать выход из режима гипервизора
с помощью команды VMXPLODE эффектным. Пока что она просто
выходит, не вызывая никаких эмоций. Хотя довольно трудно
устроить так, чтобы ассемблерная команда делала нечто такое,
что можно было бы назвать эффектным, мы можем заставить
ее станцевать дэб (dab) при выходе1. Мы рассмотрим три варианта решения этой задачи. В первом из них будем выходить,
предварительно записав в EAX значение 0xDAB.

Вариант 1: записать в EAX константу
Чтобы VMXPLODE записывала значение 0xDAB в регистр EAX перед
выходом, нужно внести небольшое изменение в одну команду
в том же файле (ia.sinc), с которым мы работали в примере 1.
Ниже показаны команды VMXOFF и VMXPLODE в том виде, в котором мы их оставили после примера 1:
:VMXOFF is vexMode=0 & byte=0x0f; byte=0x01; byte=0xc4 { vmxoff(); }
:VMXPLODE is vexMode=0 & byte=0x0f; byte=0x01; byte=0xc5 { vmxplode(); }

В описании команды добавьте присваивание регистру EAX
непосредственно перед действием vmxplode, как показано в следующем листинге:
1

556

Дэб – танцевальное движение, которое спортивные знаменитости стали
использовать начиная с 2012 года. Для этого примера оно было выбрано,
потому что является одним из немногих танцевальных движений, которые можно записать, используя только шестнадцатеричные цифры.

ГЛАВА 18

:VMXOFF is vexMode=0 & byte=0x0f; byte=0x01; byte=0xc4 { vmxoff(); }
:VMXPLODE is vexMode=0 & byte=0x0f; byte=0x01; byte=0xc5 { EAX=0xDAB;
vmxplode(); }

Если повторно открыть Ghidra и загрузить наш тестовый
файл, то Ghidra снова покажет окно, изображенное на рис. 18.6,
чтобы уведомить нас о том, что изменение в файле описания языка обнаружено и она перекомпилирует файл x86-64.sla. В окне
листинга никаких изменений после автоматического анализа
не будет, зато в окне декомпилятора отличие имеет место:
undefined4 do_vmx(undefined4 param_1)
{
undefined4 unaff_EBP;
vmxon(CONCAT44(unaff_EBP,param_1));
vmxplode();
vmxoff();
return 0xdab;
}

Теперь в окне декомпилятора предложение return возвращает содержимое регистра EAX (0xDAB). Это интересно, потому
что, как мы знаем, это функция типа void, которая ничего
не возвращает. В окне листинга строка с новой командой ничем не выдает, что команда VMXPLODE как-то изменилась:
00100726 0f 01 c5 VMXPLODE

Важное различие между декомпилятором и дизассемблером
заключается в том, что декомпилятор понимает и включает
в свой анализ полную семантику каждой команды, тогда как
дизассемблер акцентирует внимание в основном на правильном синтаксическом представлении команды. В данном примере VMXPLODE не принимает операндов и правильно отображается дизассемблером, который ни одним намеком не сообщает,
что регистр EAX изменился. Читая листинг дизассемблера, вы
сами должны устанавливать семантическое поведение команд.
Пример также демонстрирует ценность декомпилятора, который, понимая всю семантику VMXPLODE, может определить, что
Процессорные модули в Ghidra

557

регистр EAX изменяется в результате побочного эффекта этой
команды. Кроме того, декомпилятор видит, что EAX не используется в оставшейся части функции, поэтому предполагает, что
его следует вернуть вызывающей функции.
Ghidra дает возможность немного глубже заглянуть во внут­
реннюю механику команд и позволяет обнаружить и протес­
тировать тонкие различия в подобных командах. Сначала
посмотрим на кое-какую информацию о команде VMXPLODE, показанную на рис. 18.7.




Рис. 18.7. Информация о команде VMXPLODE
Слева показана наша первоначальная команда VMXPLODE,
а справа ее модифицированная версия: константа 0xdab включена в секцию Input Objects (Входные объекты) , а EAX в секцию Result Objects (Результирующие объекты). Мы можем
получить дополнительные сведения о любой команде, взглянув на ее p-код, о котором пока еще не говорили1. Ассоциированный с командой p-код может дать много полезной информации о том, что делает команда.

1

558

В справке по Ghidra p-код пишется без дефиса (pcode), а почти во всей
остальной документации через дефис. Если вам не удается найти в Ghidra
информацию по слову «p-code», попробуйте опустить дефис и поискать
снова.

ГЛАВА 18

P-код: все ниже, и ниже, и ниже
В документации по Ghidra p-код описывается как «язык межрегист­
ровых пересылок, предназначенный для приложений обратной
разработки». Язык межрегистровых пересылок (register transfer
language – RTL) – это архитектурно независимый похожий на ассемблер язык, который часто используется в качестве промежуточного представления (intermediate representation – IR), или промежуточного языка (intermediate language – IL), расположенного
между языком высокого уровня типа C и целевым языком ассемб­
лера, например для x86 или ARM. Компиляторы часто состоят
из языково-зависимой фронтальной части, которая транслирует
исходный код в промежуточное представление, и архитектурно
зависимого постпроцессора, который транслирует промежуточное представление на конкретный язык ассемблера. Эта модульность позволяет комбинировать фронтальную часть C с постпроцессором x86, в результате чего получается компилятор C, генерирующий код для x86. Но если подставить постпроцессор ARM,
то получится компилятор C, генерирующий код для ARM. А если
подставить фронтальную часть FORTRAN, то на выходе получим
компилятор FORTRAN для ARM.
Работа на уровне IR позволяет создавать инструменты, оперирующие промежуточным представлением, вместо того чтобы
поддерживать наборы инструментов для C или для ARM, бесполезные для других языков или архитектур. Например, если имеется оптимизатор, работающий с IR, то мы сможем применить
его к любой комбинации фронтальной части и поспроцессора,
не переписывая оптимизатор для каждого случая.
Инструментарий обратной разработки, понятное дело, работает
противоположно традиционному комплекту инструментов сборки. Фронтальная часть RE транслирует машинный код в IR (этот
процесс часто называют лифтингом), а построцессор RE транслирует IR на высокоуровневый язык типа C. Чистый дизассемб­
лер в этом смысле не является фронтальной частью, потому что
умеет только транслировать машинный код на язык ассемблера.
Декомпилятор Ghidra – это постпроцессор, транслирующий IR на
C. Процессорные модули – это фронтальные части, транслирующие машинный код в IR.
Создавая или модифицируя процессорный модуль Ghidra на языке SLEIGH, мы первым делом сообщаем компилятору SLEIGH
о новых операциях p-кода, которые необходимо ввести, чтобы
описать семантику новых или модифицированных команд. Например, определение операции
Процессорные модули в Ghidra

559

define pcodeop vmxplode
добавленное нами в файл ia.sinc, говорит компилятору SLEIGH,
что vmxplode – допустимое семантическое действие, которое можно использовать в описаниях поведения любой команды в нашей
архитектуре. Одна из самых трудных проблем, с которыми приходится сталкиваться, – специфицирование каждой новой или измененной команды с помощью последовательности синтаксически
корректных предложений SLEIGH, которые правильно описывают
действия, связанные с этой командой. Вся эта информация хранится в slaspec-файлах и включаемых sinc-файлах, в совокупности образующих процессор. Если эта работа проделана хорошо, то Ghidra
вознаградит вас бесплатным построцессором декомпилятора.

Чтобы просмотреть p-код в окне листинга, откройте форматер полей браузера и перейдите на вкладку Instruction/Data,
затем щелкните по полосе P-code и активируйте поле. После
того как в окне листинга появится p-код каждой команды, мы
сможем сравнить два предыдущих листинга и отметить различия. Когда p-код включен, наша первая реализация VMXPLODE
выглядит следующим образом (после каждой команды показан
связанный с ней p-код):
0010071b 48 89 e5 MOV RBP,RSP
RBP = COPY RSP
$U620:8 = INT_ADD RBP, -4:8
$U1fd0:4 = COPY EDI
STORE ram($U620), $U1fd0
00100721 f3 0f c7 75 fc VMXON qword ptr [RBP + local_c]
$U620:8 =
$Ua50:8 =
CALLOTHER
00100726 0f 01 c5 VMXPLODE
CALLOTHER
00100729 90 NOP

INT_ADD RBP, -4:8
LOAD ram($U620)
"vmxon", $Ua50
"vmxplode"

А вот как выглядит модифицированная команда VMXPLODE:

560

ГЛАВА 18

00100726 0f 01 c5 VMXPLODE
 AX = COPY 0xdab:4
CALLOTHER "vmxplode"

Теперь p-код показывает, что константа (0xdab) записывается в EAX .

Вариант 2: записать в регистр
(определяемый операндом) константу
Системы команд обычно включают команды с различным числом операндов. Чем больше число операндов и разнообразнее
их типы, тем труднее описывать семантику команды. В этом
примере мы усложним поведение VMXPLODE, добавив один регистровый операнд, который и заставим сплясать дэб. Для этого потребуется заглянуть в секции файла ia.sinc, которые мы
пока не посещали. На этот раз мы начнем с модифицированной версии команды и пойдем назад. В листинге ниже показано, какие изменения нужно внести в определение команды,
чтобы она принимала операнд, задающий регистр, в который
будет записана константа 0xDAB:
:VMXPLODE Reg32 is vexMode=0 & byte=0x0f; byte=0x01; byte=0xc5; Reg32
{ Reg32=0xDAB; vmxplode(); }

Здесь Reg32  объявлена как локальный модификатор, а затем конкатенируется с кодом операции  и становится частью
ограничений, связанных с командой. Вместо того чтобы присваивать значение 0xDAB непосредственно регистру EAX, как
раньше, команда теперь записывает значение в Reg32 . Чтобы добиться поставленной цели, мы должны придумать, как
ассоциировать значение в Reg32 с выбранным регистром x86.
Рассмотрим другие компоненты файла ia.sinc, которые помогут нам понять, как правильно отобразить операнд на регистр
общего назначения x86.
Близко к началу ia.sinc мы видим определения, которые понадобятся для спецификации в целом (листинг 18.1).

Процессорные модули в Ghidra

561

# SLA specification file for Intel x86
@ifdef IA64
@define SIZE
«8»
@define STACKPTR «RSP»
@else
@define SIZE
«4»
@define STACKPTR «ESP»
@endif
define endian=little;
define space ram type=ram_space size=$(SIZE) default;
define space register type=register_space size=4;
# General purpose registers
@ifdef IA64
define register offset=0 size=8 [ RAX RCX RDX RBX RSP RBP RSI RDI ];
define register offset=0 size=4 [ EAX _ ECX _ EDX _ EBX _ ESP _ EBP _ ESI _ EDI ];
define register offset=0 size=2 [ AX _ _ _ CX _ _ _ DX _ _ _ BX]; # truncated
define register offset=0 size=1 [ AL AH _ _ _ _ _ _ CL CH _ _ _ _ _ _]; # truncated y
define register offset=0x80 size=8 [ R8 R9 R10 R11 R12 R13 R14 R15 ];
define register offset=0x80 size=4 [ R8D _ R9D _ R10D _ R11D _ R12D _ R13D _ R14D _ R15D ];
define register offset=0x80 size=2 [ R8W _ _ _ R9W _ _ _ R10W _ _ _ R11W ]; # truncated
define register offset=0x80 size=1 [ R8B _ _ _ _ _ _ _ R9B _ _ _ _ _ _ _ ]; # truncated
@else
define register offset=0 size=4 [ EAX ECX EDX EBX ESP EBP ESI EDI ];
define register offset=0 size=2 [ AX _ CX _ DX _ BX _ SP _ BP _ SI _ DI ];
define register offset=0 size=1 [ AL AH _ _ CL CH _ _ DL DH _ _ BL BH ];
@endif

Листинг 18.1. Частичная спецификация регистров x86 на
языке SLEIGH (взято из файла ia.sinc и немного сокращено)
В начале файла мы видим имя и размер указателя стека для
32- и 64-разрядной сборок , а также порядок байтов  для
x86. Комментарий  отмечает начало определений регистров
общего назначения. В SLEIGH принято соглашение об именовании и определении регистров: регистры располагаются в специальном адресном пространстве register, и каждому регистру
(который может занимать 1 или более байт) назначается смещение от начала этого пространства. В определении регистра
на языке SLEIGH указывается смещение некоторого списка
регистров внутри адресного пространства. Все регистры, принадлежащие этому списку, располагаются подряд, если только
между ними нет знака подчеркивания, обозначающего промежуток. Расположение в адресном пространстве 64-разрядных
регистров RAX и RCX  подробно показано на рис. 18.8.
562

ГЛАВА 18

Размер

Смещение
0

1

2

3

8

5

6

7

8

9

10

11

RAX

4

_

AX
AL

_
AH

_

13

ECX

_
_

12

14

15

RCX

EAX

2
1

4

_

_
_

_

_
_

CX
_

CL

CH

_

_
_

_

_
_

_

_

Рис. 18.8. Расположение регистров RAX и RCX процессора x86-64
Регистр AL занимает то же место, что младший байт RAX, EAX
и AX (поскольку x86 – процессор с прямым порядком байтов).
Аналогично EAX занимает младшие 4 байта RAX. Знак подчеркивания показывает, что с 4-байтовым блоком адресов со
смещениями от 4 до 7 не связано никакого имени, хотя эти
байты занимают то же место, что старшая половина регистра
RAX. В листинге 18.1 описан отдельный блок регистров, начинающийся с регистра R8 со смещением 0x80 . Однобайтовый
регистр со смещением 0x80 называется R8B, а однобайтовый регистр со смещением 0x88 – R9B. Надеемся, что сходство между
текстовым определением регистров в листинге 18.1 и их таб­
личным представлением на рис. 18.8 очевидно, поскольку
определения регистров в SLEIGH-файле – не что иное, как
текстовое представление адресного пространства регистров
в архитектуре процессора.
Если вы собираетесь писать на SLEIGH описание архитектуры, которая вообще не поддерживается Ghidra, то на вас ляжет ответственность распланировать адресное пространство
регистров для нее, так чтобы никакие регистры не перекрывались, если только этого не требует архитектура (как в случае
регистров RAX, EAX, AX, AH, AL в архитектуре x86-64).
Теперь, разобравшись с тем, как представляются регистры
в SLEIGH, вернемся к нашей задаче – выбрать регистр, который спляшет дэб! Чтобы наша команда работала правильно,
она должна отображать Reg32 на регистр общего назначения.
Для этого мы можем воспользоваться существующим определением в файле ia.sinc, которое находится в следующих
строчках кода:

Процессорные модули в Ghidra

563

 define token modrm (8)
mod = (6,7)
reg_opcode = (3,5)
reg_opcode_hb = (5,5)
r_m = (0,2)
row = (4,7)
col = (0,2)
page = (3,3)
cond = (0,3)
reg8 = (3,5)
reg16 = (3,5)
reg32 = (3,5)
reg64 = (3,5)
reg8_x0 = (3,5)

В предложении define  объявляется 8-разрядный токен
modrm. В SLEIGH токен – это синтаксический элемент, используемый для представления байтовых компонентов, из которых
составлены моделируемые команды1. SLEIGH допускает определение любого числа битовых полей (состоящих из одного или
нескольких соседних битов) внутри токена. При определении
команд в SLEIGH эти битовые поля дают удобные символические средства задания операндов. В листинге выше битовое
поле reg32  охватывает биты modrm с 3 по 5. Это 3-битовое поле
принимает значения от 0 до 7, так что его можно использовать
для выбора одного из восьми 32-разрядных регистров x86.
Перейдя к следующему вхождению reg32 в файл, мы увидим
такие интересные строчки:
# attach variables fieldlist registerlist;
attach variables [ r32 reg32 base index ] [ EAX ECX EDX EBX ESP EBP ESI EDI ];
#
0 1 2 3 4 5 6 7

Первая и последняя строки листинга содержат комментарии,
показывающие синтаксис SLEIGH этого предложения и порядковые номера регистров. Предложение attach variables
ассоциирует поле со списком (в данном случае – со списком
регистров общего назначения x86). Приблизительная интер1

564

Это понятие подробно описано в разделе «Токены и поля» (6) документации по SLEIGH.

ГЛАВА 18

претация этой строки кода с учетом предыдущего определения
modrm такова: значение reg32 определяется битами 3–5 токена
modrm. Получающееся значение (от 0 до 7) используется как индекс регистра в списке.
Теперь у нас имеется способ идентифицировать регистр общего назначения, в который следует записать константу 0xDAB.
В следующий раз Reg32 встречается в коде, который содержит
конструктор Reg32 для 32- и 64-разрядных регистров, и теперь
мы видим связь между reg32 и Reg321:
Reg32: reg32 is rexRprefix=0 & reg32 { export reg32; } #64-bit Reg32
Reg32: reg32 is reg32
{ export reg32; } #32-bit Reg32

Вернемся к команде, с которой началось это небольшое приключение:
:VMXPLODE Reg32is vexMode=0 & byte=0x0f; byte=0x01; byte=0xc5; Reg32
{ Reg32=0xDAB; vmxplode(); }

Мы собираемся добавить в команду VMXPLODE операнд, который будет определять, в какой регистр записывать 0xDAB.
Обновим тестовый двоичный файл, убрав первую команду NOP
и дописав значение 0x08 в нашу ассемблированную вручную
команду. Первые 3 байта – это код операции (0f 01 c5), а следующий байт (08) – операнд, определяющий регистр:
".byte 0x0f, 0x01, 0xc5, 0x08;" // вручную ассемблированная команда
// с операндом

На рис. 18.9 показаны шаги преобразования операнда в регистр на основе информации в файле ia.sinc.

1

Это понятие подробно описано в разделе «Конструкторы» (7) документации по SLEIGH.
Процессорные модули в Ghidra

565








08

Операнд
Значение

0

0

0

0

Биты modrm

7

6

5

4

Регистры

0

0

0

3

2

1

0

001

Reg32
Порядковые
номера

1

0

1

2

3

4

5

6

7

EAX

ECX

EDX

EBX

ESP

EBP

ESI

EDI

Рис. 18.9. Путь преобразования операнда в регистр
Исходное значение операнда, показанное в первой строке,
равно 0x08 . Это значение представляется в двоичном виде
 и накладывается на поля токена modrm . Выделяются биты
3–5, что дает значение Reg32, равное 001 . Это значение используется как порядковый номер , которому соответствует
регистр ECX . Таким образом, операнд 0x08 означает, что константа 0xDAB должна быть записана в регистр ECX.
После того как мы сохраним обновленный файл ia.sinc, перезапустим Ghidra, а затем загрузим и проанализируем файл,
будет сгенерирован следующий листинг, в котором присутствует новая команда. Как и следовало ожидать, для хранения
0xDAB выбран регистр ECX:
00100721 f3 0f c7 75 fc VMXON qword ptr [RBP + local_c]
$U620:8 = INT_ADD RBP, -4:8
$Ua50:8 = LOAD ram($U620)
CALLOTHER "vmxon", $Ua50
00100726 0f 01 c5 08
VMXPLODE ECX
ECX = COPY 0xdab:4
CALLOTHER "vmxplode"
0010072a 90 NOP

Значение 0xDAB больше не появляется в окне декомпилятора,
потому что декомпилятор предполагает, что значение всегда возвращается в регистре EAX. В данном же случае мы используем ECX,
поэтому декомпилятор не считает, что это возвращаемое значение.
Теперь, когда мы знаем, как заставить выбранный регистр
станцевать дэб, добавим в качестве второго операнда 32-разрядное промежуточное значение. Это удвоит наш приветственный потенциал.
566

ГЛАВА 18

Вариант 3: операнды-регистры
и операнды-значения
Чтобы наша команда могла принимать два операнда (конечный регистр и константу), изменим определение VMXPLODE следующим образом:
:VMXPLODE Reg32,imm32 is vexMode=0 & byte=0x0f; byte=0x01; byte=0xc5;
Reg32; imm32
{ Reg32=imm32; vmxplode(); }

Включение непосредственной 32-разрядной константы в команду требует дополнительных 4 байтов. Поэтому заменим
следующие четыре команды NOP значениями, которые правильно кодируют операнд imm32 в прямом порядке байтов:
".byte 0x0f, 0x01, 0xc5, 0x08, 0xb8, 0xdb, 0xee, 0x0f;"
"nop;"
"nop;"

После перезагрузки файла VMXPLODE из режима гипервизора
выходит с другим эффектом. Как видно из следующего листинга (где показан p-код), ECX теперь содержит значение 0xFEEDBB8
(быть может, более впечатляющий выход с точки зрения любителей научной фантастики):
00100726 0f 01 c5 VMXPLODE ECX,0xfeedbb8
08 b8 db
ee 0f
ECX = COPY 0xfeedbb8:4
CALLOTHER "vmxplode"

Пример 3: добавление регистра
в процессорный модуль
Завершая примеры на тему процессорных модулей, мы расширим архитектуру, добавив два новых регистра1. Напомним
определение 32-разрядных регистров общего назначения:
1

Эта идея подробно описана в разделе «Именование регистров» (4.4) документации по SLEIGH.
Процессорные модули в Ghidra

567

define register offset=0 size=4 [EAX ECX EDX EBX ESP EBP ESI EDI];

Для определения регистра нужно задать смещение, размер и список регистров. Чтобы выбрать начальное смещение
в адресном пространстве регистров, следует рассмотреть уже
выделенные смещения и найти свободное место для двух 4-байтовых регистров. Эту информацию можно использовать для
определения двух новых 32-разрядных регистров VMID и VMVER
в файле ia.sinc, как показано в следующем листинге:
# Определить VMID и VMVER
define register offset=0x1500 size=4 [ VMID VMVER ];

У наших команд должно быть средство определить, с каким
новым регистром (VMID или VMVER) они работают. В предыдущем
примере мы использовали 3-битовое поле для выбора одного из
восьми регистров. Для выбора одного из двух регистров достаточно одного бита. В следующем предложении определяется
однобитовое поле в токене modrm и ассоциируется с vmreg:
# Ассоциировать vmreg с одним битом в токене modrm.
vmreg = (3, 3)

Следующее предложение присоединяет vmreg ко множеству
порядковых номеров, содержащему два регистра, – 0 представляет VMID, 1 – VMVER:
attach variables [ vmreg ] [ VMID VMVER ];

Определения команд могут ссылаться на vmreg, когда любой
из присоединенных регистров допустим в команде, тогда как
программисты на ассемблере могут употреблять VMID и VMER
в качестве операндов любой команды, допускающей операнд
vmreg. Сравним следующие два определения VMXPLODE. Первое
взято из предыдущего примера, где выбор производился из
числа регистров общего назначения, а во втором выбирается
один из двух новых регистров, а не регистр общего назначения:
568

ГЛАВА 18

:VMXPLODE Reg32,imm32 is vexMode=0 & byte=0x0f; byte=0x01; byte=0xc5;
Reg32, imm32
{ Reg32=imm32; vmxplode(); }
:VMXPLODE vmreg,imm32 is vexMode=0 & byte=0x0f; byte=0x01; byte=0xc5;
vmreg, imm32
{ vmreg=imm32; vmxplode(); }

Во втором листинге Reg32 заменено на vmreg. Если мы возьмем тот же самый входной файл с тестовой командой vmxplode
0x08,0xFEEDBB8, то непосредственный операнд 0xFEEDBB8 будет
загружен в VMVER, поскольку входное значение 0x08 отображается на порядковый номер 1 (т. к. поднят бит 3), как показано
на рис. 18.10, а первым регистром в vmreg как раз и является
VMVER. После загрузки тестового файла (для этого нужно сохранить ia.sinc и перезапустить Ghidra) мы увидим, что p-код
в окне листинга показывает, что непосредственный операнд
загружен в VMVER:
00100726 0f 01 c5 VMXPLODE VMVER,0xfeedbb8
08 b8 db
ee 0f
VMVER = COPY 0xfeedbb8:4
CALLOTHER "vmxplode"

Относящаяся к команде информация, показанная на рис. 18.10,
также подтверждает, что изменение произведено успешно.

Рис. 18.10. Информация о команде VMXPLODE, когда выбран
новый регистр VMVER

Процессорные модули в Ghidra

569

РЕЗЮМЕ
В этой главе мы познакомились лишь с малой толикой файла
с описанием процессора x86, но все же рассмотрели основные
компоненты процессорного модуля, включая определения команд и регистров, токены, а также использование встроенного
в Ghidra языка SLEIGH для создания, модификации и расширения процессорных модулей. Если у вас имеется желание
(или необходимость) добавить новый процессор в Ghidra, то мы
настоятельно рекомендуем посмотреть на процессоры, которые
были добавлены недавно (особенно хорошо документирован
файл SuperH4.sinc, да и сам этот процессор намного проще,
чем x86).
Невозможно переоценить роль терпения и склонности к экспериментам при разработке процессорного модуля. Трудная
работа с лихвой окупается, когда удается применить свой процессорный модуль к каждому новому двоичному файлу, поступающему на рассмотрение, и, быть может, включить модуль
в Ghidra, чтобы им могли воспользоваться другие специалисты
по обратной разработке.
В следующей главе мы займемся декомпилятором Ghidra.

19

ДЕКОМПИЛЯТОР GHIDRA

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

АНАЛИЗ С ПОМОЩЬЮ
ДЕКОМПИЛЯТОРА
Логично предположить, что содержимое окна декомпилятора
получено на основе окна листинга, но, как ни странно, оба окна
вычисляются независимо, поэтому иногда не согласуются друг
с другом. Таким образом, пытаясь определить истину, следует оценивать их содержимое в контексте. Основная функция

декомпилятора Ghidra – преобразовать команды машинного
языка в p-код (см. главу 18), а затем преобразовать p-код в C
и представить в окне декомпилятора.
Упрощенно процесс декомпиляции состоит из трех этапов.
На первом этапе декомпилятор использует файл спецификации на языке SLEIGH, чтобы создать эскиз p-кода и вывести
соответствующие простые блоки и потоки управления. Цель
второго этапа – упрощение: ненужные части, например недостижимый код, исключаются, а потоки управления корректируются в соответствии с изменениями. И на последнем этапе
добавляются завершающие штрихи, производятся некоторые
проверки, и результат прогоняется через алгоритм форматирования, после чего представляется в окне декомпилятора. Конечно, это очень упрощенное описание весьма сложного процесса, но кое-какие выводы все же можно сделать1:
► декомпилятор – это анализатор;
► он получает на входе двоичный файл и порождает p-код;
► он преобразует p-код в C;
► код на C и пояснительные сообщения отображаются
в окне декомпилятора.
Некоторые из этих шагов мы обсудим более подробно, когда будем рассматривать функциональность декомпиляции в Ghidra.
Начнем с процесса анализа и его основных возможностей.

Параметры анализа
Существует несколько анализаторов, относящихся к окну декомпилятора и вызываемых в процессе автоматического анализа. Параметры анализа задаются с помощью пункта меню
EditTool Options, они показаны на рис. 19.1, где представлены значения по умолчанию.
Далее мы обсудим два параметра, Eliminate unreachable
code (Исключать недостижимый код) и Simplify predication
(Упростить предикаты). Что касается остальных, то можете поэкспериментировать сами или обратиться к справке по Ghidra.
1

572

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

ГЛАВА 19

Рис. 19-1. Параметры анализа декомпилятора Ghidra, заданы
значения по умолчанию

Исключение недостижимого кода
Параметр Eliminate unreachable code позволяет исключить
недостижимый код из листинга декомпилятора. Например,
в следующей функции на C есть два условия, которые заведомо никогда не выполняются, из-за чего соответствующие условные блоки недостижимы:
int demo_unreachable(volatile int a) {
volatile int b = a ^ a;
 if (b) {
printf("Недостижим\n");
a += 1;
}
 if (a - a > 0) {
printf("Тоже недостижим\n");
a += 1;
} else {
printf("Мы всегда должны видеть это\n");
a += 2;
}
printf("Конец demo_unreachable()\n");
return a;
}

Переменная b инициализируется нулем, хотя и не вполне очевидным способом. При проверке b  ее значение никак не моДекомпилятор Ghidra

573

жет оказаться отличным от нуля, поэтому тело соответствующего блока if никогда не выполняется. Аналогично a - a не
может быть больше нуля, и условие во втором предложении
if  также не может быть равно true. Если флажок Eliminate
unreachable code отмечен, то в окне декомпилятора появляются предупреждения о том, что недостижимый код был удален.
/* WARNING: Removing unreachable block (ram,0x00100777) */
/* WARNING: Removing unreachable block (ram,0x0010079a) */
ulong demo_unreachable(int param_1)
{
puts("Мы всегда должны видеть это");
puts("Конец demo_unreachable()");
return (ulong)(param_1 + 2);
}

Упрощение предикатов
Этот параметр оптимизирует блоки if/else, объединяя блоки
с общим условием. В листинге ниже условия в первых двух
предложениях if одинаковы:
int demo_simppred(int a) {
if (a > 0) {
printf("A > 0\n");
}
if (a > 0) {
printf("Да, A определенно > 0!\n");
}
if (a > 2) {
printf("A > 2\n");
}
return a * 10;
}

Если флажок Simplify predication отмечен, то в окне декомпилятора блоки будут объединены:
ulong demo_simppred(int param_1)
{
if (0 < param_1) {

574

ГЛАВА 19

puts("A > 0");
puts("Да, A определенно > 0!");
}
if (2 < param_1) {
puts("A > 2");
}
return (ulong)(uint)(param_1 * 10);
}

ОКНО ДЕКОМПИЛЯТОРА
Теперь, поняв, как заполняется окно декомпилятора, посмот­
рим, как им можно воспользоваться для анализа. Навигация по окну декомпилятора сравнительно проста, потому что
в каждый момент времени в нем отображается только одна
функция. Чтобы перемещаться между функциями или посмот­
реть на функцию в контексте, полезно сопоставлять код с окном листинга. Поскольку окна декомпилятора и листинга по
умолчанию связаны, можно перемещаться в обоих, используя
подходящие средства на панели инструментов браузера кода.
Функция, отображаемая в окне декомпилятора, помогает
при анализе, но читать ее с непривычки трудно. Отсутствие
информации о типах данных в функциях заставляет Ghidra
выводить эти типы самостоятельно. Поэтому возможно чрезмерное использование приведения типов, что видно на примере следующих предложений:
printf("a=%d, b=%d, c=%d, d=%d, e=%d, f=%d, g=%d\n", (ulong)param_1,
(ulong)param_2,(ulong)uVar1,(ulong)uVar2,(ulong)(uVar1 + param_1),
(ulong)(uVar2 * 100),(ulong)uVar4);
uStack44 = *(undefined4 *)**(undefined4 **)(iStack24 + 0x10);

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

575

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

Пример 1: редактирование в окне
декомпилятора
Рассмотрим программу, которая принимает от пользователя
два целых числа и вызывает следующую функцию:
int do_math(int a, int b) {
int c, d, e, f, g;
srand(time(0));
c = rand();
printf("c=%d\n", c);
d = a + b + c;
printf("d=%d\n", d);
e = a + c;
printf("e=%d\n", e);
f = d * 100;
printf("f=%d\n", f);
g = rand() - e;
printf("g=%d\n", g);
printf("a=%d, b=%d, c=%d, d=%d, e=%d, f=%d, g=%d\n", a, b, c, d, e,
f, g);
return g;
}

В функции используется два целых параметра и пять локальных переменных. Ниже описаны взаимозависимости между ними:
► переменная c зависит от значения, возвращенного rand(),
влияет на d и e непосредственно, а на f и g опосредованно;
► переменная d зависит от a, b и c и влияет на f непосредственно;
► переменная e зависит от a и c и влияет на g непосредственно;
576

ГЛАВА 19

► переменная f зависит от d непосредственно, а от a, b и
c опосредованно и ни на что не влияет;
► переменная g зависит от e непосредственно, а от a и c опо­
средованно и ни на что не влияет.
После загрузки соответствующего двоичного файла в Ghidra
и завершения анализа в окне декомпилятора появляется следующее представление функции do_math:
ulong do_math(uint param_1,uint param_2)
{
uint uVar1;
uint uVar2;
int iVar3;
uint uVar4;
time_t tVar5;
tVar5 = time((time_t *)0x0);
srand((uint)tVar5);
uVar1 = rand();
printf("c=%d\n");
uVar2 = uVar1 + param_1 + param_2;
printf("d=%d\n");
printf("e=%d\n");
printf("f=%d\n");
iVar3 = rand();
uVar4 = iVar3 - (uVar1 + param_1);
printf("g=%d\n");
printf("a=%d, b=%d, c=%d, d=%d, e=%d, f=%d, g=%d\n", (ulong)param_1,

(ulong)param_2,(ulong)uVar1,(ulong)uVar2,(ulong)(uVar1 +
param_1),

(ulong)(uVar2 * 100),(ulong)uVar4);
return (ulong)uVar4;
}

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

577

Переопределение сигнатур функций
Мы видим ряд обращений к функции printf , которые выглядят не вполне правильно. В каждом имеется форматная
строка, но нет дополнительных аргументов. Поскольку printf
принимает переменное число аргументов, мы можем переопределить сигнатуру функции в каждом месте вызова и (исходя из
форматной строки) указать, что printf должна принимать один
целый аргумент1. Чтобы внести такое изменение, щелкните
правой кнопкой мыши по слову printf и выберите из контекстного меню команду Override Signature (Переопределить сигнатуру) – откроется диалоговое окно, показанное на рис. 19.2.

Рис. 19.2. Диалоговое окно переопределения сигнатуры
Добавление типа второго параметра, int, в сигнатуру (как
показано на рисунке) для каждого вызова printf приводит
к следующему листингу:
ulong do_math(uint param_1,uint param_2)
{
 uint uVar1;
uint uVar2;
uint uVar3;
int iVar4;
uint uVar5;
time_t tVar6;
tVar6 = time((time_t *)0x0);
srand((uint)tVar6);
1

578

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

ГЛАВА 19

uVar1 = rand();
printf("c=%d\n",uVar1);
uVar2 = uVar1 + param_1 + param_2;
printf("d=%d\n",uVar2);
 uVar3 = uVar1 + param_1;
printf("e=%d\n",uVar3);
printf("f=%d\n",uVar2 * 100);
iVar4 = rand();
 uVar5 = iVar4 - uVar3;
printf("g=%d\n",uVar5);
 printf("a=%d, b=%d, c=%d, d=%d, e=%d, f=%d, g=%d\n", (ulong)param_1,

(ulong)param_2,(ulong)uVar1,(ulong)uVar2,(ulong)(uVar1 +
param_1),

(ulong)(uVar2 * 100),(ulong)uVar4);
return (ulong)uVar4;
}

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

Изменение типов и имен переменных
Исправив обращения к функциям, мы можем продолжить расчистку листинга, переименовав (клавиша L) и изменив типы
(клавиша Ctrl-L) параметров и переменных , исходя из имен,
встречающихся в форматных строках printf. Кстати говоря,
форматные строки – весьма ценный источник информации
о типах и назначении переменных программы.
Но даже после внесения этих изменений результирующее
предложение printf  все равно выглядит громоздко:
printf("a=%d, b=%d, c=%d, d=%d, e=%d, f=%d, g=%d\n", (ulong)a,
(ulong)(uint)b, (ulong)(uint)c, (ulong)(uint)d, (ulong)(uint)e,
(ulong)(uint)(d * 100),(ulong)(uint)g);

Щелкнув по нему правой кнопкой мыши, мы сможем переопределить сигнатуру функции. Первым аргументом printf
Декомпилятор Ghidra

579

является форматная строка, ее трогать не нужно. А изменение
типов остальных аргументов на int дает более чистый код (лис­
тинг 19.1).
int
{






do_math(int a, int b)
int c;
int d;
int e;
int g;
time_t tVar1;

tVar1 = time((time_t *)0x0);
srand((uint)tVar1);
c = rand();
printf("c=%d\n",c);
d = c + a + b;
printf("d=%d\n",d);
e = c + a;
printf("e=%d\n",e);
printf("f=%d\n",d * 100);
g = rand();
g = g - e;
printf("g=%d\n",g);
printf("a=%d, b=%d, c=%d, d=%d, e=%d, f=%d, g=%d\n",a,b,c,d,e,d *
100,g);
return g;
}

Листинг 19.1. Декомпилированная функция после изменения
сигнатур
Это уже очень похоже на оригинальный исходный код и читается куда легче первоначального листинга декомпилятора,
поскольку модификации аргументов функции распространились на весь текст. Одно из отличий листинга декомпилятора
от нашего исходного кода состоит в том, что переменная f заменена эквивалентным выражением .

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

ГЛАВА 19

них. Программной областью влияния (slice) называется совокупность предложений, которые влияют на значение переменной (обратная область влияния) или подвержены влиянию
переменной (прямая область влияния). При анализе уязвимостей вопрос может ставиться так: «Я получил контроль над этой
переменной; а где используется ее значение?»
Ghidra предлагает пять пунктов в контекстном меню для выявления связей между переменными и командами в функции.
Щелкнув правой кнопкой мыши в окне декомпилятора, вы
сможете выбрать один из следующих пунктов.
Highlight Def-use (Подсветить использование). Подсвечиваются все случаи использования переменной в функции (для достижения того же эффекта можно воспользоваться средней кнопкой мыши).
Highlight Forward Slice (Подсветить прямую область
влияния). Подсвечивается все, на что влияет значение выбранной переменной. Например, если выбрана переменная b в листинге 19.1, то этот пункт подсветит все вхождения b и d в листинг, потому что изменение b может привести
также к изменению d.
Highlight Backward Slice (Подсветить обратную область
влияния). Это противоположность предыдущего пункта –
подсвечиваются все переменные, оказывающие влияние на
выбранное значение. Если щелкнуть правой кнопкой мыши
по переменной e в последнем обращении к printf в листинге 19.1 и выбрать этот пункт, то будут подсвечены все переменные, влияющие на значение e (в данном случае – e, a и c).
Изменение a или c также могло бы отразиться на значении e.
Highlight Forward Inst Slice (Подсветить расширенную прямую область влияния). Подсвечивается все предложение, ассоциированное с пунктом Highlight Forward
Slice. В листинге 19.1 использование этого пункта при выбранной переменной b привело бы к подсветке всех предложений, в которых встречается b или d.
Highlight Backward Inst Slice (Подсветить расширенную
обратную область влияния). Подсвечивается все предложение, ассоциированное с пунктом Highlight Backward
Декомпилятор Ghidra

581

Slice. В листинге 19.1 использование этого пункта при
выбранной переменной e в последнем обращении к printf
привело бы к подсветке всех предложений, в которых встречается a, c или e.
Освоив некоторые приемы работы с окном декомпилятора
и его применение в анализе, рассмотрим конкретный пример.

Пример 2: функции, не возвращающие
управление
Вообще говоря, Ghidra может предполагать, что функции возвращают управление, и считать, что вызов функции – пример последовательного потока. Однако некоторые функции,
например помеченные ключевым словом noreturn в исходном
коде или заканчивающиеся обфусцированной командой перехода во вредоносной программе, не возвращаются, из-за чего
Ghidra может сгенерировать неверный дизассемблированный
или декомпилированный код. Для обращения с такими функциями Ghidra предлагает три подхода: два анализатора функций, не возвращающих управление, и возможность редактировать сигнатуры функций вручную.
Ghidra может идентифицировать функции, не возвращающие управление, на основе списка известных noreturnфункций, например exit и abort; для этого применяется анализатор Non-Returning Functions-Known. Этот анализатор по
умолчанию подключается к автоматическому анализу, а цель
его ясна: если функция встречается в его списке, она помечается как не возвращающая управление, и анализатор делает все
возможное, чтобы исправить возможные ошибки (например,
сделать соответствующие вызовы не возвращающими управление, найти потоки, которые следует подправить, и т. д.).
Другой анализатор, Non-Returning Functions-Discovered,
ищет признаки, по которым можно было определить, что функция не возвращается (например, данные или бессмысленные
команды после вызова). Что делать с этой информацией, диктуют три параметра, показанных на рис. 19.3.

582

ГЛАВА 19

Рис. 19.3. Параметры анализатора Non-Returning FunctionsDiscovered
Первый параметр  позволяет автоматически создавать закладки во время анализа (они отображаются в полосе закладок
в окне листинга). Второй параметр  позволяет задать порог,
по достижении которого функция считается не возвращающей
управление, в результате серии проверок на наличие характеристик, свойственных таким функциям. Наконец, флажок 
говорит, что нужно исправлять ошибки в потоках управления,
связанных с такими функциями.
Если Ghidra не в состоянии идентифицировать функцию,
не возвращающую управление, то вы можете отредактировать
ее сигнатуру самостоятельно. Если по завершении анализа
остались закладки, помечающие плохие команды, значит,
с анализом, выполненным Ghidra, что-то не так. Если плохая
команда следует за командой CALL, например:
00100839
0010083e

CALL
??

noReturnA
FFh

то, скорее всего, вы увидите в окне декомпилятора заключительный комментарий с предупреждением:
noReturnA(1);
/* WARNING: Bad instruction - Truncating control flow here */
halt_baddata();
Декомпилятор Ghidra

583

Щелкнув по имени функции (в данном случае – noReturnA)
в окне декомпилятора и выбрав пункт Edit Function Signature
(Редактировать сигнатуру функции), вы сможете изменить
атрибуты функции, как показано на рис. 19.4.

Рис. 19.4. Редактирование атрибутов функции
Отметьте флажок No Return (Не возвращается), чтобы пометить функцию как не возвращающую управление. Тогда
Ghidra вставит предварительный комментарий в окне декомпилятора и заключительный комментарий в окне листинга:
/* WARNING: Subroutine does not return */
noReturnA(1);

Исправив эту ошибку, можно переходить к другим проблемам.

Пример 3: автоматизированное создание
структуры
В процессе анализа декомпилированного исходного кода на
C можно встретить предложения, которые выглядят как ссылки на поля структуры. Ghidra поможет вам создать структуру
и заполнить ее на основе ссылок, обнаруженных декомпилятором. Разберем пример, начав с исходного кода и первоначального варианта его декомпиляции.
Ниже показан исходный код, в котором определены два
структурных типа и создано по одной глобальной переменной
каждого типа:

584

ГЛАВА 19

 struct s1 {
int a;
int b;
int c;
};
 typedef struct s2 {
int x;
char y;
float z;
} s2_type;
struct s1 GLOBAL_S1;
s2_type GLOBAL_S2;

Структура  содержит элементы одного типа, а структура
 – разных. Исходный код включает также три функции, в одной из которых (do_struct_demo) объявлен локальный экземп­
ляр структуры каждого типа:
void display_s1(struct s1* s) {
printf("Поля s1 = %d, %d и %d\n", s->a, s->b, s->c);
}
void update_s2(s2_type* s, int v) {
s->x = v;
s->y = (char)('A' + v);
s->z = v * 2.0;
}
void do_struct_demo() {
s2_type local_s2;
struct s1 local_s1;
printf("Введите шесть целых чисел: ");
scanf("%d %d %d %d %d %d", (int *)&local_s1, &local_s1.b, &local_s1.c,

&GLOBAL_S1.a, &GLOBAL_S1.b, &GLOBAL_S1.c);
printf("Вы ввели: %d и %d\n", local_s1.a, GLOBAL_S1.a);
display_s1(&local_s1);
display_s1(&GLOBAL_S1);
update_s2(&local_s2, local_s1.a);
}

Декомпилятор Ghidra

585

Декомпилированная версия do_struct_demo приведена в лис­
тинге 19.2.
void do_struct_demo(void)
{
undefined8 uVar1;
uint local_20;
undefined local_1c [4];
undefined local_18 [4];
undefined local_14 [12];
uVar1 = 0x100735;
printf("Введите шесть целых чисел: ");
__isoc99_scanf("%d %d %d %d %d%d", &local_20, local_1c, local_18,

GLOBAL_S1,0x30101c,0x301020,uVar1);
printf("You entered: %d and %d\n",(ulong)local_20,(ulong)GLOBAL_
S1._0_4_);
 display_s1(&local_20);
 display_s1(GLOBAL_S1);
update_s2(local_14,(ulong)local_20,(ulong)local_20);
return;
}

Листинг 19.2. Начальная декомпиляция do_struct_demo
Перейдя к функции display_s1 двойным щелчком по любому
вызову  или , мы увидим в окне декомпилятора такой код:
void display_s1(uint *param_1)
{
printf("Поля s1 = %d, %d и %d\n", (ulong)*param_1,

(ulong)param_1[1],(ulong)param_1[2]);
return;
}

Поскольку мы подозреваем, что аргумент display_s1 может
быть указателем на структуру, попросим Ghidra автоматизировать процесс создания структуры, для чего щелкнем правой
кнопкой мыши по param_1 в списке аргументов функции и выберем из контекстного меню пункт Auto Create Structure (Автоматическое создание структуры). В ответ Ghidra находит все
вхождения param_1, рассматривает все арифметические опера586

ГЛАВА 19

ции с указателем как ссылки на член структуры и автоматически создает новый структурный тип, содержащий поля по каждому из обнаруженных смещений. Это приводит к нескольким
изменениям в листинге декомпилятора:
void display_s1(astruct *param_1)
{
printf("Поля s1 = %d, %d и %d\n",(ulong)param_1->field_0x0,

(ulong)param_1->field_0x4,(ulong)param_1->field_0x8);
return;
}

Тип параметра изменился и стал astruct*, а вызов printf теперь содержит ссылки на поля. В диспетчер типов данных добавлен новый тип, и если задержать мышь над именем структуры, то будут показаны определения полей, как на рис. 19.5.

Рис. 19.5. Автоматически созданные структуры в диспетчере
типов данных
Мы можем изменить тип local_20 и GLOBAL_S1 на astruct,
воспользовавшись командой Retype Variable (Изменить тип
переменной) в контекстном меню. Результаты показаны в лис­
тинге ниже:

Декомпилятор Ghidra

587

void do_struct_demo(void)
{
undefined8 uVar1;
 astruct local_20;
undefined local_14 [12];
uVar1 = 0x100735;
printf("Введите шесть целых чисел: ");
__isoc99_scanf("%d %d %d %d %d %d", &local_20, &local_20.field_0x4,
 &local_20.field_0x8, &GLOBAL_S1, 0x30101c, 0x301020, uVar1);
printf("Вы ввели: %d и %d\n", (ulong)local_20.field_0x0,
 (ulong)GLOBAL_S1.field_0x0);
display_s1(&local_20);
display_s1(&GLOBAL_S1);
update_s2(local_14,(ulong)local_20.field_0x0,(ulong)local_20.field_0x0);
return;
}

Сравнение с листингом 19.2 показывает, что изменился
тип local_20  и добавились ссылки на поля local_20 
и GLOBAL_S1 .
Теперь обратимся к декомпиляции третьей функции, update_s2,
показанной в листинге 19.3.
void update_s2(int *param_1,int param_2)
{
*param_1 = param_2;
*(char *)(param_1 + 1) = (char)param_2 + 'A';
*(float *)(param_1 + 2) = (float)param_2 + (float)param_2;
return;
}

Листинг 19.3. Начальная декомпиляция update_s2
Тот же подход, что и раньше, можно использовать для автоматического создания структуры для param_1. Просто щелк­
ните правой кнопкой мыши по param_1 в функции и выберите
команду Auto Create Structure из контекстного меню.
void update_s2(astruct_1 *param_1,int param_2)
{
param_1->field_0x0 = param_2;

588

ГЛАВА 19

param_1->field_0x4 = (char)param_2 + 'A';
param_1->field_0x8 = (float)param_2 + (float)param_2;
return;
}

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

Рис. 19.6. Определение второй автоматически созданной
структуры в диспетчере типов данных
В этой структуре имеются поля типа int, char, три байта
undefined (вероятно, заполнение, вставленное компилятором)
и поле типа float. Чтобы отредактировать структуру, щелкните
правой кнопкой мыши по astruct_1 и выберите из контекстного
меню команду Edit, которая открывает окно редактора структуры. Если назвать x поле типа int, y поле типа char и z поле
типа float, а затем сохранить изменения, то новые имена появятся в листинге декомпилятора:

Декомпилятор Ghidra

589

void update_s2(astruct_1 *param_1,int param_2)
{
param_1->x = param_2;
param_1->y = (char)param_2 + 'A';
param_1->z = (float)param_2 + (float)param_2;
return;
}

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

РЕЗЮМЕ
Окно декомпилятора, как и окно листинга, предлагает взгляд
на двоичный файл. У каждого окна есть свои сильные и слабые
стороны. Окно декомпилятора дает высокоуровневое представление, помогающее понять общую структуру и назначение одной функции быстрее, чем это можно сделать в окне дизассемб­
лера (особенно если у вас за плечами нет многолетнего опыта
чтения листингов дизассемблера). Окно листинга предлагает
низкоуровневое представление всего двоичного файла, со всеми деталями, но составить на этом основании общую картину
затруднительно.
Декомпилятор Ghidra можно эффективно использовать в сочетании с окном листинга и всеми остальными инструментами обратной разработки, с которыми мы познакомились в этой
книге. Но в конечном итоге только человек определяет, какой
подход лучше для решения конкретной задачи.
В этой главе мы изучали окно декомпилятора и различные
проблемы, связанные с декомпиляцией. Многие из них объясняются широким разнообразием компиляторов и их параметров – все это влияет на результирующий двоичный файл.
В следующей главе мы рассмотрим зависящее от компилятора
и параметров сборки поведение программы, чтобы лучше понять особенности двоичного файла.

590

ГЛАВА 19

20

ЗАВИСИМОСТЬ
ОТ КОМПИЛЯТОРА

Если до сих пор вы честно работали, то теперь
обладаете необходимыми для эффективного использования Ghidra навыками и, что еще важнее,
можете заставить ее покориться вашей воле. Следующий шаг – научиться справляться с теми проб­
лемами, которые подкидывают вам двоичные файлы (а вовсе
не Ghidra). В зависимости от мотивов, заставивших вас пялиться на ассемблерный код, вы либо хорошо знакомы с тем, на что
смотрите, либо понятия не имеете, с чем можете столкнуться.
Если вы денно и нощно изучаете код, скомпилированный gcc
на платформе Linux, то, наверное, привыкли к его стилю, но
можете быть ошарашены, впервые увидев отладочную версию
программы, сгенерированную компилятором Microsoft C/C++.
Если ваша профессия – анализ вредоносного ПО, то не удивимся, если каждый день еще до обеда вы любуетесь на код, созданный компиляторами gcc, clang, Microsoft C++, Delphi и др.
Как и вы, Ghidra больше знакома с одними компиляторами
и меньше с другими, а знакомство с кодом, сгенерированным

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

ВЫСОКОУРОВНЕВЫЕ КОНСТРУКЦИИ
Иногда различия между компиляторами чисто косметические, но
бывает и так, что они весьма значительны. В этом разделе мы
рассмотрим высокоуровневые конструкции языка и покажем, как
сильно выбор компилятора и его параметров может влиять на результат. Начнем с предложений switch и двух механизмов, чаще
всего используемых для выбора ветви case. Затем посмотрим, как
параметры компилятора влияют на генерацию кода типичных
выражений, после чего перейдем к тому, как разные компиляторы реализуют конструкции C++ и инициализируют программу.

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

ГЛАВА 20

лятором для отыскания наилучшего представления таблицы
switch. Эффективнее всего алгоритмы с постоянным временем
работы, например табличный поиск1. На другом конце спектра
находится линейный поиск, когда в худшем случае требуется сравнить переменную switch с каждой меткой case, чтобы
найти совпадение или выбрать ветвь по умолчанию. Такой
алгоритм наименее эффективен2. Двоичный поиск в среднем
гораздо эффективнее линейного, но у него есть свои ограничения – список должен быть отсортирован3.
Чтобы выбрать самую эффективную реализацию конкретного предложения switch, нужно понимать, как распределение
меток влияет на решение компилятора. Если метки идут подряд или близко к тому, как в исходном коде в листинге 20.1,
то компилятор обычно прибегает к табличному поиску, чтобы
сопоставить переменной switch адрес соответствующей ветви
case, – точнее, он использует таблицу переходов.
switch (a) {
/** ПРИМЕЧАНИЕ: тела ветвей case для краткости опущены **/
case 1: /*...*/ break;
case 2: /*...*/ break;
case 3: /*...*/ break;
case 4: /*...*/ break;
case 5: /*...*/ break;
case 6: /*...*/ break;
case 7: /*...*/ break;
case 8: /*...*/ break;
case 9: /*...*/ break;
case 10: /*...*/ break;
case 11: /*...*/ break;
case 12: /*...*/ break;
}

Листинг 20.1. Предложение switch с последовательными
метками
1

2

3

Для любителей анализа алгоритмов скажем, что табличный поиск позволяет найти нужную ветвь за постоянное число операций, не зависящее
от размера пространства поиска. Как вы, наверное, помните, про такие
алгоритмы говорят, что они имеют сложность O(1).
Линейные алгоритмы имеют сложность O(n) и, к счастью, не используются в предложениях switch.
Двоичный поиск имеет сложность O(log n).
Зависимость от компилятора

593

Таблица переходов – это массив указателей, каждый элемент которой содержит адрес перехода. Во время выполнения
нужный переход выбирается из таблицы по индексу. Таблицы
переходов хорошо работают, когда метки расположены плотно,
т. е. в основном являются соседними целыми числами. Компиляторы учитывают это, решая, стоит ли использовать таблицу
переходов. Для любого предложения switch мы можем вычислить минимальное количество элементов в соответствующей
ему таблице переходов по формуле:
num_entries = max_case_value – min_case_value + 1

Тогда плотность, или коэффициент заполнения таблицы
переходов, вычисляется следующим образом:
density = num_cases / num_entries

Если все элементы списка – последовательные целые числа,
то плотность равна 100 процентов (1.0). Наконец, для хранения
таблицы переходов требуется память объемом
table_size = num_entries * sizeof(void*)

Предложение switch со стопроцентной плотностью будет реа­
лизовано с помощью таблицы переходов. Если же плотность
равна, скажем, 30 процентам, то компилятор вряд ли применит
таблицу переходов, потому что в ней придется заводить записи
в том числе для отсутствующих меток, и на них придется 70 процентов всей таблицы. Если num_entries равно 30, то таблица
переходов будет содержать 21 запись для меток, на которые
нет ссылок. В 64-разрядной системе это 168 из 240 байт, выделенных под таблицу; накладные расходы вроде бы не слишком
велики, но если num_entries равно 300, то они составляют уже
1680 байт, а это слишком много для 90 реально существующих
ветвей case. Компилятор, оптимизирующий быстродействие,
может предпочесть таблицу переходов, тогда как компилятор,
оптимизирующий память, скорее, выберет альтернативную реализацию с меньшими затратами по памяти – двоичный поиск.
594

ГЛАВА 20

Двоичный поиск эффективен, когда метки разбросаны (плотность низкая), как в листинге 20.2 (плотность 0.0008)1. Поскольку двоичный поиск применим только к отсортированным спис­
кам, компилятор должен предварительно упорядочить метки
и начать поиск с медианного значения. В результате порядок
блоков case в листинге дизассемблера может отличаться от их
порядка в исходном коде2.
switch (a) {
/** ПРИМЕЧАНИЕ:
case 1:
case 211:
case 295:
case 462:
case 528:
case 719:
case 995:
case 1024:
case 8000:
case 13531:
case 13532:
case 15027:
}

тела ветвей case для краткости опущены **/
/*...*/ break;
/*...*/ break;
/*...*/ break;
/*...*/ break;
/*...*/ break;
/*...*/ break;
/*...*/ break;
/*...*/ break;
/*...*/ break;
/*...*/ break;
/*...*/ break;
/*...*/ break;

Листинг 20.2. Пример предложения switch
с непоследовательными метками
В листинге 20.3 показан набросок неитеративного алгоритма двоичного поиска по фиксированному числу констант. Примерно так компилятор реализует предложение switch из лис­
тинга 20.2.

1

2

Для тех, кто любит на досуге анализировать алгоритмы, объясним: это
означает, что для нахождения ветви, соответствующей переменной switch,
нужно не более log2N сравнений, где N – число ветвей в предложении
switch. Сложность такого алгоритма составляет O(log n).
Хотя сложность сортировки очень велика по сравнению со сложностью поиска, важно помнить, что сортировка производится один раз во время компиляции, а поиск – каждый раз, когда во время выполнения встречается
switch.
Зависимость от компилятора

595

if (value < median) {
// value принадлежит процентилю [0-50)
if (value < lower_half_median) {
// value принадлежит процентилю [0-25)
// ... продолжать деление пополам, пока
} else {
// value принадлежит процентилю [25-50)
// ... продолжать деление пополам, пока
}
} else {
// value принадлежит процентилю [50-100)
if (value < upper_half_median) {
// принадлежит процентилю [50-75)
// ... продолжать деление пополам, пока
} else {
// принадлежит процентилю [75-100)
// ... продолжать деление пополам, пока
}
}

не будет найдено value

не будет найдено value

не будет найдено value

не будет найдено value

Листинг 20.3. Неитеративный двоичный поиск по
фиксированному числу констант
Компиляторы часто могут выполнить более точную оптимизацию в диапазонах значений меток. Например, встретив такой набор меток:
label_set = [1, 2, 3, 4, 5, 6, 7, 8, 50, 80, 200, 500, 1000, 5000, 10000]

не особенно агрессивный компилятор может заметить, что
плотность равна 0.0015, и применить двоичный поиск ко всем
15 случаям. Но более агрессивный компилятор может сгенерировать таблицу переходов для меток 1–8, а к остальным применить двоичный поиск, добившись тем самым оптимальной
производительности в более чем половине случаев.
Прежде чем рассматривать дизассемблированные версии
листингов 20.1 и 20.2, взглянем на окна графов функций для
этих листингов, показанные бок о бок на рис. 20.1.

596

ГЛАВА 20

Рис. 20.1. Окна графов функций для примеров предложения switch
На левом графе, соответствующем листингу 20.1, мы видим
вертикальную стопку блоков case. Глубина вложенности всех блоков одинакова, что естественно для ветвей предложения switch.
Структура стопки наводит на мысль, что мы можем быстро выбрать любой блок по индексу (вспомните о доступе к массиву).
Именно так работает решение с таблицей переходов, и левый
граф визуально подтверждает, что именно оно реализовано, –
даже на результат дизассемблирования смотреть не нужно.
Правый граф можно понять, только глядя на результат дизассемблирования листинга 20.2. Из-за отсутствия таблицы переходов гораздо труднее идентифицировать здесь предложение
switch. То, что мы видим, – это визуальное представление switch
с помощью вложенного расположения кода Ghidra. Такое расЗависимость от компилятора

597

положение графов функций применяется в Ghidra по умолчанию и служит для представления структуры потока управления
в программе. Горизонтальное ветвление предполагает условное
выполнение (if/else) взаимно исключающих альтернатив. Вертикальная симметрия наводит на мысль о том, что альтернативные пути выполнения были тщательно сбалансированы, чтобы
в каждой вертикальной половине графа было примерно одинаковое число блоков. Наконец, протяженность графа по горизонтали – индикатор глубины поиска, которая, в свою очередь,
определяется общим числом меток case в предложении switch.
В случае двоичного поиска эта глубина всегда будет иметь порядок log2(num_cases). Сходство между отступами в графическом
представлении и в алгоритме в листинге 20.3 бросается в глаза.
Теперь переключимся на окно декомпилятора. На рис. 20.2
показана частичная декомпиляция функций рис. 20.1. Слева –
декомпилированная версия листинга 20.1. Как и в случае графа, наличие таблицы переходов в двоичном файле помогает
Ghidra распознать предложение switch.
Справа показана декомпилированная версия листинга 20.2.
Декомпилятор представил предложение switch в виде вложенной конструкции if/else, согласованной с двоичным поиском
и структурно похожей на листинг 20.3. Мы видим, что первое
сравнение производится со значением 719, медианным в спис­
ке, а каждое последующее сравнение делит пространство поиска пополам. Обратившись к рис. 20.1 (а также к листингу 20.3),
мы снова замечаем, что графические представления функций
очень похожи на структуру отступов в окне декомпилятора.
Теперь, понимая, что происходит на верхнем уровне, заглянем внутрь двоичного файла и посмотрим, что там на нижнем
уровне. Поскольку в этой главе наша цель – изучить различие
между компиляторами, мы представим этот пример как серию
сравнений двух компиляторов: gcc и Microsoft C/C++1.
1

598

принимает много аргументов в командной строке, и каждый может
повлиять на сгенерированный в итоге код. Чтобы с чего-то начать, мы откомпилировали этот пример с такими флагами: . В случае
Microsoft C/C++ использовалась немодифицированная отладочная сборка
для x86. Дополнительные параметры будут описаны в последующих примерах.

ГЛАВА 20

Рис. 20.2. Примеры декомпилированных Ghidra предложений
switch

Пример: сравнение компиляторов gcc
и Microsoft C/C++
В этом примере мы сравним два 32-разрядных двоичных файла
для процессора x86, сгенерированных по коду в листинге 20.1
двумя разными компиляторами. Мы попробуем идентифицировать компоненты предложения switch в каждом файле, найти соответствующие таблицы переходов и отметим существенные различия между файлами. Начнем с относящихся к switch
компонентам в двоичном файле, созданном gcc.
0001075a CMP  dword ptr [EBP + value],12
0001075e JA switchD_00010771::caseD_0 
00010764 MOV EAX,dword ptr [EBP + a]
00010767 SHL EAX,0x2
0001076a ADD EAX,switchD_00010771::switchdataD_00010ee0 = 00010805
0001076f MOV EAX,dword ptr [EAX]=>->switchD_00010771::caseD_0 = 00010805
switchD_00010771::switchD
Зависимость от компилятора

599

00010771 JMP EAX
switchD_00010771::caseD_1 
XREF[2]:
00010771(j), 00010ee4(*)
00010773 MOV EDX,dword ptr [EBP + a]
00010776 MOV EAX,dword ptr [EBP + b]
00010779 ADD EAX,EDX
0001077b MOV dword ptr [EBP + result],EAX
0001077e JMP switchD_00010771::caseD_0
;--остальные ветви опущены- switchD_00010771::switchdataD_00010ee0 XREF[2]: switch_version_1:0001076a(*),

switch_version_1:0001076f(R)
00010ee0 addr switchD_00010771::caseD_0 
00010ee4 addr switchD_00010771::caseD_1
00010ee8 addr switchD_00010771::caseD_2
00010eec addr switchD_00010771::caseD_3
00010ef0 addr switchD_00010771::caseD_4
00010ef4 addr switchD_00010771::caseD_5
00010ef8 addr switchD_00010771::caseD_6
00010efc addr switchD_00010771::caseD_7
00010f00 addr switchD_00010771::caseD_8
00010f04 addr switchD_00010771::caseD_9
00010f08 addr switchD_00010771::caseD_a
00010f0c addr switchD_00010771::caseD_b
00010f10 addr switchD_00010771::caseD_c

Ghidra распознает проверку границ switch , таблицу переходов  и отдельные блоки case по значению, например блок
switchD_00010771::caseD_1 . Компилятор сгенерировал таблицу переходов с 13 записями, хотя в листинге 20.1 всего 12 блоков
case. Дополнительный блок, case 0 (первая запись  в таблице
переходов), разделяет конечный адрес со всеми значениями вне
диапазона 1–12. Иными словами, case 0 – часть ветви default.
Может показаться, что отрицательные числа не входят в ветвь
default, но последовательность команд CMP, JA озна­чает, что
сравниваются значения без знака; таким образом, –1 (0xFFFFFFFF)
рассматривается как 4294967295, а это значение намного больше 12, поэтому исключается из допустимого диапазона индексов
таблицы переходов. Команда JA направляет все такие случаи на
ветвь default: switchD_00010771::caseD_0.
Поняв, как устроен код, сгенерированный gcc, обратимся
к тем же компонентам в коде, сгенерированном компилятором
Microsoft C/C++ в отладочном режиме:

600

ГЛАВА 20

00411e88 MOV ECX,dword ptr [EBP + local_d4]
00411e8e SUB  ECX,0x1
00411e91 MOV dword ptr [EBP + local_d4],ECX
00411e97 CMP  dword ptr [EBP + local_d4],11
00411e9e JA switchD_00411eaa::caseD_c
00411ea4 MOV EDX,dword ptr [EBP + local_d4]
switchD_00411eaa::switchD
00411eaa JMP dword ptr [EDX*0x4 + ->switchD_00411eaa::caseD = 00411eb1
switchD_00411eaa::caseD_1 XREF[2]: 00411eaa(j), 00411f4c(*)
00411eb1 MOV EAX,dword ptr [EBP + param_1]
00411eb4 ADD EAX,dword ptr [EBP + param_2]
00411eb7 MOV dword ptr [EBP + local_c],EAX
00411eba JMP switchD_00411eaa::caseD_c
;--остальные ветви опущены- switchD_00411eaa::switchdataD_00411f4c XREF[1]: switch_version_1:00411eaa(R)
00411f4c addr switchD_00411eaa::caseD_1 
00411f50 addr switchD_00411eaa::caseD_2
00411f54 addr switchD_00411eaa::caseD_3
00411f58 addr switchD_00411eaa::caseD_4
00411f5c addr switchD_00411eaa::caseD_5
00411f60 addr switchD_00411eaa::caseD_6
00411f64 addr switchD_00411eaa::caseD_7
00411f68 addr switchD_00411eaa::caseD_8
00411f6c addr switchD_00411eaa::caseD_9
00411f70 addr switchD_00411eaa::caseD_a
00411f74 addr switchD_00411eaa::caseD_b
00411f78 addr switchD_00411eaa::caseD_c

Здесь переменная switch (в данном случае – local_d4) уменьшается на 1 , чтобы сдвинуть диапазон допустимых значений в 0–11 , устранив тем самым необходимость в фиктивной записи таблицы для значения 0. Поэтому первая запись
(с индексом 0) в таблице переходов  на самом деле относится
к коду в ветви case 1.
Еще одно, более тонкое различие между двумя листингами – местоположение таблицы переходов в файле. Компилятор
gcc помещает таблицы переходов switch в секцию постоянных
данных (.rodata) двоичного файла, логически отделяя код, связанный с предложением switch, от данных, необходимых для
реализации таблицы переходов. А компилятор Microsoft C/
C++ вставляет таблицы переходов в секцию .text, сразу после
функции, содержащей код предложения switch. Местоположение таблицы переходов не оказывает никакого влияния на поЗависимость от компилятора

601

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

ПАРАМЕТРЫ КОМПИЛЯТОРА
Компилятор преобразует высокоуровневый код в эквивалентный низкоуровневый. Разные компиляторы решают одну и ту
же задачу по-разному. Кроме того, даже один компилятор может решать задачу по-разному в зависимости от заданных параметров. В этом разделе мы рассмотрим ассемблерный код, генерируемый разными компиляторами при разных параметрах
(иногда различия имеют понятные объяснения, а иногда нет).
Microsoft Visual Studio может строить отладочную или выпускную версию программы1. Чтобы понять, чем различаются эти версии, сравним параметры, задаваемые для создания
каждой. Выпускная версия обычно оптимизирована, а отладочная нет. С другой стороны, в отладочную версию включается дополнительная информация о символах, и она компонуется с отладочными версиями библиотек2. Отладочные символы
позволяют отладчикам отображать команды языка ассемблера на соответствующий им исходный код и определять имена
локальных переменных (в противном случае эта информация
в процессе компиляции теряется). Отладочные версии библио­
тек Microsoft также скомпилированы с включением отладоч1

2

602

Другие компиляторы, например gcc, тоже умеют вставлять отладочные
символы в сгенерированный код.
Оптимизация обычно включает исключение избыточности кода или выбор
более быстрых, пусть даже потенциально больших по объему последовательностей команд в зависимости от того, чего хочет разработчик: создать
более быструю или меньшую по размеру программу. Анализировать оптимизированный код труднее, чем неоптимизированный, поэтому на этапе
разработки и отладки лучше отключать оптимизацию.

ГЛАВА 20

ных символов, выключенной оптимизацией и дополнительными проверками правильности параметров.
Отладочная сборка проекта Visual Studio, дизассемблированная
Ghidra, сильно отличается от выпускной. Это результат параметров
компилятора и компоновщика, задаваемых только для отладочной
сборки, например флага /RTCx, который вставляет дополнительный
код проверки в результирующий двоичный файл1. Перейдем к рассмотрению некоторых отличий в дизассемблированном коде.

Пример 1: оператор деления по модулю
Начнем с простой математической операции – деления по модулю. В листинге ниже показан исходный код программы, единственная задача которой – принять целое число от пользователя и продемонстрировать работу оператора деления по модулю.
int




}

main(int argc, char **argv) {
int x;
printf("Введите целое число: ");
scanf("%d", &x);
printf("%d %% 10 = %d\n", x, x % 10);

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

Деление по модулю: Microsoft C/C++ Win x64 Debug
Ниже показано, какой код генерирует Visual Studio в отладочном режиме:
1400119c6
1400119c9
1400119ca
1400119cf
1400119d1
1400119d3
1400119d6
1400119d9
1400119e0

1

MOV
CDQ
MOV
IDIV 
MOV
MOV 
MOV
LEA
CALL

EAX,dword ptr [RBP + local_f4]
ECX,0xa
ECX
EAX,EDX
R8D,EAX
EDX,dword ptr [RBP + local_f4]
RCX,[s_%d_%%_10_=_%d_140019d60]
printf

См. https://docs.microsoft.com/en-us/cpp/build/reference/rtc-run-time-error-checks.
Зависимость от компилятора

603

Бесхитростная команда x86 IDIV  оставляет частное
в регист­ре EAX, а остаток от деления в регистре EDX. Затем результат перемещается в младшие 32 бита регистра R8 (R8D) ,
который передается третьим аргументом функции printf.

Деление по модулю: Microsoft C/C++ Win x64
Release
В выпускных версиях производится оптимизация быстродействия и размера, чтобы добиться максимальной производительности и уменьшить объем занимаемой памяти. Для оптимизации быстродействия авторы компиляторов прибегают
к неочевидным реализациям обычных операций. В листинге
ниже показано, как Visual Studio генерирует ту же самую операцию деления по модулю в выпускной сборке.
140001136
14000113a
14000113f
140001141
140001144
140001147
140001149
14000114c
14000114e
140001151
140001153
140001155
14000115c
14000115f

MOV
MOV
IMUL 
MOV
SAR
MOV
SHR
ADD
LEA
MOV
ADD
LEA
SUB 
CALL 

ECX,dword ptr [RSP + local_18]
EAX,0x66666667
ECX
R8D,ECX
EDX,0x2
EAX,EDX
EAX,0x1f
EDX,EAX
EAX,[RDX + RDX*0x4]
EDX,ECX
EAX,EAX
RCX,[s_%d_%%_10_=_%d_140002238]
R8D,EAX
printf

В этом случае используется умножение  вместо деления,
и после длинной цепочки арифметических операций нечто, что
должно быть результатом деления по модулю, оказывается в регистре R8D  (который, как и раньше, является третьим аргументом printf ). Интуитивно совершенно понятно, правда? Объяснение этого кода мы приведем после следующего примера.

Деление по модулю: gcc для Linux x64
Мы видели, как по-разному может вести себя один и тот же
компилятор при изменении параметров командной строки.
Можно ожидать, что другой компилятор сгенерирует совсем
604

ГЛАВА 20

непохожий код. В листинге ниже показана версия той же операции деления по модулю, сгенерированная gcc, и она не кажется такой уж незнакомой.
00100708
0010070b
00100710
00100712
00100714
00100717
00100719
0010071c
0010071e
00100720
00100723
00100725
00100727
00100729

MOV
MOV
MOV
IMUL 
SAR
MOV
SAR
SUB
MOV
SHL
ADD
ADD
SUB
MOV 

ECX,dword ptr [RBP + x]
EDX,0x66666667
EAX,ECX
EDX
EDX,0x2
EAX,ECX
EAX,0x1f
EDX,EAX
EAX,EDX
EAX,0x2
EAX,EDX
EAX,EAX
ECX,EAX
EDX,ECX

Этот код очень похож на сгенерированный Visual Studio
в выпускном режиме. Мы снова видим умножение  вместо деления, за которым следует цепочка арифметических операций,
оставляющая результат в EDX  (который передается третьим
аргументом printf).
В этом коде для деления производится умножение на обратное число, потому что оборудование выполняет умножение
быст­рее, чем деление. Можно видеть, то умножение реализовано в виде последовательности сложений и арифметических
сдвигов, потому что каждая из этих операций выполняется намного быстрее умножения.
Распознаете ли вы в этом коде деление по модулю 10, зависит
от опыта, терпения и творческих способностей. Если вы встречали подобный код раньше, то, вероятно, вам будем проще понять, что здесь происходит. А иначе можно попробовать выполнить код вручную на нескольких значениях в надежде увидеть
какую-то закономерность. Можно даже потратить время на то,
чтобы вставить этот ассемблерный код в тестовую программу
на C и воспользоваться высокоскоростной генерацией тестовых
данных. Декомпилятор Ghidra может стать еще одним полезным ресурсом для сведения сложных или необычных последовательностей команды к более знакомым эквивалентам на C.
Зависимость от компилятора

605

В качестве последнего средства (и не надо этого стыдиться) можно поискать ответ в интернете. Но что именно искать?
Обычно наиболее релевантные результаты получаются, если
задать как можно более специфичный вопрос, а самым специ­
фичным в этом коде является константа 0x66666667. Все три
верхних ответа на этот запрос полезны, но особенно заслуживает помещения в закладки документ по адресу http://flaviojslab.
blogspot.com/2008/02/integer-division.html. Уникальные константы довольно часто используются в криптографических
алгоритмах, и быстрого поиска в интернете может оказаться
вполне достаточно, чтобы понять, какая именно криптографическая подпрограмма у нас перед глазами.

Пример 2: тернарный оператор
Тернарный оператор вычисляет выражение, а затем возвращает один из двух возможных результатов в зависимости от
булева значения выражения. Концептуально тернарный оператор можно считать предложением if/else (и даже заменить
его таким предложением). В следующем намеренно не оптимизированном исходном коде демонстрируется использование
этого оператора:
int



}

main() {
volatile int x = 3;
volatile int y = x * 13;
volatile int z = y == 30 ? 0 : -1;

ПРИМЕЧАНИЕ

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

Что до поведения неоптимизированного кода, то присваивание переменной z  можно было бы заменить следующим
предложением if/else, не изменяя семантики программы:
606

ГЛАВА 20

if (y == 30) {
z = 0;
} else {
z = -1;
}

Посмотрим, как тернарный оператор обрабатывается различными компиляторами при разных параметрах.

Тернарный оператор: gcc для Linux x64
Компилятор gcc без параметров сгенерировал следующий ассемблерный код инициализации z:
00100616 MOV EAX,dword ptr [RBP + y]
00100619 CMP  EAX,0x1e
0010061c JNZ LAB_00100625
0010061e MOV EAX,0x0
00100623 JMP LAB_0010062a
LAB_00100625
00100625 MOV EAX,0xffffffff
LAB_0010062a
0010062a MOV  dword ptr [RBP + z],EAX

Здесь используется реализация в виде if/else. Локальная
переменная y сравнивается с 30 , и в зависимости от результата сравнения в регистр EAX записывается 0 или 0xffffffff,
а затем этот регистр копируется в z .

Тернарный оператор: Microsoft C/C++ Win x64
Release
В Visual Studio предложение, содержащее тернарный оператор, компилируется совершенно по-другому. Компилятор понимает, что для условного генерирования значения 0 или -1
(и никаких других) достаточно одной команды, и эта команда
используется вместо конструкции if/else:
140001013
140001017
14000101a
14000101c
14000101e

MOV
SUB 
NEG 
SBB 
MOV

EAX,dword ptr [RSP + local_res8]
EAX,0x1e
EAX
EAX,EAX
dword ptr [RSP + local_res8],EAX
Зависимость от компилятора

607

Команда SBB  (вычитание с заниманием) вычитает второй
операнд из первого, а затем еще вычитает флаг переноса CF (который может принимать только значения 0 или 1). Арифметическое выражение, эквивалентное команде SBB EAX,EAX, имеет
вид EAX – EAX – CF, что сводится к 0 – CF. Это выражение может
быть равно только 0 (если CF == 0) или -1 (если CF == 1). Чтобы
этот трюк сработал, компилятор должен правильно установить
флаг переноса, перед тем как выполнять команду SBB. Для этого
он сравнивает EAX с константой 0x1e (30) , пользуясь операцией
вычитания, которая оставляет регистр EAX равным 0, только если
первоначально он был равен 0x1e. Затем команда NEG  устанавливает флаг переноса для следующей за ней команды SBB1.

Тернарный оператор: gcc для Linux x64
(с оптимизацией)
Когда мы просим gcc поработать чуть усерднее и оптимизировать код (флаг -O2), результат получается похожим на код, сгенерированный Visual Studio в предыдущем примере:
00100506
0010050a
0010050d
00100510
00100513
00100515

MOV EAX,dword ptr [RSP + y]
CMP EAX,0x1e
SETNZ  AL
MOVZX EAX,AL
NEG  EAX
MOV  dword ptr [RSP + z],EAX

В этом случае gcc использует команду SETNZ , чтобы условно
записать в регистр AL 0 или 1 в зависимости от состояния флага нуля, установленного в результате предыдущей операции
сравнения. Затем результат инвертируется командой NEG
и становится равным 0 или -1, после чего присваивается переменной z .

Пример 3: встраивание функций
Помечая функцию ключевым словом inline, программист
предлагает компилятору заменить все вызовы этой функции
1

608

Команда NEG очищает флаг переноса (CF), если ее операнд равен 0, и устанавливает во всех остальных случаях.

ГЛАВА 20

встраиванием ее тела целиком. Идея в том, чтобы ускорить
работу функции, устранив накладные расходы на инициализацию и очистку кадра стека. Однако из-за наличия многих
копий встраиваемой функции двоичный файл становится
больше. Распознать встраиваемые функции в двоичном файле
очень трудно, потому что отсутствует отличительный признак
функции – команда call.
Даже если ключевое слово inline не использовалось, компилятор может встроить функцию по собственной инициативе. В нашем третьем примере мы обращаемся к следующей
функции:
int

}
int


}

maybe_inline() {
return 0x12abcdef;
main() {
int v = maybe_inline();
printf("после maybe_inline: v = %08x\n", v);return 0;

Вызов функции: gcc для Linux x86
В результате дизассемблирования двоичного файла, собранного в Linux x86 компилятором gcc без оптимизации, мы получаем такой код:
00010775
00010776
00010778
00010779
0001077c
00010781
00010784
00010787
0001078a
0001078f

PUSH
MOV
PUSH
SUB
CALL
MOV
SUB
PUSH
PUSH
CALL

EBP
EBP,ESP
ECX
ESP,0x14
maybe_inline
dword ptr [EBP + local_14],EAX
ESP,0x8
dword ptr [EBP + local_14]
s_after_maybe_inline:_v_=_%08x_000108e2
printf

Вызов  функции maybe_inline хорошо виден, хотя это всего
одна строка кода, возвращающая константу.

Зависимость от компилятора

609

Вызов функции: gcc для Linux x86 с оптимизацией
Теперь посмотрим на оптимизированный (-O2) ассемблерный
код того же исходного файла:
0001058a
0001058b
0001058d
0001058e
00010591
00010596
0001059b
0001059d

PUSH
MOV
PUSH
SUB
PUSH
PUSH
PUSH
CALL

EBP
EBP,ESP
ECX
ESP,0x8
0x12abcdef
s_after_maybe_inline:_v_=_%08x_000108c2
0x1
__printf_chk

В отличие от неоптимизированной версии, вызов maybe_
inline исключен, а константа , возвращенная maybe_inline,
помещается прямо в стек и становится аргументом printf. Эта
оптимизированная версия вызова функции совпадает с тем,
что мы увидели бы, если бы пометили ее как inline.
Рассмотрев, как оптимизация может повлиять на код, сгенерированный компилятором, обратимся к другим способам реализации особенностей языка, которые проектировщики компиляторов
выбирают в тех случаях, когда проектировщики языка оставляют
детали реализации на усмотрение авторов компилятора.

РЕАЛИЗАЦИЯ ЗАВИСЯЩИХ
ОТ КОМПИЛЯТОРА
ОСОБЕННОСТЕЙ C++
Языки программирования проектируются программистами
для программистов. После того как пыль проектирования уляжется, на долю авторов компилятора выпадает построение
инструментов, которые верно транслируют программы, написанные на новом языке высокого уровня, в семантически эквивалентные программы на машинном языке. Если язык разрешает программисту делать A, B и C, то автор компилятора
должен придумать, как воплотить эти «хотелки» в жизнь.
C++ дает три прекрасных примера обязательных свойств
языка, детали реализации которых оставлены на усмотрение
автора компилятора:
610

ГЛАВА 20

► внутри нестатической функции-члена класса программист может обращаться к переменной this, которая никогда и нигде не объявляется явно (см. главы 6 и 8 о том,
как компиляторы обращаются с this);
► разрешена перегрузка функций. Программист вправе называть разные функции одним и тем же именем при соблюдении определенных ограничений на списки параметров;
► поддерживается интроспекция типа с помощью операторов dynamic_cast и typeid.

Перегрузка функций
Перегрузка функций в C++ позволяет программисту называть
разные функции одним именем, при условии что у них различаются последовательности параметров. Декорирование имен,
описанное в главе 8, – это внутренний механизм, благодаря которому перегрузка работает; он гарантирует, что никакие два
символа не будут иметь одинаковые имена к тому моменту, как
придет черед поработать компоновщику.
Зачастую один из первых признаков того, что мы работаем
с двоичным файлом программы, написанной на C++, – присутствие декорированных имен. Две самые популярные схемы
декорирования имен – Microsoft и Intel Itanium ABI1. Стандарт
Intel широко используется и в других компиляторах для Unix,
в частности g++ и clang. Ниже показано имя функции C++ и его
декорированный вариант для схем Microsoft и Intel:
Функция void SubClass::vfunc1()
Схема Microsoft ?vfunc1@SubClass@@UAEXXZ
Схема Intel _ZN8SubClass6vfunc1Ev
В большинстве языков, допускающих перегрузку, включая
Objective-C, Swift и Rust, имеется та или иная форма декорирования имен на уровне реализации. Мимолетное знакомство
со стилями декорирования имен может дать ключ к языку, на
котором была написана программа, и к компилятору, сгенерировавшему двоичный файл.
1

См. https://docs.microsoft.com/en-us/cpp/build/reference/decorated-names для
Microsoft и https://itanium-cxx-abi.github.io/cxx-abi/abi.html#mangling для
Intel.
Зависимость от компилятора

611

Реализации RTTI
В главе 8 мы обсуждали механизм идентификации типа во время выполнения (RTTI) в C++, а также отсутствие стандарта реа­
лизации RTTI компилятором. На самом деле идентификация
типа во время выполнения вообще не упоминается в стандарте C++, поэтому неудивительно, что реализации различаются.
Для поддержки оператора dynamic_cast в структурах данных
RTTI нужно хранить не только имя класса, но и всю иерархию наследования, в т. ч. множественного. Найти эти структуры данных очень полезно для понимания объектной модели
программы. Автоматическое распознавание конструкций, относящихся к RTTI, в двоичном файле – еще одна область, где
возможности Ghidra зависят от компилятора.
В программах на Microsoft C++ нет встроенной информации о символах, но структуры данных RTTI хорошо понятны,
и Ghidra находит их, если они присутствуют. Вся относящаяся
к RTTI информация, найденная Ghidra, обобщена в папке Classes
дерева символов; для каждого класса, обнаруженного RTTIанализатором, в этой папке содержится по одному элементу.
Программы, построенные g++, включают информацию о таб­
лице символов, если файл не был зачищен. Для таких незачищенных двоичных файлов Ghidra полагается исключительно
на обнаруженные декорированные имена и ассоциированные
с ними классы. Как и в случае двоичных файлов, созданных
Microsoft C++, вся относящаяся к RTTI информация включается в папку Classes дерева символов.
Чтобы понять, как конкретный компилятор встраивает информацию о типах для классов C++, можно написать простую программу, в которой имеются классы, содержащие виртуальные
функции. После компиляции программы получившийся исполняемый файл можно загрузить в Ghidra и поискать строки, содержащие имена используемых классов. Каким бы компилятором ни была создана программа, у структур данных RTTI есть
одна общая черта – все они так или иначе ссылаются на строку,

612

ГЛАВА 20

содержащую декорированное имя представляемого ими класса.
Используя выделенные строки и перекрестные ссылки на данные, можно найти в двоичном файле потенциальные структуры данных RTTI. Последний шаг – связать кандидата на роль
структуры RTTI с vf-таблицей соответствующего класса, следуя
по перекрестным ссылкам от структуры в обратную сторону, пока
не будет достигнута таблица указателей на функции (искомая vfтаблица). Рассмотрим пример, в котором используется этот метод.

Пример: нахождение информации RTTI в двоичном
файле, созданном g++ для Linux x86-64
Для демонстрации вышеупомянутых концепций мы создали небольшую программу, содержащую классы BaseClass,
SubClass, SubSubClass с виртуальными функциями. В листинге
ниже показана часть главной программы, в которой мы ссылаемся на наши классы и функции.
BaseClass *bc_ptr_2;
srand(time(0));
if (rand() % 2) {
bc_ptr_2 = dynamic_cast(new SubClass());
}
else {
bc_ptr_2 = dynamic_cast(new SubSubClass());
}

Мы откомпилировали программу с помощью g++, создав двоичный файл с символами для 64-разрядной версии Linux. После
анализа программы дерево символов выглядит, как показано
на рис. 20.3.
Папка Classes содержит элементы для всех трех классов нашей программы. В раскрытой ветви класса SubClass мы видим
дополнительную информацию, добытую Ghidra. В зачищенной
версии того же двоичного файла информации будет гораздо
меньше, она показана на рис. 20.4.

Зависимость от компилятора

613

Рис. 20.3. Классы в дереве символов незачищенного двоичного
файла

Рис. 20.4. Классы в дереве символов зачищенного двоичного
файла
В этом случае мы могли – неправильно – предположить, что
двоичный файл вообще не содержит интересных классов C++,
хотя программа, скорее всего, написана на C++, о чем говорит
ссылка на класс basic_ostream из стандартной библиотеки C++.
Поскольку при зачистке удаляется только информация о символах, мы все еще можем найти информацию RTTI, поискав
имена классов в строках программы и проложив путь назад
к структурам данных RTTI. Поиск по строкам дает результаты,
показанные на рис. 20.5.
614

ГЛАВА 20

Рис. 20.5. Поиск по строкам находит имена классов
Щелкнув по строке "8SubClass", мы попадем в следующую
часть окна листинга:
s_8SubClass_00101818
00101818 ds "8SubClass"

XREF[1]: 00301d20(*)

В двоичных файлах, созданных g++, структуры, относящиеся
к RTTI, содержат ссылку на строку с именем соответствующего
класса. Если проследовать по ссылке в первой строке к ее источнику, то мы окажемся в следующей части листинга дизассемблера:
PTR___gxx_personality_v0_00301d18 XREF[2]: FUN_00101241:00101316(*),

00301d10(*)
 00301d18 addr __gxx_personality_v0 = ??
 00301d20 addr s_8SubClass_00101818 = "8SubClass"
00301d28 addr PTR_time_00301d30
= 00303028

Источником перекрестной ссылки  является второе поле
в структуре typeinfo класса SubClass, которая начинается по
адресу 00301d18 . К сожалению, если вы не готовы копаться в исходном коде g++, то определить формат таких структур
можно только экспериментально. Последнее, что осталось сделать, – найти vf-таблицу класса SubClass. В этом примере, если
мы последуем по единственной ссылке на структуру typeinfo,
берущей начало в области данных  (другая ссылка  исходит
из функции и вряд ли может вести на vf-таблицу), то упремся
в тупик. Немного математики – и мы узнаем, чтоперекрестная
Зависимость от компилятора

615

ссылка исходит из адреса, непосредственно предшествующего
структуре typeinfo (00301d18 – 8 == 00301d10). При нормальных
обстоятельствах должна была бы существовать перекрестная
ссылка из vf-таблицы на структуру typeinfo; однако, не располагая символами, Ghidra не может создать эту ссылку. Поскольку мы знаем, что где-то должен быть еще один указатель на
нашу структуру typeinfo, мы можем обратиться к Ghidra за помощью. Поместив курсор в начало структуры , мы можем воспользоваться командой меню SearchFor Direct References
(ИскатьПрямые ссылки), которая просит Ghidra найти для
нас текущий адрес в памяти. Результаты показаны на рис. 20.6.

Рис. 20.6. Результаты поиска прямой ссылки
Ghidra нашла еще две ссылки на эту структуру typeinfo. Рассмотрев обе, мы, наконец, придем к vf-таблице:
 00301c60 ?? 18h ?-> 00301d18
00301c61 ?? 1Dh
00301c62 ?? 30h 0
00301c63 ?? 00h
00301c64 ?? 00h
00301c65 ?? 00h
00301c66 ?? 00h
00301c67 ?? 00h
PTR_FUN_00301c68
XREF[2]: FUN_00101098:001010b0(*),

FUN_00101098:001010bb(*)
 00301c68 addr FUN_001010ea
00301c70 addr FUN_00100ff0
00301c78 addr FUN_00101122
00301c80 addr FUN_00101060
00301c88 addr FUN_0010115a

616

ГЛАВА 20

Ghidra не отформатировала источник  перекрестной ссылки
на typeinfo как указатель (что объясняет отсутствие перекрестной
ссылки), но оставила концевой комментарий с предположением,
что это может быть указатель . Сама vf-таблица начинается
8 байтами ниже  и содержит пять указателей на виртуальные
функции, принадлежащие классу SubClass. В таблице нет декорированных имен, потому что двоичный файл был зачищен.
В следующем разделе мы применим эту технику «поиска по
хлебным крошкам», чтобы найти функцию main в двоичных
файлах, созданных разными компиляторами.

НАХОЖДЕНИЕ ФУНКЦИИ MAIN
С точки зрения программиста, выполнение программы начинается в функции main, поэтому приступать к анализу программы
с этой функции – разумная стратегия. Однако компиляторы
и компоновщики (а также библиотеки) добавляют код, выполняемый еще до входа в main. Таким образом, предположение о том,
что точка входа в двоичный файл совпадает с функцией main,
написанной автором программы, часто оказывается неверным.
На самом деле идея о том, что у любой программы есть функция
main, – всего лишь соглашение компилятора C/C++, а не непреложное правило написания программ. Если вы когда-нибудь
писали приложения с графическим интерфейсом Windows, то,
наверное, знакомы с функцией WinMain – вариацией на тему
main. А отойдя на шаг в сторону от C/C++, вы столкнетесь с языками, в которых главная точка входа называется совсем иначе.
Но мы будем употреблять общее название – «функция main».
Если в двоичном файле имеется символ main, то можно просто
попросить Ghidra доставить вас туда, однако если файл был зачищен, то Ghidra оставит вас на заголовке файла, и искать main
придется самостоятельно. Но, немного понимая, как работает
исполняемый файл, и при наличии какого-никакого опыта эта
задача не выглядит такой уж пугающей.
В любом исполняемом файле должен быть указан адрес первой команды, выполняемой после отображения файла в память. Ghidra называет этот адрес entry или _start в зависимости от типа файла и доступности символов. В большинстве
Зависимость от компилятора

617

форматов исполняемых файлов этот адрес указывается в области заголовка файла, и загрузчики Ghidra точно знают, как
его найти. В ELF-файле адрес точки входа хранится в поле
e_entry, а в PE-файлах – в поле AddressOfEntryPoint. В откомпилированной C-программе независимо от платформы, для
которой она предназначена, по адресу точки входа находится
вставленный компилятором код, осуществляющий переход от
только что созданного процесса к работающей программе. Частью этого перехода является сбор и передача main (в соответствии с соглашением о вызове) аргументов и переменных окружения, предоставленных процессу ядром в момент создания.
Ядро операционной системы знать не знает, на
каком языке был написан исполняемый файл.
Ядру точно известен единственный способ передачи параметров новому процессу, и этот способ
вовсе необязательно совместим с функцией, являющейся точкой входа в программу. Навести мост
через эту пропасть – задача компилятора.

ПРИМЕЧАНИЕ

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

Пример 1: от _start к main с компилятором
gcc для Linux x86-64
Изучая стартовый код в незачищенном исполняемом файле,
мы можем узнать, как достигается main для данной комбинации компилятора и операционной системы. gcc для Linux предлагает довольно простой путь:
_start
004003b0
004003b2
004003b5
004003b6
004003b9
004003bd
004003be
004003bf

618

XOR
MOV
POP
MOV
AND
PUSH
PUSH
MOV

ГЛАВА 20

EBP,EBP
R9,RDX
RSI
RDX,RSP
RSP,-0x10
RAX
RSP=>local_10
R8=>__libc_csu_fini,__libc_csu_fini

004003c6 MOV RCX=>__libc_csu_init,__libc_csu_init
004003cd MOV RDI=>main,main 
004003d4 CALL  qword ptr [->__libc_start_main]

Адрес main загружается в регистр RDI  непосредственно перед
вызовом  библиотечной функции __libc_start_main, т. е. адрес
main передается первым аргументом __libc_start_main. Вооруженные этим знанием, мы легко найдем main в зачищенном двоичном файле. В следующем листинге показано, как выглядит
подготовка к вызову __libc_start_main в зачищенном файле:
004003bf
004003c6
004003cd
004003d4

MOV
MOV
MOV
CALL

R8=>FUN_004008a0,FUN_004008a0
RCX=>FUN_00400830,FUN_00400830
RDI=>FUN_0040080a,FUN_0040080a
qword ptr [->__libc_start_main]

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

Пример 2: от _start к main с компилятором
clang для FreeBSD x86-64
В современных версиях FreeBSD компилятором C по умолчанию является clang, а функция _start побольше, и протрассировать ее труднее, чем простую вставку _start в Linux.
Для простоты воспользуемся декомпилятором Ghidra, чтобы
взглянуть на конец _start.
// ~40 строк для краткости опущено
atexit((__func *)cleanup);
handle_static_init(argc,ap,env);
argc = main((ulong)pcVar2 & 0xffffffff,ap,env);
/* ПРЕДУПРЕЖДЕНИЕ: эта подпрограмма не возвращает управление */
exit(argc);
}

В данном случае main – предпоследняя функция, вызываемая в _start, а возвращенное ей значение сразу же передается
Зависимость от компилятора

619

функции exit, которая завершает программу. Применение декомпилятора Ghidra к зачищенной версии того же файла дает
такой листинг:
// ~40 строк для краткости опущено
atexit(param_2);
FUN_00201120(uVar2 & 0xffffffff,ppcVar5,puVar4);
__status = FUN_00201a80(uVar2 & 0xffffffff,ppcVar5,puVar4) ;
/* ПРЕДУПРЕЖДЕНИЕ: эта подпрограмма не возвращает управление */
exit(__status);
}

И снова мы можем выдернуть main  из толпы, хотя двоичный
файл и был зачищен. Если вам интересно, почему в листинге
присутствуют незачищенные имена двух функций, то это потому, что двоичный файл был скомпонован динамически. Функции atexit и exit – не символы в двоичном файле, а внешние
зависимости, которые остаются даже после зачистки и видны
в декомпилированном коде. Ниже показан соответствующий
код статически скомпонованного зачищенного файла:
FUN_0021cc70();
FUN_0021c120(uVar2 & 0xffffffff,ppcVar13,puVar11);
uVar7 = FUN_0021caa0(uVar2 & 0xffffffff,ppcVar13,puVar11);
/* ПРЕДУПРЕЖДЕНИЕ: эта подпрограмма не возвращает управление */
FUN_00266d30((ulong)uVar7);
}

Пример 3: от _start к main с компилятором
Microsoft’s C/C++
Стартовая вставка, добавляемая компилятором Microsoft C/
C++, несколько сложнее, потому что основной интерфейс с ядром
Windows осуществляется с помощью библиотеки kernel32.dll
(а не libc, как в большинстве Unix-систем), которая не экспортирует никаких библиотечных функций на C. В результате
компилятор часто статически включает многие библиотечные
функции C прямо в исполняемый файл. Стартовая вставка использует эти и другие функции для интерфейса с ядром на этапе инициализации среды выполнения C-программы.
620

ГЛАВА 20

Но в конце концов вставка все-таки должна вызвать main
и завершить процесс после возврата из нее. Поиск main в стартовом коде обычно сводится к идентификации функции с тремя аргументами (main), значение которой передается функции
с одним аргументом (exit). Следующий фрагмент двоичного
файла этого типа содержит вызовы обеих искомых функций.
140001272
140001277
14000127e
140001285
14000128c
140001292
140001297
140001299
14000129d
14000129f
1400012a1
1400012a3

CALL
MOV
MOV
MOV
MOV
CALL
MOV
MOV
TEST
JNZ
MOV
CALL

_amsg_exit 
R8,qword ptr [DAT_14000d310]
qword ptr [DAT_14000d318],R8
RDX,qword ptr [DAT_14000d300]
ECX,dword ptr [DAT_14000d2fc]
FUN_140001060
EDI,EAX
dword ptr [RSP + Stack[-0x18]],EAX
EBX,EBX
LAB_1400012a8
ECX,EAX
FUN_140002b30

Здесь FUN_140001060  – функция с тремя аргументами, на
самом деле main, а FUN_140002b30  – функция exit с одним
аргументом. Заметим, что Ghidra смогла восстановить имя 
одной из статически скомпонованных функций, вызываемых
из стартовой вставки, поскольку она имеется в базе данных
FidDb. Мы можем воспользоваться подсказками в виде идентифицированных символов, чтобы сэкономить немного времени
на поиске main.

РЕЗЮМЕ
Количество зависящих от компилятора особенностей настолько велико, что в одной главе (да и в целой книге, если на то
пошло) их не описать. В частности, компиляторы различаются алгоритмами реализации высокоуровневых конструкций
и способами оптимизации сгенерированного кода. Поскольку
на поведение компилятора сильно влияют аргументы, переданные ему в командной строке, один и тот же компилятор может генерировать совершенно непохожие двоичные файлы из
одного исходного.
Зависимость от компилятора

621

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

Часть V

РЕАЛЬНЫЕ
ПРИЛОЖЕНИЯ

21

АНАЛИЗ
ОБФУСЦИРОВАННОГО КОДА

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

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

ПРОТИВОДЕЙСТВИЕ ОБРАТНОЙ
РАЗРАБОТКЕ
Противодействие обратной разработке, или антиобратная
разработка, – тема, охватывающая все приемы, которыми
разработчики программ пользуются, чтобы затруднить обратную разработку своих изделий. Существует много инструментов и методов, помогающих достичь этой цели, и каждый день
появляются новые. Экосистема RE/анти-RE похожа на битву
между авторами вредоносных программ и производителями
антивирусов.
Любой, кто занимается обратной разработкой, рано или
поздно столкнется с самыми разными препятствиями – от тривиальных до почти непреодолимых. Подходы к их преодолению также сильно зависят от природы мер противодействия
обратной разработке и могут потребовать уверенного владения
методами статического и динамического анализов. В следующих разделах мы обсудим некоторые типичные методы противодействия обратной разработке, причины их использования
и подходы к борьбе с ними.

Обфускация
В словарях слово обфускация (obfuscation) определяется как
действие, направленное на то, чтобы запутать, смутить, ввести
в заблуждение с целью не дать другим людям разобраться
в назначении обфусцированного предмета. В этой книге применительно к Ghidra обфусцированными предметами являются двоичные исполняемые файлы (в отличие, например, от исходного кода или кремниевых кристаллов).
Тема обфускации сама по себе слишком обширна, чтобы считаться только техникой противодействия обратной разработке.
Да и все приемы такого противодействия она не охватывает.
626

ГЛАВА 21

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

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

Рассинхронизация дизассемблера
Одна из довольно старых техник воспрепятствовать процессу
дизассемблирования – нестандартное использование команд
и данных с целью помешать дизассемблеру найти правильный
начальный адрес одной или нескольких команд. Такие помехи
дизассемблеру приводят к невозможности получить листинг
программы или, по крайней мере, к неправильному листингу.
В листинге 21.1 показана попытка Ghidra дизассемблировать
часть инструмента антиобратной разработки Shiva1.

1

За прошедшие годы появилось несколько презентаций, относящихся
к Shiva, первой была следующая: http://cansecwest.com/core03/shiva.ppt.
Анализ обфусцированного кода

627

0a04b0d1 e8 01 00 00 00 CALL FUN_0a04b0d7
0a04b0d6 c7
?? C7h
************************************************************
*
FUNCTION
*
************************************************************
undefined FUN_0a04b0d7()
undefined AL:1
FUN_0a04b0d7
XREF[1]: FUN_0a04b0c4:0a04b0d1(c)
0a04b0d7 58 POP EAX
0a04b0d8 8d 40 0a LEA EAX,[EAX + 0xa]
LAB_0a04b0db+1
XREF[0,1]: 0a04b0db(j)
 0a04b0db eb ff JMP
LAB_0a04b0db+1
0a04b0dd e0 ?? E0h

Листинг 21.1. Пример первоначальной попытки
дизассемблирования Shiva
В этом примере выполняется команда CALL , за которой следует POP . Эта последовательность часто встречается в самомодифицируемом коде и служит для определения того, в каком
месте памяти находится работающий код. Команда CALL  возвращает управление по адресу 0a04b0d6, этот адрес находится
на вершине стека, когда программа доходит до команды POP.
Команда POP извлекает из стека адрес возврата и загружает его
в EAX, а следующая за ней команда LEA  прибавляет к EAX значение 0xa (10), так что теперь EAX содержит 0a04b0e0 (запомните
это значение, оно нам скоро понадобится).
Маловероятно, что вызванная функция когда-нибудь вернется в точку вызова, потому что адреса возврата на вершине
стека уже нет, и Ghidra не может сформировать команду по
адресу возврата , поскольку байт C7h не является началом
какой-либо команды.
До сих пор код может показаться немного необычным и, быть
может, его трудно проследить, но Ghidra хотя бы дизассемблировала его правильно. Однако все меняется по достижении команды JMP . Эта двухбайтовая команда расположена по адресу 0a04b0db, а конечный адрес перехода равен LAB_0a04b0db+1.
С суффиксом +1 мы еще не встречались. Компонент адреса
в метке совпадает с адресом самой метки. А +1 говорит, что
адрес перехода отстоит на 1 байт после LAB_0a04b0db. Иными
словами, переход ведет в середину двухбайтовой команды пере628

ГЛАВА 21

хода. Процессор такая необычная ситуация не настораживает
(он радостно выберет то, на что указывает счетчик команд, чем
бы это ни было), но вот Ghidra не может продолжить работу.
У Ghidra попросту нет средств, чтобы показать байт по адресу
0a04b0db (ff) и как второй байт команды перехода, и как первый
байт следующей за ней команды. В результате Ghidra прерывает дизассемблирование, на что указывает неопределенное значение данных по адресу 0a04b0dd . (Такое поведение характерно не только для Ghidra: практически все дизассемблеры,
не важно, используют они алгоритм рекурсивного спуска или
линейной развертки, становятся жертвой этой техники.)
Ghidra помечает все проблемы, встретившиеся при диз­
ассемблировании, включая в листинг закладки ошибок.
На рис. 21.1 показано две такие закладки (значок X слева от
проблематичного адреса) в левом поле окна листинга. Задержав мышь над закладкой ошибки, мы увидим сообщение, содержащее подробности. Кроме того, можно открыть список всех
закладок в текущем двоичном файле, выбрав из меню пункт
WindowBookmarks (ОкноЗакладки).
По поводу первой ошибки Ghidra сообщает «Unable to resolve
construc­tor at 0a04b0d6 (flow from 0a04b0d1)», приблизительно
это означает: «Я думаю, что по адресу 0a04b0d6 должна быть команда, но не могу ее создать». А по поводу второй Ghidra говорит:
«Failed to disassemble at 0a04b0dc due to conflicting instruction at
0a04b0db (flow from 0a04b0db)», что означает «Не могу дизассемб­
лировать команду, начинающуюся внутри другой команды».

Рис. 21.1. Закладки ошибок
Анализ обфусцированного кода

629

У пользователя Ghidra нет способа исправить первую ошибку. Последовательность байтов либо содержит допустимую команду, либо нет. Но вот со второй, приложив немного усилий,
справиться можно. Чтобы разрешить эту ситуацию, нужно отменить команду, содержащую байты, являющиеся конечным
адресом перехода внутри, и определить новую команду, начинающуюся по этому адресу, попытавшись тем самым ресинхронизировать дизассемблер. Первоначальная команда при этом
будет потеряна, но вы можете оставить комментарий о том, что
в этом месте было. Ниже показана часть предыдущего листинга, содержащая перекрывающиеся команды:
LAB_0a04b0db+1
 0a04b0db eb ff JMP
LAB_0a04b0db+1
0a04b0dd e0 ?? E0h

XREF[0,1]: 0a04b0db(j)

Щелкнув правой кнопкой мыши по команде JMP  и выбрав
из контекстного меню пункт Clear Code Bytes (Очистить байты кода) (клавиша C), мы увидим следующий перечень неопределенных байтов:
0a04b0db eb
 0a04b0dc ff
0a04b0dd e0

??
??
??

EBh
FFh
E0h

Теперь байт, являющийся целью  команды JMP, можно переформатировать. Чтобы преобразовать неформатированные
байты в код, нужно щелкнуть правой кнопкой мыши по начальному байту кода и выбрать пункт Disassemble (клавиша D). Тогда листинг примет такой вид:
 0a04b0dc ff e0
0a04b0de 90
0a04b0df c7

JMP
??
??

EAX
90h
C7h

Команда по адресу перехода оказывается еще одной командой перехода. Но теперь дизассемблер не может выполнить
переход (да и аналитик-человек будет озадачен), потому что его
адрес находится в регистре (EAX) и вычисляется во время выпол630

ГЛАВА 21

нения. Это еще одна техника противодействия статическому
анализу, мы обсудим ее в разделе «Динамически вычисляемые
целевые адреса» ниже. Ранее мы определили, что, когда программа доходит до этой команды перехода, EAX содержит значение 0a04b0e0; именно с этого адреса мы должны возобновить
процесс дизассемблирования. Намылить, смыть, повторить1.
Вернемся к листингу 2.1; вместо того чтобы вручную переходить по адресу 0a04b0e0, дабы возобновить дизассемблирование,
мы можем записать в EAX известное значение, для чего нужно
щелкнуть правой кнопкой мыши по адресу  и выбрать из контекстного меню пункт Set Register Values (Задать значения
регистров). Тогда Ghidra окружит команду специальной разметкой, называемой регистровым переходом (register transition),
которая показывает предполагаемое значение конечного адреса
в команды JMP, хранящегося в EAX. Последующая очистка (клавиша C) и дизассемблирование (клавиш D) с этого места возоб­
новят процесс рекурсивного спуска от JMP к целевому адресу
0a04b0e0 и далее (включая создание XREF между блоками кода).
Преимущество этого подхода в том, что код снабжен аннотацией, в которой указан конечный адрес JMP, что позволяет
другим аналитикам проследить истинный поток управления
в этом участке программы (будет еще понятнее, если сочетать
это с переопределением проваливания (Fallthrough Override)
для команды LEA по адресу 0a04b0d8 в листинге 21.1). Этот альтернативный подход показан в следующем листинге:
0a04b0d7 58
POP
0a04b0d8 8d 40 0a LEA

EAX
EAX,[EAX + 0xa]
-- Fallthrough Override: 0a04b0dc
0a04b0db eb
??
EBh
assume EAX = 0xa04b0e0
LAB_0a04b0dc
XREF[1]: 0a04b0d8
0a04b0dc ff e0
JMP
EAX=>LAB_0a04b0e0
assume EAX =
0a04b0de 90
??
90h
0a04b0df c7
??
C7h
LAB_0a04b0e0
XREF[1]: 0a04b0dc(j)
0a04b0e0 58
POP
EAX
1

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

631

Еще один пример рассинхронизации, взятый из другого
двоичного файла, демонстрирует, как флаги процессора можно использовать для превращения условных переходов в без­
условные. В следующем листинге дизассемблера показано
применение для этой цели флага Z процессора x86:
00401000 XOR  EAX,EAX
00401002 JZ  LAB_00401009+1
00401004 MOV EBX,dword ptr [EAX]
00401006 MOV dword ptr [param_1 + -0x4],EBX
 LAB_00401009+1
XREF[0,1]: 00401002(j)
 00401009 CALL SUB_adfeffc6
0040100e FICOM word ptr [EAX + 0x59]

Здесь команда XOR  обнуляет регистр EAX и устанавливает
флаг Z. Программист, зная, что флаг Z поднят, пишет команду
перехода, если нуль, (JZ), которая всегда выполняется, и тем
самым имитирует безусловный переход. Поэтому команды
между переходом  и конечным адресом перехода  никогда не выполняются и призваны только запутать аналитика,
не осознающего этот факт. В данном примере также замаскирован реальный адрес перехода, поскольку команда JMP ведет
в середину команды CALL по адресу 00401009 . Правильно диз­
ассемблированный код должен выглядеть так:
00401000 XOR EAX,EAX
00401002 JZ LAB_0040100a
00401004 MOV EBX,dword ptr [EAX]
00401006 MOV dword ptr [param_1 + -0x4],EBX
 00401009 ?? E8h
LAB_0040100a
XREF[1]: 00401002(j)
 0040100a MOV EAX,0xdeadbeef
0040100f PUSH EAX
00401010 POP param_1

Истинный адрес перехода  обнаружился, как и дополнительный байт , который стал причиной рассинхронизации.
Конечно, можно использовать куда более запутанные способы
установки и проверки флагов до выполнения условного перехода. Анализ такого кода тем сложнее, чем больше операций
может воздействовать на флаг процессора перед его проверкой.
632

ГЛАВА 21

Динамически вычисляемые целевые адреса
Фраза «динамически вычисляемый» означает, что адрес, по которому пойдет программа, вычисляется на этапе выполнения.
В этом разделе мы рассмотрим несколько способов определения
такого адреса. Цель всегда состоит в том, чтобы скрыть (обфусцировать) истинный поток управления от любопытных глаз
процедуры статического анализа.
Один пример такой техники был показан в предыдущем разделе. В нем мы использовали команду CALL, чтобы поместить
в стек адрес возврата. Затем этот адрес извлекался непосредственно из стека в регистр, и к регистру прибавлялась константа, чтобы вычислить конечный адрес, на который команда JMP
переходила, пользуясь значением в указанном регистре.
Можно придумать бесконечное число подобных последовательностей команд для определения конечного адреса и передачи по нему управления. Следующий код, тоже взятый из
Shiva, демонстрирует другой метод динамического вычисления конечного адреса.















0a04b3be
0a04b3c3
0a04b3c5
0a04b3c9
0a04b3cf
0a04b3d1
0a04b3d7
0a04b3d9
0a04b3da
0a04b3db
0a04b3dc
0a04b3dd
0a04b3de
0a04b3e1

MOV ECX,0x7f131760 ; ECX = 7F131760
XOR EDI,EDI ; EDI = 00000000
MOV DI,0x1156 ; EDI = 00001156
ADD EDI,0x133ac000 ; EDI = 133AD156
XOR ECX,EDI ; ECX = 6C29C636
SUB ECX,0x622545ce ; ECX = 0A048068
MOV EDI,ECX ; EDI = 0A048068
POP EAX
POP ESI
POP EBX
POP EDX
POP ECX
XCHG dword ptr [ESP],EDI ; TOS = 0A048068
RET ; вернуться по адресу 0A048068

Комментарии справа от точки с запятой документируют
изменения регистров процессора после выполнения каждой
команды. Процесс завершается помещением вычисленного
значения на вершину стека (TOS) , в результате чего команда возврата передает управление по вычисленному адресу
(в данном случае 0A048068). Аналитик вынужден выполнить
Анализ обфусцированного кода

633

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

Обфускация потока управления
В последние годы разработаны и используются гораздо более
сложные методы сокрытия потока управления. В наиболее
сложных ситуациях программа использует несколько потоков
или дочерних процессов для вычисления информации о потоке
управления и получает эту информацию с помощью того или
иного механизма межпроцессного взаимодействия (в случае
дочерних процессов) или примитивов синхронизации (в случае
нескольких потоков).
В таких ситуациях статический анализ может оказаться исключительно трудным делом, поскольку нужно понять не только поведение нескольких исполняемых сущностей, но и точный
способ обмена информацией между ними. Например, один поток может ждать разделяемого семафора, а второй в это время
вычисляет какие-то значения или модифицирует код, который
будет использовать первый поток, когда второй просигнализирует ему о своем завершении с помощью семафора1.
Еще одна техника, часто встречающаяся во вредоносных
программах для Windows, – настроить обработчик исключения2, намеренно вызвать исключение и в процессе его обработки изменить состояние регистров процессора. Следующий
пример, взятый из средства противодействия обратной разработке tElock, призван запутать истинный поток выполнения
программы:

1

2

634

Можете считать, что семафор – это предмет, который должен находиться
у вас, перед тем как вы сможете войти в комнату и выполнить какое-то
действие. Пока предмет у вас, никто другой войти в комнату не может.
Сделав в комнате все, что нужно, вы можете выйти и передать предмет
еще кому-то, кто сможет войти в комнату и воспользоваться плодами вашего труда (а вы об этом знать не будете, потому что в комнате вас больше
нет). Семафоры часто применяются, чтобы организовать взаимно исключающие блокировки доступа к коду или данным.
Дополнительные сведения о структурной обработке исключений в Windows
(SEH) см. по адресу http://bytepointer.com/resources/pietrek_crash_course_
depths_of_win32_seh.htm.

ГЛАВА 21

 0041d07a CALL LAB_0041d07f
LAB_0041d07f XREF[1]: 0041d07a(j)
 0041d07f POP EBP
 0041d080 LEA EAX,[EBP + 0x46]
 0041d083 PUSH EAX
0041d084 XOR EAX,EAX
 0041d086 PUSH dword ptr FS:[EAX]
 0041d089 MOV dword ptr FS:[EAX],ESP
 0041d08c INT 3
0041d08d NOP
0041d08e MOV EAX,EAX
0041d090 STC
0041d091 NOP
0041d092 LEA EAX,[EBX*0x2 + 0x1234]
0041d099 CLC
0041d09a NOP
0041d09b SHR EBX,0x5
0041d09e CLD
0041d09f NOP
0041d0a0 ROL EAX,0x7
0041d0a3 NOP
0041d0a4 NOP
 0041d0a5 XOR EBX,EBX
 0041d0a7 DIV EBX
0041d0a9 POP dword ptr FS:[0x0]

Последовательность начинается с использования CALL  для
вызова следующей команды ; команда CALL помещает значение 0041d07f в стек в качестве адреса возврата, и сразу вслед за
тем это значение извлекается из стека в регистр EBP . Затем
в регистр EAX  записывается сумма EBP и 46h, или 0041d0c5, и этот
адрес помещается в стек  как адрес возврата функции-обработчика исключения. Остальная часть настройки обработчика
исключения происходит в командах  и , которые завершают
включение нового обработчика в цепочку уже существующих
обработчиков исключений, на которую указывает FS:[0]1.
Следующий шаг – намеренно возбудить исключение , в данном случае INT 3 – программное прерывание для запуска отладчика (в программах для x86 команда INT 3 используется отлад1

Windows настраивает регистр FS, так чтобы он указывал на базовый адрес
блока окружения потока (TEB). Первым полем TEB является начало связного списка указателей на обработчики исключений, которые вызываются
в нужном порядке, если в процессе происходит исключение.
Анализ обфусцированного кода

635

чиками для реализации программной точки останова). Обычно
в этой точке управление получает присоединенный отладчик,
поскольку отладчикам шанс обработать исключение предоставляется в первую очередь. В нашем случае программа ожидает
это исключение, поэтому отладчику нужно сказать, чтобы он
передал исключение программе. Если не разрешить программе
обработать это исключение, то, возможно, она будет работать неправильно или аварийно завершится. Не зная, как обрабатывается исключение INT 3, невозможно понять, что может произойти
в программе дальше. Если предположить, что выполнение просто
возобновляется с точки, следующей за INT 3, то все выглядит
так, будто команды  и  приводят к ошибке деления на нуль.
Декомпилированный обработчик исключения для показанного выше кода начинается по адресу 0041d0c5. Ниже показана
первая часть функции:
int FUN_0041d0c5(EXCEPTION_RECORD *param_1,void *frame,CONTEXT *ctx) {
DWORD code;
 ctx->Eip = ctx->Eip + 1;
 code = param_1->ExceptionCode;
 if (code == EXCEPTION_INT_DIVIDE_BY_ZERO) {
ctx->Eip = ctx->Eip + 1;
 ctx->Dr0 = 0;
ctx->Dr1 = 0;
ctx->Dr2 = 0;
ctx->Dr3 = 0;
ctx->Dr6 = ctx->Dr6 & 0xffff0ff0;
ctx->Dr7 = ctx->Dr7 & 0xdc00;
}

Третьим аргументом функции обработки исключения 
является указатель на структуру Windows CONTEXT (определена в заголовочном файле Windows API winnt.h). Структура
CONTEXT инициализируется значениями регистров процессора
в момент исключения. Обработчик может осмотреть и, если захочет, изменить содержимое структуры CONTEXT. Если обработчик полагает, что решил проблему, ставшую причиной исключения, то может уведомить операционную систему о том, что
«сбойнувшему» потоку можно разрешить дальнейшую работу.
В этот момент операционная система копирует в регистры про636

ГЛАВА 21

цессора для данного потока значения из структуры CONTEXT,
установленные обработчиком исключения, и выполнение потока продолжается как ни в чем не бывало.
В нашем примере обработчик исключения первым делом обращается к структуре CONTEXT потока и увеличивает на единицу
счетчик команд , чтобы выполнение возобновилось с команды, следующей за той, что возбудила исключение. Затем выбирается код типа исключения (поле предоставленной структуры
EXCEPTION_RECORD) , чтобы определить природу исключения.
В этой части обрабатывается ошибка деления на нуль , сгенерированная в предыдущем примере, – обнуляются  все аппаратные отладочные регистры x86, и запрещаются аппаратные точки останова1. Без изучения других частей кода tElock
не понятно, зачем очищаются отладочные регистры. В данном
случае tElock очищает значения, оставшиеся от предыдущей
операции, в которой отладочные регистры использовались для
установки еще четырех точек останова в дополнение к уже рассмотренной INT 3. Очистка и изменение отладочных регистров
x86 в сочетании с обфускацией истинного потока управления
могут полностью нарушить работу таких программных отладчиков, как OllyDbg или GDB. Подобные антиотладочные методы обсуждаются в разделе «Методы противодействия динамическому анализу» ниже.

Обфускация кода операции
Методы, описанные до сих пор, могут помешать – собственно,
для этого они и предназначены – понять, как устроен поток
управления в программе, но помешать увидеть правильную
дизассемблированную форму программы ни один не может.
Рассинхронизация влияет – и еще как – на работу дизассемб­
лера, но мы легко справлялись с ней, переформатируя листинг,
так чтобы он отражал истинный поток команд.
Более эффективный способ предотвратить правильное дизассемблирование – закодировать или зашифровать настоящие
1

В архитектуре x86 отладочные регистры от 0 до 7 (DR0–DR7) используются
для управления аппаратными точками останова. DR0–DR3 служат для
задания адресов точек останова, а DR6 и DR7 – чтобы разрешить и запретить конкретные аппаратные точки останова.
Анализ обфусцированного кода

637

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

Модифицированная
точка входа

Заголовок
Точка
входа

Код
Данные

Заголовок
Обфусцированный код

Обфускатор

Обфусцированные данные
Вставка деобфускации

Переход на
оригинальную
точку входа
(OEP)

Рис. 21.2. Общий вид процесса обфускации
Как видим, на вход подается подлежащая обфускации программа. Часто она написана на стандартном языке программирования с помощью стандартных инструментов (редактора,
компилятора и т. д.), без каких-либо мыслей о предстоящей обфускации. Получившийся исполняемый файл подается на вход
утилите обфускации, которая преобразует его в функционально
эквивалентный, но обфусцированный двоичный файл. На рисунке показано, что эта утилита отвечает за обфускацию секций кода
и данных исходной программы и включение дополнительного
кода (вставки деобфускации), который во время выполнения восстанавливает исходную форму программы, прежде чем ее можно
будет использовать. Утилита обфускации также модифицирует
заголовки программы, перенаправляя точку входа на вставку деобфускации. После того как программа деобфусцирована, управление передается на оригинальную точку входа, и выполнение
продолжается так, будто никакой обфускации не было.
В основе этого процесса, описание которого мы сильно упрос­
тили, лежит утилита, предназначенная для создания обфусцированного двоичного файла. Количество таких утилит постоянно растет, а их функциональность варьируется от сжатия до
противодействия дизассемблированию и отладке. В качестве
примеров упомянем UPX (упаковщик, работает также с ELF638

ГЛАВА 21

файлами; https://upx.github.io/), ASPack (упаковщик; http://
www.aspack.com/), ASProtect (утилита, противодействующая
обратной разработке, от создателей ASPack) и tElock (упаковка
и противодействие обратной разработке; http://www.softpedia.
com/get/Programming/Packers-Crypters-Protectors/Telock.shtml)
для PE-файлов в Windows. Возможности утилит обфускации
возросли до такой степени, что некоторые инструменты противодействия обратной разработке, например VMProtect, интегрируются в процесс сборки, что позволяет программистам
включать средства противодействия на всех этапах разработки – от исходного кода до постобработки откомпилированного
двоичного файла (https://vmpsoft.com/).

Песочницы
Назначение песочницы в процессе обратной разработки – дать
вам возможность выполнять программу таким образом, чтобы
можно было наблюдать за ее поведением, но при этом не позволить ей воздействовать на критические компоненты платформы
или на подключенные к ней компьютеры. Песочницы обычно
строятся с помощью платформенно зависимого ПО виртуализации, это можно делать и на специально выделенных системах,
которые можно восстановить в известном корректном состоянии
после выполнения вредоносной программы.
Песочницы чаще оснащают развитыми средствами наблюдения
и сбора информации о поведении работающей программы. Собранные данные могут включать сведения об операциях файловой системы, реестра (в случае Windows) и сетевой активности
программы. Примером полнофункциональной песочницы может
служить Cuckoo (https://cuckoosandbox.org/) – популярная песочница с открытым исходным кодом, специально предназначенная
для анализа вредоносных программ.

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

639

Один из таких инструментов, предназначенный для деобфускации исполняемых файлов в Windows, называется QuickUnpack
(http://qunpack.ahteam.org/?p=458; сайт на русском языке).
QuickUnpack, как и многие другие автоматизированные распаковщики, работает как отладчик; он дает возможность обфусцированному двоичному файлу выполнить весь этап деобфускации,
а затем выгружает образ программы из памяти. Имейте в виду,
что такого рода инструменты реально выполняют вредоносные
программы в надежде перехватить управление после распаковки
или деобфускации, но до того, как они успели причинить какойто вред. Поэтому выполнять их следует только в песочнице.
Использование среды чисто статического анализа обфусцированного кода – трудная задача. Необходимо распаковать или
расшифровать обфусцированные части программы, не имея
возможности выполнить вставку деобфускации. В полосе обзора типа адреса справа на рис. 21.3 показана структура исполняемого файла, упакованного UPX. Ghidra кодирует цветом
участки полосы обзора, чтобы дать представление о содержимом двоичного файла. Перечислим виды содержимого:
► функция;
► не инициализировано;
► внешняя ссылка;
► команда;
► данные;
► не определено.
Глядя на полосу обзора, мы видим результаты предварительной оценки различных частей программы. Задержав мышь над
участком полосы обзора, мы получим дополнительную информацию о ней. Необычный вид этой конкретной полосы – признак того, что файл был обфусцирован. Рассмотрим некоторые
участки более пристально.
Ghidra нашла секцию данных  в начале файла. Изучив ее
содержимое, мы обнаружим заголовки файла и информативную
строку, указывающую вид примененной к файлу обфускации:
This file is packed with the UPX executable packer http://upx.tsx.org
UPX 1.07 Copyright (C) 1996-2001 the UPX Team. All Rights Reserved.

640

ГЛАВА 21









Рис. 21.3. Окно листинга и полоса обзора типа адреса для
двоичного файла, упакованного UPX
За этой секцией следует блок неопределенного содержимого
 примерно такого вида:
004008a3 72 ??
004008a4 85 ??
004008a5 6c ??

72h r
85h
6Ch l

Анализ обфусцированного кода

641

Самая большая секция  содержит неинициализированные
данные, которые выглядят в окне листинга так:
004034e3 ?? ??
004034e4 ?? ??

Немного дальше в файле Ghidra нашла еще один блок неопределенного содержимого . В конце этих данных имеется область,
идентифицированная как функция . Эта функция легко опо­
знается как вставка распаковки; Ghidra определила, что это точка
входа в двоичный файл, как видно в левой части окна листинга
на рис. 21.3. Сегменты неопределенного содержимого  и  – результат процесса упаковки UPX. Задача вставки распаковки –
распаковать эти данные в неинициализированную область ,
перед тем как передать управление распакованному коду.
Информацию, показанную в полосе обзора типа адреса, можно сопоставить со свойствами каждого сегмента двоичного файла
и определить, согласована ли информация в двух представлениях. Карта памяти этого двоичного файла показана на рис. 21.4.




Рис. 21.4. Карта памяти двоичного файла, упакованного UPX
В этом файле весь диапазон адресов в сегменте UPX0  и сегменте UPX1  (00401000–00408fff) помечен как выполняемый (флаг X
поднят). С учетом этого факта можно было бы ожидать, что вся
полоса обзора типа адреса будет окрашена в цвет функции. Но это
не так, и в сочетании с тем, что весь диапазон UPX0 не инициализирован, это должно вызвать серьезные подозрения и дать ценную
подсказку о том, как анализировать этот двоичный файл.
Методы применения Ghidra к выполнению распаковки в статическом контексте (без выполнения двоичного файла) для
таких файлов, как этот, обсуждаются в разделе «Статическая
деобфускация двоичных файлов в Ghidra» ниже.
642

ГЛАВА 21

Обфускация импортированной функции
Методы противодействия статическому анализу могут также
скрывать, какие библиотеки и библиотечные функции используются в двоичном файле, чтобы предотвратить утечку информации о потенциальных действиях этого файла. В большинстве
случаев это делает такие инструменты, как dumpbin, ldd и objdump,
бесполезными для получения зависимостей от библиотек.
Результат подобной обфускации нагляднее всего виден в дереве символов. На рис. 21.5 показано все дерево символов приведенного выше примера tElock.

Рис. 21.5. Дерево символов для обфусцированного двоичного файла
Ссылки имеются только на две импортированные функции:
GetModulehandleA (из kernel32.dll) и MessageBoxA (из user32.dll).
Из этого короткого списка нельзя сделать практически никаких выводов о поведении программы. Методы тоже бывают
разными, но всегда программа должна загрузить дополнительные библиотеки, от которых зависит, а затем найти в них
требуемые функции. В большинстве случаев это делает вставка деобфускации до передачи управления деобфусцированной
программе. Конечная цель – правильно инициализировать
таб­лицу импорта программы – так, будто это сделал встроенный в операционную систему загрузчик.
Анализ обфусцированного кода

643

Для двоичных файлов в Windows проще всего воспользоваться функцией LoadLibrary для загрузки необходимых библиотек по имени, а затем вызвать функцию GetProcAddress, чтобы
найти адреса функций в каждой библиотеке. Чтобы использовать эти функции, программа должна быть либо скомпонована с ними статически, либо располагать альтернативными
средствами поиска. В дереве символов программы tElock нет
ни одной из этих функций, а в дереве символов для примера
с UPX, показанного на рис. 21.6, есть обе.

Рис. 21.6. Дерево символов для примера с UPX
В листинге 21.1 показан код UPX, отвечающий за восстановление таблицы импорта.
LAB_0040886c
0040886c MOV EAX,dword ptr [EDI]
0040886e OR EAX,EAX
00408870 JZ LAB_004088ae
00408872 MOV EBX,dword ptr [EDI + 0x4]
00408875 LEA EAX,[EAX + ESI*0x1 + 0x8000]
0040887c ADD EBX,ESI

644

ГЛАВА 21

XREF[1]: 0040888e(j)

0040887e PUSH EAX
0040887f ADD EDI,0x8
00408882 CALL  dword ptr [ESI + 0x808c]=>KERNEL32.DLL::LoadLibraryA
00408888 XCHG EAX,EBP
LAB_00408889 XREF[1]: 004088a6(j)
00408889 MOV AL,byte ptr [EDI]
0040888b INC EDI
0040888c OR AL,AL
0040888e JZ LAB_0040886c
00408890 MOV ECX,EDI
00408892 PUSH EDI
00408893 DEC EAX
00408894 SCASB.REPNE ES:EDI
00408896 PUSH EBP
00408897 CALL  dword ptr [ESI + 0x8090]=>KERNEL32.DLL::GetProcAddress
0040889d OR EAX,EAX
0040889f JZ LAB_004088a8
004088a1 MOV  dword ptr [EBX],EAX ; сохранить в таблице импорта
004088a3 ADD EBX,0x4
004088a6 JMP LAB_00408889

Листинг 21.2. Реконструкция таблицы импорта в UPX
В этом примере имеется внешний цикл, отвечающий за загрузку LoadLibrary, и внутренний цикл, отвечающий за вызов GetProcAddress . После каждого успешного обращения
к GetProcAddress полученный адрес функции сохраняется в реконструированной таблице импорта .
Эти циклы выполняются в самом конце вставки деобфускации UPX, потому что каждая функция принимает указатель
на строку, содержащую либо имя библиотеки, либо имя функции, а эти строки находятся в упакованной области данных,
чтобы их не обнаружила утилита strings. Поэтому загрузка
библиотек в UPX не может начаться, пока требуемые строки
не будут распакованы.
В примере tElock возникает другая проблема. Имея всего
две импортированные функции, ни одна из которых не совпадает с LoadLibrary или GetProcAddress, как tElock решает задачу разрешения функций, так успешно выполненную UPX?
Все процессы Windows зависят от kernel32.dll, а это означает,
что она присутствует в памяти любого процесса. Если программа может найти kernel32.dll, то сравнительно простая процедура позволяет отыскать любую функцию в этой DLL, в т. ч.
Анализ обфусцированного кода

645

LoadLibrary и GetProcAddress. Как показано выше, располагая
этими функциями, мы можем загрузить все остальные библиотеки, необходимые процессу, и найти в них нужные функции.
В статье «Understanding Windows Shellcode» Скейп обсуждает, как это можно сделать1. Хотя в tElock используется не совсем та техника, которую описывает Скейп, параллелей между
ними много, а итог один – обфусцировать детали процесса загрузки и компоновки. Если прослеживать команды программы невнимательно, то очень легко пройти мимо загрузки биб­
лиотеки и поиска адресов функций. В следующем коротком
фрагменте демонстрируется, как tElock пытается найти адрес
LoadLibrary:
0041d1e4
0041d1ea
0041d1ec
0041d1f3
0041d1f5
0041d1fc

CMP
JNZ
CMP
JNZ
CMP
JNZ

dword ptr [EAX],0x64616f4c
LAB_0041d226
dword ptr [EAX + 0x4],0x7262694c
LAB_0041d226
dword ptr [EAX + 0x8],0x41797261
LAB_0041d226

В глаза бросается несколько идущих подряд сравнений.
А вот цель этих сравнений очевидна не сразу. Переформатирование операндов (щелкнуть правой кнопкой мыши и выбрать
из меню пункт ConvertChar Sequence) каждой команды
сравнения проливает свет на этот код, как показано в следующем листинге.
0041d1e4
0041d1ea
0041d1ec
0041d1f3
0041d1f5
0041d1fc

CMP
JNZ
CMP
JNZ
CMP
JNZ

dword ptr [EAX],"Load"
LAB_0041d226
dword ptr [EAX + 0x4],"Libr"
LAB_0041d226
dword ptr [EAX + 0x8],"aryA"
LAB_0041d226

Каждая шестнадцатеричная константа – это последовательность четырех символов ASCII, которые Ghidra может отобразить как заключенную в кавычку ASCII-строку. Вместе они
1

646

См. http://www.hick.org/code/skape/papers/win32-shellcode.pdf, конкретно
главу 3 «Shellcode Basics» и раздел 3.3 «Resolving Symbol Addresses».

ГЛАВА 21

образуют строку LoadLibraryA1. Если все три сравнения завершились успешно, значит, tElock нашла в таблице экспорта запись
о LoadLibraryA, и еще через несколько команд мы получим адрес
этой функции для загрузки дополнительных библиотек. Принятый в tElock подход к поиску функций в какой-то мере противостоит анализу строк, потому что 4-байтовые константы, встроенные непосредственно в команды, не выглядят как стандартные
завершаемые нулем строки и потому не включаются в сгенерированный Ghidra список строк, если только не изменить парамет­
ры по умолчанию (например, сбросить флажок Require Null
Termination (требуется завершающий нуль) при поиске строк).
Ручная реконструкция таблицы импорта программы посредством тщательного анализа кода программы в UPX и tElock
не слишком сложна, потому что в обоих случаях в распакованном коде имеются ASCII-данные, с помощью которых можно
точно определить, какие библиотеки и функции нужны. В статье
Скейпа подробно описан процесс разрешения функций, когда
в коде вообще нет строк. Основная идея – заранее вычислить
уникальный хеш-код имени каждой необходимой функции2.
Чтобы разрешить функцию, мы просматриваем таблицу экспортируемых из библиотеки имен. Каждое имя хешируется,
и результат сравнивается с предварительно вычисленными
хеш-кодами. Если хеши совпадают, то искомая функция найдена, и мы легко можем найти ее адрес в таблице экспортируемых библиотекой функций.
Чтобы статически проанализировать двоичный файл, обфусцированный таким способом, нужно понимать алгоритм хеширования имен и применять его ко всем именам, экспортированным из библиотеки, в которой производится поиск. Имея
полную таблицу хешей, можно без труда поискать в ней хеши
и определить, к какой функции относится хеш. Часть такой
таб­лицы, сгенерированная для kernel32.dll, могла бы выглядеть следующим образом:
1

2

Многие функции Windows, принимающие строковые параметры, сущест­
вуют в двух вариантах: один принимает ASCII-строки, другой – Юникодстроки. Имя первого варианта оканчивается суффиксом A, а имя второго –
суффиксом W.
Хеширование – это математическая процедура, которая возвращает результат фиксированной длины (скажем, 4 байта) для входных данных
произвольной длины (скажем, строки).
Анализ обфусцированного кода

647








GetProcAddress : 8A0FB5E2
GetProcessAffinityMask : B9756EFE
GetProcessHandleCount : B50EB87C
GetProcessHeap : C246DA44
GetProcessHeaps : A18AAB23
GetProcessId : BE05ED07

Отметим, что хеш-коды зависят от функции хеширования,
используемой в конкретном двоичном файле, и, скорее всего,
будут различны в разных файлах. Если таблица выглядит так,
как показано выше, и обнаружен хеш-код 8A0FB5E2 , то мы можем сразу сказать, что программа пытается найти адрес функции GetProcAddress.
Техника применения хеш-кодов для разрешения имен функций, описанная Скейпом, первоначально была разработана
и документирована для использования в полезных нагрузках
эксплойтов уязвимостей Windows; однако впоследствии она
была адаптирована и для использования в обфусцированных
программах.

Методы противодействия
динамическому анализу
Ни один из рассмотренных в предыдущих разделах методов
противодействия статическому анализу не может помешать
выполнить программу. Конечно, они могут затруднить понимание истинного поведения программы средствами одного
лишь статического анализа, но если бы они не давали выполнять программу, то были бы попросту бесполезны, и тогда программу не нужно было бы анализировать вовсе.
Поскольку любая программа должна выполняться, чтобы совершать какую-то полезную работу, динамический анализ ставит целью наблюдать за поведением программы «в движении»
(когда она работает), а не «в покое» (когда программа не работает – это задача статического анализа). В этом разделе мы
кратко опишем некоторые распространенные методы противодействия динамическому анализу. По большей части они слабо
связаны с инструментами статического анализа, но в тех случаях, когда они перекрываются, мы будем это отмечать.
648

ГЛАВА 21

Обнаружение виртуализации
В песочницах обычно используются программные средства
виртуализации, например VMware, чтобы организовать среду
выполнения для вредоносных (или любых других) программ.
Такие среды хороши тем, что предлагают механизмы создания контрольной точки и отката, позволяющие быстро восстановить заведомо хорошее состояние песочницы. А основной
недостаток заключается в том, что вредоносная программа
может обнаружить наличие песочницы. В предположении,
что виртуализация = наблюдение, многие программы, желающие остаться незамеченными, просто завершаются, определив, что запущены внутри виртуальной машины. Впрочем,
с учетом широкого распространения виртуализации для производственных целей это предположение в наши дни не столь
однозначно, как было в прошлом.
Ниже описаны некоторые методы, применяемые программами, работающими в виртуализированных средах, для определения того, что они выполняются под управлением виртуальной машины, а не на «голом железе».
Обнаружение программ, специфичных для виртуализации
Пользователи часто устанавливают вспомогательные приложения на виртуальные машины, чтобы организовать
взаимодействие между виртуальной машиной и операционной хост-системой или просто для повышения производительности виртуальной машины. Набор VMware Tools –
пример подобного рода ПО. Наличие таких программ
легко определяется программами, работающими внутри
виртуальной машины. Например, когда VMware Tools
устанавливается в виртуальную машину в ОС Microsoft
Windows, создаются разделы реестра, которые может прочитать любая программа. Обнаружив такие разделы, вредоносная программа, возможно, предпочтет завершиться,
не раскрывая достойного внимания поведения. С другой
стороны, ныне виртуализация распространена так широко, что наличие образа VMware с неустановленным набором VMware Tools может выглядеть не менее подозрительно в глазах вредоносной программы.
Анализ обфусцированного кода

649

Обнаружение оборудования, специфичного
для виртуализации
В виртуальных машинах используются уровни абстрагирования оборудования для организации интерфейса между
виртуальной машиной и реальным оборудованием хосткомпьютера. Характеристики виртуального оборудования
зачастую легко обнаруживаются программами, работающими внутри виртуальной машины. Например, компании
VMware были назначены организационно уникальные
идентификаторы (OUI) для ее виртуализированных сетевых адаптеров1. Увидев такой OUI, можно с большой долей
уверенности сказать, что программа работает внутри виртуальной машины. Программу, которая завершается по
этой причине, можно обмануть и заставить выполняться,
изменив MAC-адреса, назначенные виртуальным сетевым
адаптерам, относящимся к виртуальной машине.
Обнаружение изменений в поведении процессора
Добиться совершенной виртуализации трудно. В идеале программа не должна замечать никаких различий
между виртуализированной средой и реальным оборудованием. Но так бывает редко. Иоанна Рутковска (Joanna
Rutkowska) разработала методику обнаружения VMware
под названием Red Pill, наблюдая за различиями в поведении команды x86 sidt на реальном оборудовании и под
управлением виртуальной машины2.

Обнаружение оснащения инструментальными
средствами
После создания песочницы, но до выполнения программы,
за которой мы хотим наблюдать, необходимо разместить инструментальные средства, которые будут собирать и сохранять
информацию о поведении анализируемой программы. Для
такого рода задач мониторинга есть немало инструментов.
Упомянем лишь два особенно популярных: Process Monitor от
1
2

650

OUI занимает первые 3 байта заводского MAC-адреса сетевого адаптера.
См. https://web.archive.org/web/20041130172213/http://invisiblethings.org/
papers/redpill.html.

ГЛАВА 21

группы Sysinternals в Microsoft и Wireshark1. Process Monitor –
утилита, умеющая вести мониторинг некоторых операций любого процесса в Windows, в т. ч. доступ к реестру и к файловой
системе. Wireshark – инструмент сбора и анализа сетевых пакетов, который часто используется для анализа сетевого трафика, генерируемого вредоносными программами.
Авторы вредоносных программ с параноидальным складом
ума могут искать работающие экземпляры таких программ
мониторинга. Методы при этом применяются разные: от просмотра списка активных процессов на предмет наличия имен
известных программ до поиска известных строк в полосах заголовков всех активных приложений Windows. Возможно и более глубокое исследование; иные программы заходят настолько
далеко, что ищут особенности компонентов GUI, характерные
для некоторых инструментальных средств.

Обнаружение отладчиков
Отладчик не ограничивается пассивным наблюдением за программой, а позволяет аналитику полностью управлять ее поведением. Отладчики обычно используются, чтобы выполнить
обфусцированную программу до момента, когда распаковка
или дешифрирование уже выполнены, после чего можно воспользоваться имеющимися у отладчика средствами доступа
к памяти, чтобы выгрузить образ деобфусцированной программы из памяти. В большинстве случаев для завершения анализа выгруженного образа процесса можно воспользоваться стандартными инструментами и методами статического анализа.
Авторы утилит обфускации прекрасно знают о таких методах
деобфускации с применением отладчика и разработали способы
противодействия использованию отладчиков для выполнения
обфусцированных программ. Обнаружив отладчик, программа
часто предпочитает завершиться, а не выполнять операции, которые позволили бы аналитику определить, что она делает.
Методы обнаружения отладчиков варьируются от простых
запросов к операционной системе с помощью таких хорошо
известных API, как функция IsDebuggerPresent в Windows, до
1

См. соответственно https://docs.microsoft.com/en-us/sysinternals/downloads/
procmon и http://www.wireshark.org/.
Анализ обфусцированного кода

651

низкоуровневых проверок памяти или процессора на предмет
наличия признаков присутствия отладчика. Пример последнего – проверка, что в процессоре поднят флаг трассировки (одношагового режима).
Если знать, что искать, то обнаружить присутствие отладчика совсем не трудно, а попытки такого рода легко определяются в ходе статического анализа (если только одновременно
не применяются методы противодействия статическому анализу). Дополнительные сведения об обнаружении отладчика см.
в статье «Anti Debugging Detection Techniques with Examples»,
где приводится подробный обзор методов противодействия отладчикам в Windows1.

Воспрепятствование отладке
Даже если отладчик не обнаруживается, ему можно помешать,
применяя дополнительные приемы, например устанавливать
случайные точки останова, сбрасывать аппаратные точки останова, затруднить дизассемблирование, чтобы не дать поставить
точки останова в нужное место, или вообще помешать отладчику
присоединиться к процессу. Многие методы, обсуждаемые в вышеупомянутой статье о противодействии отладчикам, направлены на то, чтобы воспрепятствовать правильной работе отладчика.
Намеренное возбуждение исключений – еще одна попытка
затруднить отладку. В большинстве случаев присоединенный
отладчик перехватывает исключение, а пользователь должен
проанализировать, почему исключение произошло и следует ли
передать его отлаживаемой программе. В случае программной
точки останова, например команды x86 INT 3, бывает трудно отличить программное прерывание, сгенерированное отлаживаемой программой, от результата срабатывания настоящей точки
останова отладчика. Именно этой путаницы и добивается автор обфусцированной программы. В таких случаях можно, хотя
это и труднее, понять истинный поток выполнения программы,
внимательно проанализировав листинг дизассемблера.
Кодирование участков программы имеет двойной эффект: помешать статическому анализу, сделав невозможным дизассемб­
1

652

См. https://www.apriorit.com/dev-blog/367-anti-reverse-engineering-protectiontechniques-to-use-before-releasing-software.

ГЛАВА 21

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

Трассировка процесса
API ptrace, или трассировки процесса, имеющийся во многих
Unix-подобных системах, позволяет одному процессу наблюдать и управлять выполнением другого процесса. Отладчик GNU
(gdb) – одно из самых известных приложений на основе ptrace.
Пользуясь API ptrace, родительский процесс может присоединиться к дочернему и управлять им. После присоединения родительского процесса дочерний останавливается при каждом получении сигнала, а родитель уведомляется об этом с помощью
определенной в стандарте POSIX функции wait. В этот момент
родитель может проинспектировать или изменить состояние дочернего процесса, прежде чем позволить ему продолжить выполнение. После того как родительский процесс присоединился
к дочернему, никакой другой процесс не сможет к нему присоединиться, пока трассирующий родитель не отсоединится.

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

653

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

СТАТИЧЕСКАЯ ДЕОБФУСКАЦИЯ
ДВОИЧНЫХ ФАЙЛОВ В GHIDRA
Сейчас, узнав о существующих методах противодействия обратной разработке, вы, наверное, недоумеваете, как вообще возможно проанализировать программу, если автор вознамерился
сохранить ее в секрете. Учитывая, что помехи ставятся инструментам и статического, и динамического анализов, какой подход лучше выбрать, чтобы сделать тайное явным? К сожалению, решения, пригодного для всех случаев, не существует.
В общем и целом решение зависит от ваших навыков и имеющихся инструментов. Если вы предпочитаете отладчик, то
должны будете разработать стратегию обхода мер обнаружения
и воспрепятствования работе отладчика. Если же ваш любимый
инструмент – дизассемблер, то нужно подумать, как получить верный листинг программы, а если код самомодифицируемый, то как
имитировать его поведение, чтобы правильно изменять листинг.
В этом разделе мы обсудим два метода обращения с самомодифицируемым кодом при статическом анализе (т. е. без выполнения кода). Статический анализ может оказаться единственным
выходом, когда вы не хотите (из-за враждебного кода) или не можете (из-за отсутствия оборудования) проанализировать программу под управлением отладчика. Не отчаивайтесь, если вам
кажется, что так вы идете прямиком к кроличьей норе. В Ghidra
есть секретное (а может, и не такое уж секретное) оружие, которым
можно воспользоваться в битве за статическую деобфускацию.

Скриптовая деобфускация
Поскольку Ghidra можно использовать для дизассемблирования двоичных файлов, собранных для разных процессоров – и их число постоянно возрастает, – нередко бывает, что
анализируется файл, предназначенный совсем не для той
платформы, на которой работает Ghidra. Например, вас могут
654

ГЛАВА 21

попросить проанализировать файл для Linux x86, хотя в данный момент вы работаете с Ghidra на macOS. Или проанализировать файл для MIPS или ARM, хотя Ghidra работает на x86.
В подобных случаях вам просто недоступны такие инструменты, как отладчики, необходимые для динамического анализа двоичного файла. Если такой файл был обфусцирован путем кодирования участков программы, то нет другого выхода,
кроме как создать скрипт Ghidra, который имитирует стадию
деобфускации с целью правильно декодировать программу
и дизассемблировать декодированные команды и данные.
Такая задача поначалу внушает трепет, но во многих случаях при декодировании обфусцированной программы используется только небольшое подмножество системы команд процессора, поэтому изучать все команды, может быть, и не придется.
В главе 14 описан алгоритм разработки скриптов, эмулирующих поведение участков программы. В следующем примере
мы воспользуемся им для написания простого скрипта Ghidra,
который будет декодировать программу, зашифрованную инструментом Burneye ELF Encryption. В нашем примере выполнение начинается командами, показанными в листинге 21.3.
 05371035 PUSH dword ptr [DAT_05371008]
 0537103b PUSHFD
 0537103c PUSHAD
 0537103d MOV ECX,dword ptr [DAT_05371000]
05371043 JMP LAB_05371082
...
LAB_05371082
XREF[1]:
 05371082 CALL FUN_05371048
05371087 SHL byte ptr [EBX + -0x2b],1
0537108a PUSHFD
0537108b XCHG byte ptr [EDX + -0x11],AL
0537108e POP SS
0537108f XCHG EAX,ESP
05371090 CWDE
05371091 AAD 0x8e
05371093 PUSH ECX
 05371094 OUT DX,EAX
05371095 ADD byte ptr [EDX + 0xa81bee60],BH
0537109b PUSH SS
0537109c RCR dword ptr [ESI + 0xc],CL
0537109f PUSH CS

05371043(j)

Анализ обфусцированного кода

655

053710a0
053710a2
053710a5
053710af
053710b1
053710b4
 053710b5

SUB
CMP
CMP
AND
SBB
POP
??

AL,0x70
CH,byte ptr [EAX + 0x6e]
dword ptr [DAT_cbd35372],0x9c38a8bc
AL,0xf4
EBP,ESP
DS
C6h

Листинг 21.3. Начальная последовательность команд Burneye
и обфусцированный код
Сначала программа помещает в стек содержимое ячейки памяти по адресу 05371008h , а затем флаги процессора  и все
регистры . Назначение этих команд не вполне понятно, поэтому мы просто примем их к сведению и разберемся позже.
Затем в регистр ECX загружается содержимое ячейки памяти по
адресу 05371000h . В соответствии с алгоритмом из главы 14
мы должны в этот момент объявить переменную с именем ECX
и инициализировать ее содержимым памяти с помощью функции Ghidra getInt:
int ECX = getInt(toAddr(0x5371000)); // из команды по адресу 0537103d

После безусловного перехода программа вызывает функцию
FUN_05371048 , которая помещает в стек адрес 05371087h (адрес
возврата). Дизассемблированные команды, следующие за этим
вызовом CALL, становятся все менее и менее осмысленными.
Команда OUT  вообще не должна встречаться в пользовательском коде, а команду по адресу 053710B5h  Ghidra не смогла
дизассемблировать. Это признаки того, что с двоичным файлом не все в порядке (дополнительным свидетельством является тот факт, что дерево символов содержит всего две функции:
entry и FUN_05371048).
В этот момент анализ следует продолжить с вызова функции
FUN_05371048, показанной в листинге 21.4.
FUN_05371048
entry:05371082(c)
 05371048 POP ESI
 05371049 MOV EDI,ESI

656

ГЛАВА 21

XREF[1]:

 0537104b MOV EBX,dword ptr [DAT_05371004] = C09657B0h
05371051 OR EBX,EBX
 05371053 JZ LAB_0537107f
 05371059 XOR EDX,EDX
 LAB_0537105b
XREF[1]: 0537107d(j)
0537105b MOV EAX,0x8
 LAB_05371060
XREF[1]: 05371073(j)
05371060 SHRD EDX,EBX,0x1
05371064 SHR EBX,1
05371066 JNC LAB_05371072
0537106c XOR EBX,0xc0000057
LAB_05371072
XREF[1]: 05371066(j)
05371072 DEC EAX
05371073 JNZ LAB_05371060
05371075 SHR EDX,0x18
05371078 LODSB ESI
05371079 XOR AL,DL
0537107b STOSB ES:EDI
0537107c DEC ECX
0537107d JNZ LAB_0537105b
LAB_0537107f
XREF[1]: 05371053(j)
0537107f POPAD
05371080 POPFD
05371081 RET

Листинг 21.4. Функция декодирования из Burneye
Это необычная функция: она сразу извлекает адрес возврата
из стека в регистр ESI . Напомним, что сохранен был адрес
возврата 05371087h, а принимая во внимание инициализацию
регистров EDI , EBX  и EDX , мы можем продолжить скрипт
следующим образом:
int
int
int
int
int

ECX
ESI
EDI
EBX
EDX

=
=
=
=
=

getInt(toAddr(0x5371000));
0x05371087;
ESI;
getInt(toAddr(0x5371004));
0;

//
//
//
//
//

из
из
из
из
из

команды
команды
команды
команды
команды

по
по
по
по
по

адресу
адресу
адресу
адресу
адресу

0537103D
05371048
05371049
0537104B
05371059

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

657

public void run() throws Exception {
int ECX = getInt(toAddr(0x5371000)); // из команды по адресу 0537103D
int ESI = 0x05371087; // из команды по адресу 05371048
int EDI = ESI; // из команды по адресу 05371049
int EBX = getInt(toAddr(0x5371004)); // из команды по адресу 0537104B
if (EBX != 0) { // из команды по адресу 05371051
// и 05371053
int EDX = 0; // из команды по адресу 05371059
do {
int EAX = 8; // из команды по адресу 0537105B
do {
// имитировать команду x86 shrd
// с помощью нескольких операций
EDX = EDX >>> 1; // сдвиг вправо без знака на 1 бит
int CF = EBX & 1; // запомнить младший бит EBX
if (CF == 1) { // CF представляет флаг переноса x86
EDX = EDX | 0x80000000; // вдвинуть младший бит EBX,
если 1
}
EBX = EBX >>> 1; // сдвиг вправо без знака на 1 бит
if (CF == 1) { // из команды по адресу 05371066
EBX = EBX ^ 0xC0000057; // из команды по адресу 0537106C
}
EAX--; // из команды по адресу 05371072
} while (EAX != 0); // из команды по адресу 05371073
EDX = EDX >>> 24; // сдвиг вправо без знака на 24 бита
 EAX = getByte(toAddr(ESI)); // из команды по адресу 05371078
ESI++;
EAX = EAX ^ EDX; // из команды по адресу 05371079
clearListing(toAddr(EDI)); // очистить байт, чтобы можно было
// его изменить
 setByte(toAddr(EDI), (byte)EAX); // из команды по адресу 0537107B
EDI++;
ECX--; // из команды по адресу 0537107C
} while (ECX != 0); // из команды по адресу 0537107D
}
}

Пытаясь эмулировать какую-нибудь команду, обращайте
особое внимание на размеры данных и псевдонимы регистров.
В этом примере нам нужно выбрать подходящий размер данных и переменную для правильной реализации команд x86
LODSB (загрузить байт строки) и STOSB (сохранить байт строки).
Эти команды записывают (LODSB) и читают (STOSB) младшие
658

ГЛАВА 21

8 бит регистра EAX1, оставляя старшие 24 бита без изменения.
В Java не существует никакого другого способа разбить переменную на битовые участки, кроме как использовать различные поразрядные операции для маскирования и последующего объединения. Конкретно для команды LODSB  корректная
эмуляция имела бы такой вид:
EAX = (EAX & 0xFFFFFF00) | (getByte(toAddr(ESI)) & 0xFF);

Здесь мы сначала очищаем младшие 8 бит переменной EAX,
а затем записываем в них другие 8 бит с помощью операции OR.
В алгоритме декодирования Burneye в регистр EAX записывается значение 8 в начале каждой итерации внешнего цикла, это
эквивалентно обнулению старших 24 битов EAX. Поэтому мы решили упростить реализацию LODSB , проигнорировав запись
в старшие 24 бита EAX. Особо задумываться о реализации STOSB
 нет нужды, потому что функция setByte требует привести
второй аргумент к типу byte.
После выполнения скрипта декодирования Burneye результат дизассемблирования будет отражать все изменения, которые
в обычной ситуации не были бы видны до выполнения обфусцированной программы в Linux. Если деобфускация произведена
правильно, то мы, вероятно, увидим гораздо больше осмысленных строк, воспользовавшись командой поиска SearchFor
Strings. Чтобы убедиться в этом, возможно, придется сначала
щелкнуть по значку Refresh в окне поиска строк.
Осталось (1) определить, куда возвращает управление функция декодирования, учитывая, что она с самого начала извлекла из стека адрес возврата, и (2) убедить Ghidra правильно
отобра­зить декодированные значения байтов как команды или
данные. Функция декодирования из Burneye заканчивается
такими тремя командами:
0537107f POPAD
05371080 POPFD
05371081 RET
1

Младшие 8 бит регистра EAX называются также регистром AL.
Анализ обфусцированного кода

659

Напомним, что функция первым делом извлекла из стека
свой адрес возврата, а это означает, что оставшиеся в стеке значения были помещены туда вызывающей стороной. Команды
POPAD и POPFD противоположны командам PUSHAD и PUSHFD, выполненным в начале стартовой последовательности Burneye,
которую мы еще раз повторяем ниже:
entry
 05371035 PUSH dword ptr [DAT_05371008]
0537103b PUSHFD
0537103c PUSHAD

Стало быть, в стеке осталось только то, что было помещено
в первой строке последовательности entry . Это и есть тот
адрес, на который передаст управление функция декодирования после возврата, и именно с него мы должны продолжить
анализ защищенного двоичного файла.
Из рассмотренного примера может сложиться впечатление,
будто написать скрипт декодирования или распаковки обфусцированного двоичного файла – относительно несложное дело. Это
действительно так в случае инструмента Burneye, в котором не используются особо изощренные алгоритмы обфускации. Деобфус­
кация в Ghidra файлов, упакованных такими хитроумными утилитами, как ASPack или tElock, потребовала бы больше усилий.
К преимуществам скриптовой деобфускации можно отнести
тот факт, что анализируемый двоичный файл не нужно выполнять и что можно создать работающий скрипт, не понимая до
конца все детали алгоритма деобфускации. Последнее утверждение может показаться противоречащим интуиции, поскольку естественно думать, что прежде чем эмулировать алгоритм,
в нем необходимо досконально разобраться. Но для применения описанной здесь и в главе 14 процедуры нужно лишь хорошо понимать все команды процессора, участвующие в процессе
деобфускации. Если корректно реализовать все операции процессора в Ghidra и правильно расположить их, следуя листингу дизассемблера, то получится скрипт, который имитирует
действия программы, даже если вы не понимаете все детали
высокоуровневого алгоритма, эти действия содержащего.
Но у скриптового подхода есть и недостаток – хрупкость
скриптов. Если алгоритм деобфускации изменится в результа660

ГЛАВА 21

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

Эмуляторная деобфускация
При создании скриптов деобфускации вновь и вновь возникает
вопрос об эмуляции системы команд процессора, чтобы скрипт
вел себя точно так же, как деобфусцируемая программа. Эмуляторы команд позволяют передать эту работу или хотя бы ее
часть эмулятору и значительно сократить время, затрачиваемое на деобфускацию в Ghidra. Эмуляторы расположены между скриптами и отладчиками и иногда оказываются более гибкими, чем отладчики. Например, эмулятор может эмулировать
команды MIPS на платформе x86 или команды из двоичного
ELF-файла для Linux на платформе Windows.
Эмуляторы различаются по своим возможностям. Как минимум, эмулятору нужен поток байтов команд и достаточная
память для имитации стека и регистров процессора. Более развитые эмуляторы могут предоставлять доступ к эмулируемым
аппаратным устройствам и службам операционной системы.

Класс Emulator
По счастью, Ghidra предлагает развитый класс Emulator, а также EmulatorHelper, который реализует высокоуровневую абстракцию типичной функциональности эмулятора и позволяет
быстро и легко создавать скрипты эмуляции. В главе 18 мы познакомились с p-кодом – промежуточным представлением реального ассемблерного кода, – рассказали, как это позволяет
Анализ обфусцированного кода

661

декомпилятору работать с разнообразными целевыми архитектурами. Но p-код также поддерживает функциональность эмулятора, а класс ghidra.pcode.emulate.Emulate дает возможность
эмулировать одну команду p-кода.
Мы можем использовать классы Ghidra для построения эмуляторов широкого круга процессоров. Как и для всех пакетов
и классов Ghidra, эта функциональность документирована
в формате Javadoc, а для доступа к ней нужно щелкнуть по красному значку плюса в окне диспетчера скриптов. Если вас интересует разработка эмуляторов, то рекомендуем почитать описание методов эмулятора, используемых в следующем примере.

Крякни crackme
crackme – это задачка, составляемая специалистами по обратной
разработке для себе подобных. Название происходит от слова
«crack» – взломать (на жаргоне «крякнуть»), – означающего обход
ограничений на копирование или использование программы –
одно из самых бесчестных применений обратной разработки. Головоломки типа crackme – легальный способ попрактиковаться,
а также возможность для автора задачи и человека, который ее
анализирует, продемонстрировать свои таланты.
Типичная crackme получает данные от пользователя, каким-то
образом преобразует их и сравнивает результат преобразования с заранее вычисленным. Желающий решить задачу обычно
получает только откомпилированный исполняемый файл, содержащий код преобразования, и результат преобразования неизвестных данных. Задача считается решенной, если удалось вычислить входные данные, на основе которых был сгенерирован
результат. Это означает, что вы разобрались в преобразовании
настолько хорошо, что можете построить обратную функцию.

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

ГЛАВА 21

 unsigned char check_access[] = {
0xf0, 0xed, 0x2c, 0x40, 0x2c, 0xd8, 0x59, 0x26, 0xd8,
0x59, 0xc1, 0xaa, 0x31, 0x65, 0xaa, 0x13, 0x65, 0xf8, 0x66
};
unsigned char key = 0xa5;
void unpack() {
for (int ii = 0; ii < sizeof(check_access); ii++) {
 check_access[ii] ^= key;
}
}
void do_challenge() {
int guess;
int access_allowed;
int (*check_access_func)(int);
 unpack();
printf("Введите правильное целое число: ");
scanf("%d", &guess);
check_access_func = (int (*)(int))check_access;
access_allowed = check_access_func(guess);
if (access_allowed) {
printf("Доступ разрешен!\n");
} else {
printf("Доступ запрещен!\n");
}
}
int main() {
do_challenge();
return 0;
}

Даже при наличии исходного кода для решения этой crackme
пришлось бы приложить некоторые усилия из-за наличия закодированного участка . Декомпилятор Ghidra часто оказывается замечательным помощником в решении crackme, но
у этой конкретной есть интересные особенности, затрудняющие
процесс. Ghidra видит только закодированное тело функции.
Во время выполнения функция unpack  декодирует функцию
check_access  до ее вызова в точке . Эта crackme обфусцирована, и мы можем написать эмуляторный скрипт Ghidra, который поможет ее решить. В отличие от предыдущего примера,
этот эмулятор не просто решит частную задачу, но сможет эмулировать до некоторой степени произвольный код.

Анализ обфусцированного кода

663

Шаг 1: поставить задачу

Наша задача состоит в том, чтобы спроектировать и разработать простой эмулятор, который позволит выбрать участок
листинга дизассемблера и эмулировать содержащиеся в нем
команды. Эмулятор нужно будет добавить в Ghidra и сделать доступным в качестве скрипта. Например, если мы выберем функцию unpack в качестве crackme и выполним скрипт, то наш эмулятор должен будет использовать ключ key для распаковки массива
check_access и сообщить нам решение задачи. Скрипт запишет
байты распакованного кода в память программы в Ghidra.

Шаг 2: создать проект скрипта в Eclipse

Чтобы создать проект SimpleEmulator, воспользуемся командой GhidraDevNewGhidra Script Project. В результате
в Eclipse будет создана папка SimpleEmulator с подпапкой
Home scripts (см. рис. 15.16), куда нужно будет поместить новый скрипт. Нам еще нужно создать сам скрипт и ввести метаданные, содержащие документацию и позволяющие каталогизировать скрипт. Метаданные, введенные в диалоговом окне
создания скрипта, уже включены в файл, и, как показано на
рис. 21.7, нам осталось сделать только одну вещь: Add script
code here (Добавить сюда код скрипта).

Рис. 21.7. Шаблон скрипта SimpleEmulator
664

ГЛАВА 21

Шаг 3: написать эмулятор

Мы знаем, что Eclipse будет подсказывать, что нужно импортировать, по мере разработки, поэтому можем сразу приступить к кодированию и добавлять рекомендованные предложения import,
когда Eclipse сочтет необходимым. В классе SimpleEmulator нам
понадобятся следующие переменные экземпляра:
private EmulatorHelper emuHelper;
private Address executionAddress;
private Address endAddress;

// объект типа EmulatorHelper
// инициализируется началом выбранной области
// конец выбранной области

В комментариях описано назначение переменных. Переменная executionAddress инициализируется начальным адресом
выбранной области, а потом продвигается вперед.
Шаг 3-1: настроить эмулятор
Первое, что нужно сделать в методе скрипта run, – создать объект помощника эмулятора и активировать прослеживание всех
операций записи памяти в эмулятор, чтобы измененные значения можно было записать назад в текущую программу. Акт создания объекта играет роль блокировки, аналогичной блокировке, которую браузер кода ставит на открытый двоичный файл.
emuHelper = new EmulatorHelper(currentProgram);
emuHelper.enableMemoryWriteTracking(true);

Шаг 3-2: выбрать подлежащий эмуляции диапазон адресов
Поскольку мы хотим, чтобы пользователь сам выбирал подлежащий эмуляции участок кода, нужно проверить, что в окне
листинга что-то выбрано. В противном случае мы выдаем сообщение об ошибке.
if (currentSelection != null) {
executionAddress = currentSelection.getMinAddress();
endAddress = currentSelection.getMaxAddress().next();
} else {
println("Ничего не выбрано");
return;
}
Анализ обфусцированного кода

665

Шаг 3-3: подготовиться к эмуляции
Нужно, чтобы в выбранной области указатель executionAddress
был направлен на команду, – тогда можно будет определить
начальный контекст процессора, инициализировать указатель
стека и поставить точку останова в конце выбранной области.
Флаг continuing говорит, продолжаем мы эмуляцию или только приступаем к ней, и соответственно определяет, какой вариант метода emuHelper.run вызывается на шаге 3-4.
Instruction executionInstr = getInstructionAt(executionAddress);
if (executionInstr == null) {
printerr("Команда не найдена по адресу: " + executionAddress);
return;
}
long stackOffset = (executionInstr.getAddress().getAddressSpace().
getMaxAddress().getOffset() >>> 1) - 0x7fff;
emuHelper.writeRegister(emuHelper.getStackPointerRegister(), stackOffset);
// Поставить точку останова на конечном адресе
emuHelper.setBreakpoint(endAddress);
// Сбросить continuing в false, поскольку мы уже начали эмуляцию
boolean continuing = false;;

Шаг 3.4: выполнить эмуляцию
Дочитав до этого места, вы уже должны узнавать некоторые
функции Ghidra API, описанные в главе 14 (например, monitor.
isCancelled). Эмуляция производится в цикле, пока не будет
выполнено определенное нами условие.
 while (!monitor.isCancelled() &&
!emuHelper.getExecutionAddress().equals(endAddress)) {
if (continuing) {
emuHelper.run(monitor);
} else {
emuHelper.run(executionAddress, executionInstr, monitor);
}
 executionAddress = emuHelper.getExecutionAddress();
// определить, почему остановился эмулятор, и обработать
// возможные причины
 if (emuHelper.getEmulateExecutionState() ==

EmulateExecutionState.BREAKPOINT) {

666

ГЛАВА 21

continuing = true;
} else if (monitor.isCancelled()) {
println("Эмуляция прервана по адресу 0x" + executionAddress);
continuing = false;
} else {
println("Ошибка эмуляции по адресу 0x" + executionAddress +

": " + emuHelper.getLastError());
continuing = false;
}
 writeBackMemory();
if (!continuing) {
break;
}
}

В этом примере эмуляция продолжается, пока не выполнено хотя бы одно из следующих условий: монитор обнаружил,
что пользователь прервал работу, достигнут конец выбранной
области команд или произошла ошибка . После остановки
эмулятора мы должны обновить текущий адрес выполнения
, а затем обработать причину остановки . И на последнем
шаге мы вызываем метод writeBackMemory() .
Шаг 3-5: запись памяти обратно в программу
Ниже показана реализация метода writeBackMemory() . Этот
эмулятор будет тестироваться на процедуре распаковки, которая в конечном итоге записывает байты в память. Изменения, произведенные эмулятором, существуют только в его
рабочей памяти, содержимое которой нужно записать назад
в двоичный файл, чтобы изменения, ставшие результатом выполнения команд в процедуре распаковки, были отражены
в листинге и других пользовательских интерфейсах. Ghidra
предоставляет необходимую для этого функциональность
в классе emulatorHelper.
private void writeBackMemory() {
AddressSetView memWrites = emuHelper.getTrackedMemoryWriteSet();
AddressIterator aIter = memWrites.getAddresses(true);
Memory mem = currentProgram.getMemory();
while (aIter.hasNext()) {
Address a = aIter.next();
MemoryBlock mb = getMemoryBlock(a);
Анализ обфусцированного кода

667

if (mb == null) {
continue;
}
if (!mb.isInitialized()) {
// инициализировать память
try {
mem.convertToInitialized(mb, (byte)0x00);
} catch (Exception e) {
println(e.toString());
}
}
try {
mem.setByte(a, emuHelper.readMemoryByte(a));
} catch (Exception e) {
println(e.toString());
}
}
}

Шаг 3-6: освободить ресурсы
На этом шаге мы должны освободить ресурсы и блокировку, поставленную на текущую программу. То и другое делается одним предложением:
emuHelper.dispose();

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

Шаг 4: добавить скрипт в Ghidra

Для добавления скрипта в Ghidra нужно лишь поместить его
туда, где Ghidra сможет его найти. Если проект скрипта настроен как связанный, то Ghidra уже знает, где искать скрипт.
Если же проект не был связан (или если вы создавали скрипт
в другом редакторе), то сохраните скрипт в одном из каталогов
скриптов Ghidra, как описано в главе 14.
668

ГЛАВА 21

Шаг 5: протестировать скрипт в Ghidra

Для тестирования скрипта загрузим двоичный файл, ассоциированный с задачей crackme. Загрузив файл и перейдя к функции unpack, мы заметим, что она содержит ссылку на метку
check_access:
0010077d 48 8d 05 8c 08 20 00 LEA RAX,[check_access]

Код в окне декомпилятора содержит следующую строку, которая не приближает нас к решению crackme:
check_access[(int)local_c] = check_access[(int)local_c] ^ key;

Двойной щелчок по check_access в окне листинга перемещает нас по адресу 00301010, но команд, принадлежащих функции, здесь не видно.
00301010 f0 ed 2c 40 2c d8 59
26 d8 59 c1 aa 31 65
aa 13 65 f8 66

undefined1[19]

Если бы мы попробовали дизассемблировать эту область, то
Ghidra сообщила бы о плохих данных. Окно декомпилятора
тоже ничем не помогает. Что ж, воспользуемся нашим скриптом
и посмотрим, удастся ли эмулировать функцию unpack. Выделим команды, составляющие unpack, откроем диспетчер скриптов и запустим наш скрипт. Ни в функции unpack, ни в окне декомпилятора изменений не видно. Но, перейдя к check_access
(00301010), мы увидим, что содержимое изменилось!
00301010 55 48 89 e5 89 7d
undefined1[19]
fc 83 7d fc 64 0f
94 c0 0f b6 c0 5d c3

Мы можем очистить эти байты кода (клавиша C), а затем
диз­ассемблировать (клавиша D). Получится такой результат:

Анализ обфусцированного кода

669

check_access
00301010 55 PUSH RBP
00301011 48 89 e5 MOV RBP,RSP
00301014 89 7d fc MOV dword ptr [RBP + -0x4],EDI
00301017 83 7d fc 64 CMP dword ptr [RBP + -0x4],100
0030101b 0f 94 c0 SETZ AL
0030101e 0f b6 c0 MOVZX EAX,AL
00301021 5d POP RBP
00301022 c3 RET

А вот как выглядит соответствующий код в окне декомпилятора:
ulong UndefinedFunction_00301010(int param_1)
{
return (ulong)(param_1 == 100);
}

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

РЕЗЮМЕ
В наши дни обфусцированные вредоносные программы –
скорее, правило, а не исключение. Любая попытка изучить
внутренние операции вредоноса почти наверняка потребует
деобфускации того или иного типа. Какой бы подход ни выбрать – динамический с применением отладчика или не рис­
ковать и остановиться на скриптах либо эмуляции, – конечной целью является получение деобфусцированного двоичного
670

ГЛАВА 21

файла, который можно полностью дизассемблировать и проанализировать.
В большинстве случаев для конечного анализа применяются
такие инструменты, как Ghidra. Имея в виду эту конечную цель
(т. е. использование Ghidra для анализа), разумно попытаться
использовать Ghidra на всех этапах – от начала до конца. Методы, описанные в этой главе, призваны продемонстрировать,
что Ghidra умеет гораздо больше, чем просто генерировать лис­
тинги дизассемблера, а в следующей главе мы воспользуемся полученными знаниями и покажем, как с помощью Ghidra
вносить исправления в листинги.

22

ИЗМЕНЕНИЕ ДВОИЧНОГО
КОДА

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

В этой главе мы не собираемся учить вас плохому, а обсудим общие проблемы модификации двоичного файла с целью
отра­зить произведенные в Ghidra изменения. В главе 14 мы
познакомились с функцией setByte, а в главе 21 показали, как
с по­мощью эмуляторных скриптов изменить содержимое программы, загруженной в Ghidra. Но все эти методы лишь изменяют то, что было загружено в Ghidra, и никак не влияют на оригинальный двоичный файл, обработанный Ghidra в процессе
импорта. Чтобы завершить процесс исправления, нужно знать,
как заставить Ghidra записать изменения обратно в файл на
диске. Мы также обсудим проблемы, которые могут представлять различные типы таких исправлений, или «заплат».

ПЛАНИРОВАНИЕ ЗАПЛАТЫ
Процесс латания файла обычно состоит из следующих шагов.
1. Определить тип заплаты. Он часто зависит от причины, по
которой вы собираетесь изменять двоичный файл (см. выше).
2. Определить место (или места), которые нужно залатать.
Обычно этому предшествует исследование и анализ изменяемой программы.
3. Спланировать содержимое заплаты. Могут потребоваться
новые данные, новый машинный код или то и другое. В любом случае изменения нужно тщательно продумать, чтобы
не «поломать» программу.
4. Воспользоваться Ghidra, чтобы заменить существующее содержимое программы (данные или код) новым.
5. Воспользоваться Ghidra, чтобы проверить, что изменения
реализованы корректно.
6. Воспользоваться Ghidra, чтобы экспортировать изменения
в новый двоичный файл.
7. Проверить, что новый двоичный файл ведет себя, как было
задумано. При необходимости повторить, начиная с шага 2.
Иногда многие из этих шагов почти тривиальны, а иногда
очень сложны. В следующих разделах мы рассмотрим те шаги,
на которых Ghidra может помочь, и обсудим ситуации, когда
приходится напрягать способности – свои или Ghidra – до последнего предела. Начнем с шага 2 и поговорим о том, как Ghidra
помогает находить интересные с точки зрения латания места.
674

ГЛАВА 22

ПОИСК ТОГО, ЧТО НУЖДАЕТСЯ
В ИЗМЕНЕНИИ
Что нужно изменить, зависит от характера заплаты. Для модификации заставки или строк требуется найти соответствующие данные. Для изменения логики программы нужно модифицировать
или вставить код. В этом случае, возможно, придется изрядно потрудиться над обратной разработкой, чтобы только найти места,
нуждающиеся в модификации. Многие средства Ghidra, облегчающие такого рода деятельность, описаны в предыдущих главах.
Рассмотрим некоторые возможности, полезные для латания.

Поиск в памяти
Если заплата подразумевает модификацию данных программы, то для поиска места, которое нужно залатать, полезен прежде
всего поиск в памяти. Самым общим видом поиска в памяти является пункт меню браузера кода SearchMemory (клавиша
S), открывающий окно, показанное на рис. 22.1 (все дополнительные средства раскрыты). Диалоговое окно поиска в памяти
обсуждалось в главе 6.
Диалоговое окно поиска в памяти особенно полезно, когда
требуется найти в двоичном файле точно известные данные,
например строки или последовательности шестнадцатеричных цифр. Если поиск завершится успешно, то все связанные
окна будет позиционированы, так чтобы найденные байты
были видны, или если выбран переключатель Search All (Искать все), то откроется новое диалоговое окно, содержащее список всех адресов, по которым обнаружились совпадения. Для
очень больших двоичных файлов полезно ограничить поиск
областями программы Instructions (Команды), Defined Data
(Определенные данные), Undefined Data (Неопределенные
данные) и т. д., которые с большой вероятностью могут содержать искомое, а все остальные области исключить, сбросив соответствующие флажки.

Изменение двоичного кода

675

Рис. 22.1. Диалоговое окно поиска в памяти
ПРИМЕЧАНИЕ

Хотя меню SearchMemory предлагает настраиваемый в очень широких пределах поиск общего
назначения в Ghidra, этот поиск производится
по исходным байтам, хранящимся в базе данных,
тогда как другие виды поиска могут быть в большей степени ориентированы на то, что вы ищете.
Так, SearchMemory не стоит выбирать, если
требуется искать по телам комментариев, добавленных вами в программу. Дополнительные сведения о поиске по самому листингу дизассемблера см.
в разделе «Поиск по тексту программы» главы 6.

Поиск прямых ссылок
В главе 20 мы пользовались командой SearchFor Direct
References для поиска в двоичном файле всех вхождений заданного адреса. Чаще всего этот вид поиска используется для
нахождения указателей на интересные данные, когда Ghidra
676

ГЛАВА 22

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

Поиск командных паттернов
Пункт SearchFor Instruction Patterns (Поиск Командных
паттернов) ищет последовательность команд, отвечающую заданному образцу. Определяя командный паттерн, нужно соблюс­
ти тонкий баланс между слишком специфичными и слишком
общими паттернами. Рассмотрим пример, иллюстрирующий эту
мысль. Предположим, что листинг содержит функцию cleanup_
and_exit, которая завершает программу:
int test_even(int v) {
return (v % 2 == 0);
}
int test_multiple_10(int v) {
return (v % 10 == 0);
}
int test_lt_100(int v) {
return v < 100;
}
int test_gte_20(int v) {
return v >= 20;
}
 void cleanup_and_exit(int rv, char* s) {
printf("Результат: %s\n", s);
exit(rv);
}
void do_testing() {
int v;
srand(time(0));
v = rand() % 150;
printf("Тестируется %d\n", v);
 if (!test_even(v)) {
cleanup_and_exit(-1, "тест на четность не прошел");
}
if (test_multiple_10(v)) {
cleanup_and_exit(-2, "тест на кратность 10 не прошел");
}
if (!test_lt_100(v)) {
cleanup_and_exit(-3, "тест на < 100 не прошел");
Изменение двоичного кода

677

}
if (!test_gte_20(v)) {
cleanup_and_exit(-4, "тест на > 20 не прошел");
}
// все тесты прошли, приступаем к интересной работе
 system("/bin/sh");
cleanup_and_exit(0, "успешно!");
}
int main() {
do_testing();
return 0;
}

Функция do_testing выполняет серию тестов . Если хотя бы
один тест не прошел, то вызывается функция cleanup_and_exit
, и программа завершается. Если все тесты прошли, то выполняется какой-то интересный код . Наша задача – определить, куда наложить заплату, чтобы программа считала, что
все тесты прошли, и мы могли добраться до интересного кода.
Загрузив двоичный файл в Ghidra, мы можем поискать все
обращения к cleanup _and_exit, чтобы понять, что нужно латать, чтобы все тесты проходили, сколько бы их ни было. Можно рассмотреть несколько вариантов.
► Можно было бы исправить саму функцию, чтобы программа не завершалась, когда тест не проходит, а продолжала
работать. Это плохое решение, потому что функция используется также для естественного выхода из программы после завершения интересной работы.
► Можно воспользоваться поиском или XREF’ами на
cleanup_and_exit. Это дало бы нам все вызовы, но мы хотим залатать только часть.
► Можно было бы выявить командный паттерн, общий для
всех вызовов, и воспользоваться поиском SearchFor
Instruction Patterns для нахождения тех вызовов, которые нужно изменить.
Для поиска необходимо понять, какой паттерн был бы полезен. Все тесты, которые мы хотим успешно пройти, в окне
листинга имеют такой вид:

678

ГЛАВА 22

001008af
001008b4
001008b6
001008b8
001008bf
001008c4

CALL
TEST
JNZ
LEA
MOV
CALL

test_even
EAX,EAX
LAB_001008c9
RSI,[s_failed_even_test_00100a00]
EDI,0xffffffff
cleanup_and_exit

Попробуем поискать эту последовательность, для чего выделим ее и выберем из меню пункт SearchFor Instruction
Patterns. В результате будет автоматически заполнено окно
поиска командных паттернов, показанное на рис. 22.2.

Рис. 22.2. Окно поиска командных паттернов, когда все поля
выбраны
Нажав кнопку Search All (Искать все), мы увидим только
один результат (то самое место, с которого мы и начали поиск),
как показано на рис. 22.3.

Изменение двоичного кода

679

Рис. 22.3. Результаты поиска командного паттерна, когда все
поля выбраны
Проблема в том, что мы включили операнды, меняющиеся
от теста к тесту. Например, в первом вызове операндом является адрес конкретной тестовой функции. Мы можем отменить
выбор отдельных компонент (мнемонический код и операнды)
любой команды в паттерне, чтобы сделать его более общим; это
показано на рис. 22.4. При поиске все, что не выбрано, рассмат­
ривается как метасимвол.

Рис. 22.4. Окно поиска командных паттернов, когда некоторые
операнды не выбраны
Нажав кнопку Search All, когда часть операндов не выбрана, мы увидим три результата (рис. 22.5).
680

ГЛАВА 22

Рис. 22.5. Результаты поиска командного паттерна, когда
некоторые операнды не выбраны
Мы все еще не нашли обращение к функции test_multiple_10,
в котором используется команда JZ, а не JNZ. Отменив выбор
мнемонического кода в команде JNZ и повторив поиск, мы получим результаты, показанные на рис. 22.6, где присутствуют все
четыре обращения, которые мы хотим залатать, но не присутствует последнее обращение к cleanup_and_exit, которое нужно
оставить как есть.

Рис. 22.6. Результаты поиска командного паттерна, когда JNZ
и некоторые операнды не выбраны
У такого поиска есть и другие применения, помимо нахождения потенциальных последовательностей команд для латания.
Его можно использовать для анализа уязвимостей, поиска конИзменение двоичного кода

681

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

Поиск конкретных типов поведения
Поведение программы определяется командами, которые она
выполняет, в сочетании с данными, которые она обрабатывает. Когда латание подразумевает модификацию поведения
программы, найти это поведение обычно гораздо труднее, чем
данные, подлежащие изменению. Поскольку мы не можем
предсказать точную последовательность команд, которую компилятор изберет для реализации исходного кода, использовать
средства автоматического поиска Ghidra для отыскания места
наложения заплаты затруднительно. Нахождение конкретного поведения сводится к старому доброму анализу функций
программы с помощью описанных в книге методов.
Помимо тщательного анализа всех функций в двоичном файле или не менее тщательного обхода дерева вызовов, начиная
с хорошо известной функции, например main, есть еще два распространенных способа идентификации интересных функций,
которые опираются на имя функции (в предположении, что
в двоичном файле присутствуют символы) и использование перекрестных ссылок из «интересных» данных, чтобы обратным
проходом отследить интересные функции. Например, если нас
интересуют функции, относящиеся к аутентификации, то можно было бы поискать такие типичные строки, как "Please enter
your password:" (Введите пароль) и "Authentication failed"
(Ошибка аутентификации).
Такие строки часто обрамляют процедуру аутентификации,
и нахождение функций, которые на них ссылаются, может
значительно сократить пространство поиска других связанных
с аутентификацией функций.
И в этом случае природа данных, которые могут привести
к интересным функциям, зависит от цели латания. Но какой бы
подход вы ни избрали для нахождений кандидатов на латание,
обязательно проверяйте, что функция действительно реализует
поведение, которое вы хотите модифицировать. В частности, с подозрением относитесь к именам функций, потому что нет такого
закона, чтобы поведение функции соответствовало ее имени.
682

ГЛАВА 22

НАЛОЖЕНИЕ ЗАПЛАТЫ
Наконец-то ваш тяжкий труд и настойчивость увенчались успехом, и вы нашли код или данные, которые нужно модифицировать. И что теперь? В предположении, что вы уже разработали
замену, которую намереваетесь включить в двоичный файл,
и точно знаете, куда ее поместить, пришло время подвергнуть
испытанию средства модификации программы, которыми располагает Ghidra.
Прежде всего нужно сравнить размеры заплаты и заменяемого содержимого. Если размер заплаты меньше или равен,
то вам повезло, потому что заплата поместится в память, отведенную для оригинального содержимого. А вот если заплата
больше, то задачка становится похитрее, и мы рассмотрим этот
случай чуть ниже.

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

Средство просмотра байтов
На рис. 22.7 показано средство просмотра байтов Ghidra
(WindowBytes). Это стандартное шестнадцатеричное представление неформатированного содержимого листинга, начиная с текущего места, синхронизированное со всеми связанными окнами.

Изменение двоичного кода

683





Рис. 22.7. Средство просмотра байтов
Средство просмотра байтов может также играть роль шестнадцатеричного редактора, если переключить его в режим редактирования ; это удобно, когда нужно изменить несколько байтов.
К сожалению, Ghidra не позволяет редактировать байты,
являющиеся частью существующей команды. Обойти это ограничение можно, очистив команду в окне листинга (щелкнуть
правой кнопкой мыши и выбрать из меню пункт Clear Code
Bytes или нажать клавишу C). Значок Byte Viewer Options
(Параметры средства просмотра байтов)  открывает диалоговое окно, показанное на рис. 22.8, которое позволяет настроить
отображение в окне просмотра байтов.

Рис. 22.8. Диалоговое окно параметров средства просмотра
байтов
684

ГЛАВА 22

Если флажок Ascii отмечен, то в окно добавляется область
ASCII-кода (рис. 22.9), и тогда в режиме редактирования средство просмотра байтов может использоваться еще и как ASCIIредактор.

Рис. 22.9. Средство просмотра байтов с включенным показом
ASCII-кода
Закончив ввод новых значений, выйдите из режима редактирования и вернитесь в окно листинга, чтобы проверить правильность изменений.

Оформление изменений в виде скрипта
Если заплата достаточно длинная, то самый эффективный
способ модификации исходных байтов – написать скрипт. При
условии, что заплата представлена в виде массива байтов и задан ее начальный адрес, следующая функция применяет заплату внутри Ghidra:
public void patchBytes(Address start, byte[] patch) throws Exception {
Address end = start.add(patch.length);
 clearListing(start, end);
setBytes(start, patch);
}

Мы можем включить эту функцию в скрипт, который создает массив байтов заплаты из источника по вашему выбору
(например, путем объявления инициализированного массива
или загрузки содержимого файла). Обращение к clearListing
 необходимо, потому что Ghidra не позволяет модифицироИзменение двоичного кода

685

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

Использование ассемблера
Желая залатать код в двоичном файле, вы, скорее всего, будете
думать о замене одной ассемблерной команды на другую (например, замене CALL _exit на NOP) – это необязательно некорректно, но обходит стороной некоторые сложности, связанные
с латанием кода. Когда придет время фактически применить
заплату к программе, вы должны будете вставить не предложения ассемблерного кода, а соответствующие байты машинного кода, а это значит, что понадобится ассемблер для генерирования машинного кода всех новых команд, призванных
заменить старые.
Можно, конечно, написать ассемблерный код во внешнем редакторе, ассемблировать его внешним ассемблером (например,
nasm или as), извлечь машинный код1 и, наконец, вставить его
в программу, быть может, воспользовавшись для этого скриптом, как описано выше. Альтернативный подход – воспользоваться встроенным в Ghidra ассемблером, для чего достаточно
щелкнуть правой кнопкой мыши и выбрать из контекстного
меню пункт Patch Instruction (Изменить команду).
Спецификации на языке SLEIGH не только говорят Ghidra,
как транслировать машинный код на язык ассемблера, но
и позволяют выполнять трансляцию ассемблерного кода на
машинный язык, т. е. действовать в роли ассемблера. При
первом выборе из меню пункта Patch Instruction для данной
архитектуры Ghidra построит ассемблер на основе SLEIGHспецификации этой архитектуры. Вы увидите сообщение, показанное на рис. 22.10 (или похожее на него).

1

686

Флаг –f bin говорит nasm, что нужно выводить машинный код без заголовков файлов. При работе с as понадобится вторая утилита, например
objcopy, чтобы извлечь чистые байты кода из результирующего объектного
файла.

ГЛАВА 22

Рис. 22.10. Диалоговое окно рейтинга ассемблера
Разработчики Ghidra тестировали правильность сгенерированных Ghidra ассемблеров. Если ассемблер для некоторого процессора тестировался, то ему присваивается один из следующих
рейтингов (в порядке убывания правильности): платиновый,
золотой, серебряный, бронзовый и неудовлетворительный.
Непротестированным ассемблерам рейтинг не присваивается
(unrated). Дополнительные сведения о рейтингах ассемблеров
Ghidra, а также текущие рейтинги всех имеющихся ассемблеров можно найти в справке по Ghidra.
После закрытия окна рейтинга ассемблера Ghidra строит ассемблер по SLEIGH-спецификации процессора. Пока ассемблер
строится, Ghidra отображает окно, показанное на рис. 22.11.

Рис. 22.11. Окно ожидания ассемблера
Построив ассемблер, Ghidra заменяет выбранную команду
в окне листинга двумя полями ввода (см. рис. 22.12), в которых
можно изменить мнемонический код и операнды команды. Нажатие клавиши Esc отменяет изменения до их ассемблироваИзменение двоичного кода

687

ния, а нажатие Enter ассемблирует новую команду и заменяет
машинный код старой команды байтами новой.

Рис. 22.12. Ассемблирование новой команды
Поскольку ассемблеры построены по той же спецификации,
что и соответствующие дизассемблеры, они распознают тот
же синтаксис, что используется в окне листинга. Ассемблеры
Ghidra чувствительны к регистру и предлагают автодополнение при вводе команд. После ввода команды Ghidra восстанавливает обычное представление окна листинга. Если нужно
изменить другие команды, вы можете снова выбрать из меню
пункт Patch Instruction. Таким образом, для коротких заплат ассемблер Ghidra – это удобный способ одновременно ассемблировать команды и модифицировать программу.

Подводные камни при замене команд
Ассемблер Ghidra, конечно, позволяет по-быстрому изменить
одну команду, но новая команда может быть короче, длиннее
или того же размера, что и старая. Третий случай, когда длины
старой и новой команд одинаковы, наименее интересен. (Первые два случая могут возникнуть только в архитектурах с переменным размером команды, каковой является, в частности, x86.)
Рассмотрим первый случай, когда новая команда короче старой, как в следующем листинге.
;BEFORE:
0804851b 83 45 f4 01 ADD dword ptr [EBP + local_10],0x1
0804851f 83 45 f0 01 ADD dword ptr [EBP + local_14],0x1
;AFTER
0804851b
0804851d
0804851e
0804851f

688

66 90 NOP
f4 ?? F4h
01 ?? 01h
83 45 f0 01 ADD dword ptr [EBP + local_14],0x1

ГЛАВА 22

;FIXED:
0804851b
0804851d
0804851e
0804851f

66 90 NOP
90 NOP
90 NOP
83 45 f0 01 ADD dword ptr [EBP + local_14],0x1

В данном случае 4-байтовая команда ADD  заменяется
2-байтовой командой NOP . Ассемблер Ghidra сделал все возможное, чтобы заполнить освободившееся место префиксным
байтом (66)  перед кодом операции NOP (90). К сожалению,
новая команда все равно слишком коротка, и от прежней команды осталось два байта ; один из них транслируется в HLT
(нажмите клавишу D, чтобы дизассемблировать его), а второй
Ghidra вообще не может дизассемблировать, т. е. это не команда. Если бы вы залатали оригинальный двоичный файл таким
образом и запустили его, то, дойдя до этого места, программа,
вне всяких сомнений, «грохнулась» бы.
Ghidra никак не дает знать – если не считать символов ?? в лис­
тинге, – что имеет место проблема, потому что не понимает, для
чего вы вносите изменение, а «правильное» решение зависит от
конкретной цели. Если вы модифицируете команды в листинге,
не намереваясь их экспортировать, то можете воспользоваться
возможностью переопределения проваливания в контекстном
меню, чтобы обойти нежелательные байты1. Можно вместо этого
попросить Ghidra дизассемблировать неопределенные байты,
но крайне маловероятно, что при этом получится осмысленная
команда. Самое распространенное решение в этой ситуации –
заменить оставшиеся от оригинальной команды байты командами NOP  до начала следующей команды.
Если новая команда длиннее старой, то возникают другие
проблемы, показанные ниже:
;BEFORE:
08048502
08048504
08048507
0804850a
1

6a
ff
ff
e8

01 PUSH 0x1
75 f0 PUSH dword ptr [EBP + local_14]
75 08 PUSH dword ptr [EBP + param_1]
51 fe ff ff CALL read

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

689

;AFTER:
08048502 68 00 01 00 00 PUSH 0x100
08048507 ff 75 08 PUSH dword ptr [EBP + param_1]
0804850a e8 51 fe ff ff CALL read

В этом случае цель латания – прочитать 256 (0x100) байт вместо одного. Оригинальная 2-байтовая команда PUSH , которая
помещает третий аргумент read (длину) в стек, заменяется
5-байтовой командой PUSH , которая помещает в стек более
широкую константу. Дополнительные байты новой команды
затирают команду, которая помещала в стек второй аргумент
read (буфер чтения) .
Мало того что получившийся код передает read недостаточно
аргументов, так он еще и передает целое число вместо ожидаемого указателя. Как и в предыдущем примере, это почти
наверняка приведет к краху залатанной программы. Потенциальные решения этой проблемы нетривиальны и обсуждаются
в следующем разделе.

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

Слишком большие заплаты на код
Если заплата не помещается в память, занятую старыми командами, то ничего другого не остается, как найти или создать
неиспользуемую область достаточного размера, поместить код
заплаты в эту область, а затем вставить команду перехода (обход) на место оригинального кода, чтобы передать управление
новому. Как правило, придется еще добавить команду перехода
в конец заплаты, чтобы вернуть управление в нужное место залатанной функции. На рис. 22.13 схематически показан поток
управления в залатанной функции после установки обхода.
690

ГЛАВА 22

Функция
.
.
.

Обход
Заплата
.
.
.

.
.
.
.

Возврат из заплаты

Рис. 22.13. Функция с установленной заплатой
Неиспользуемое место для размещения заплаты должно
удовлетворять следующим условиям:
► его размер должен быть не меньше размера заплаты;
► оно должно находиться по адресу, который будет исполняться на этапе выполнения;
► оно должно инициализироваться из файла, иначе заплата не загрузится на этапе выполнения.
Начать поиск больших неиспользуемых исполняемых блоков
байтов проще всего с пещер в коде, которые могут присутствовать в двоичном файле. Пещера образуется, когда исполняемая секция файла, например .text, дополняется, чтобы выполнить требования о выравнивании, предъявляемые форматом
исполняемого файла. Пещеры часто встречаются в двоичных
PE-файлах для Windows, поскольку каждая секция такого
файла должна быть выровнена на границу 512 байт.
Поиск пещеры следует начинать с конца секции .text. Для
перехода в конец этой (или любой другой) секции дважды
щелк­ните по имени секции в окне деревьев программ в браузере кода, а затем прокрутите окно листинга до конца.
В нашем примере PE-файла конец секции .text в окне лис­
тинга выглядит следующим образом:
140012df8
140012df9
140012dfa
140012dfb
140012dfc
140012dfd
140012dfe
140012dff

??
??
??
??
??
??
??
??

00h
00h
00h
00h
00h
00h
00h
00h
Изменение двоичного кода

691

Это означает, что:
► Ghidra не классифицировала эти байты (??);
► байты инициализированы значением 00h;
► секция .text заканчивается по адресу 140012dff, который удовлетворяет требованиям выравнивания – секция должна начинаться на границе, кратной 512 байтам
(140012e00 кратно 0x200).
Прокрутив окно вверх до предыдущей команды (или воспользовавшись инструментом I в браузере кода с направлением поиска вверх), находим:
140012cbd
140012cbe
140012cbf
140012cc0

POP RBP
RET
?? CCh
?? 00h

Команда RET  – последняя осмысленная команда в данном
двоичном файле, и теперь мы можем вычислить размер этой
пещеры в коде: 0x140012e00 - 0x140012cbf = 0x141 (или 321
байт). Это означает, что в файле можно без труда разместить
заплаты общей длиной 321 байт. В предположении, что мы
разместили свой новый код с адреса 0x140012cbf, нужно будет
вставить в какое-то место существующего кода команду перехода на этот адрес, чтобы управление попало на заплату.
Если найти в коде пещеру достаточного размера не удается,
то нужно проявить изворотливость, чтобы все-таки отыскать
место для заплаты. В зависимости от параметров компилятора, заданных при сборке двоичного файла, возможно, удастся распределить заплату в нескольких промежутках между
функциями, предназначенных для выравнивания. Такие промежутки образуются, когда компилятор выравнивает начало
каждой функции на границу, кратную степени 2 (часто 16).
Если выравнивание функций включено, то средний размер
промежутка между функциями будет составлять align / 2 байт,
а максимальный align – 1 байт. В листинге ниже показан оптимальный (с точки зрения латания) промежуток (align = 16)
между двумя соседними функциями:
692

ГЛАВА 22

1400010a0 RET
 1400010a1 ?? CCh
1400010a2 ?? CCh
1400010a3 ?? CCh
1400010a4 ?? CCh
1400010a5 ?? CCh
1400010a6 ?? CCh
1400010a7 ?? CCh
1400010a8 ?? CCh
1400010a9 ?? CCh
1400010aa ?? CCh
1400010ab ?? CCh
1400010ac ?? CCh
1400010ad ?? CCh
1400010ae ?? CCh
 1400010af ?? CCh
**************************************************************
*
FUNCTION
*
**************************************************************

Все байты с 1400010a1  по 1400010af  можно безопасно перезаписать кодом заплаты.
Существуют и другие способы втиснуть код заплаты в двоичный
файл, некоторые из них подразумевают расширение существующих секций программы или включение новых. Любой подобный
метод манипулирования секциями требует согласованного изменения заголовков секций в двоичном файле. Поэтому такие методы сильно зависят от формата файла, и для их использования
нужно хорошо понимать структуру заголовков файла.

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

693

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

ЭКСПОРТ ФАЙЛОВ
Чтобы подтвердить, что внесенные изменения оказывают желаемое действие на поведение двоичного файла, нам необходимо
обновить оригинальный двоичный файл. В этом разделе мы обсудим некоторые средства экспорта Ghidra в контексте латания.
Пункт меню FileExport Program (ФайлЭкспортировать
программу) позволяет экспортировать информацию о программе в нескольких форматах. На рис. 22.14 показано открывающееся диалоговое окно экспорта.

Рис. 22.14. Диалоговое окно экспорта
694

ГЛАВА 22

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

Форматы экспорта из Ghidra
Ghidra поддерживает следующие форматы экспорта, но для
латания двоичных файлов полезен только один (binary).
ASCII. Используется для сохранения текстового представления программы, похожего на то, что отображается
в окне листинга. Параметры позволяют указать, какие
поля включать в выходной файл.
Binary. В этом формате порождается двоичный файл, он
наиболее полезен для латания приложений и обсуждается
в следующем разделе.
C/C++. Используется для сохранения сгенерированного
декомпилятором представления исходного кода программы вместе с объявлениями всех типов, известных диспетчеру типов данных. Эта возможность доступна также из
окна декомпилятора.
Ghidra Zip File. Под zip-файлом в Ghidra понимается сериализованное представление программы в виде объекта
Java, пригодное для импорта в другие экземпляры Ghidra.
HTML. Генерируется представление листинга программы
в формате HTML. Параметры, аналогичные задаваемым
при экспорте в формате Ascii, позволяют выбрать, какие
поля включаются в выходной файл. Метки и перекрестные
ссылки представляются гиперссылками, которые обеспечивают простую навигацию по сгенерированному файлу.
Intel Hex. Формат Intel Hex определяет ASCIIпредставление двоичных данных, обычно используемое
для программирования электрически стираемого программируемого ПЗУ (EEPROM).
Изменение двоичного кода

695

XML. Содержимое программы выводится в структурированном формате XML, а параметры позволяют указать,
какие программные конструкции включать в сгенерированный файл. Это можно делать также для отдельных
функций в окне декомпилятора, чтобы упростить отладку
декомпилированной функции. Хотя Ghidra включает парный загрузчик XML, при попытке экспорта в этом формате выдается предупреждение: «Warning: XML is lossy and
intended only for transferring data to external tools. GZF is
the recommended format for saving and sharing program
data» (В формате XML часть информации теряется, он
предназначен только для передачи данных во внешние
инструменты. Для сохранения и совместного использования данных программы рекомендуется формат GZF).

Двоичный формат экспорта
Экспорт в формате Binary используется для записи двоичного
содержимого в файл. Все инициализированные блоки памяти
(см. Window Memory Map) конкатенируются в один выходной файл. Будет ли выходной файл идентичен оригинальному,
зависит от модуля загрузчика, который импортировал файл.
Если использовался загрузчик Raw Binary, то идентичность
гарантируется, потому что все байты оригинального файла загружаются в один блок памяти. Другие загрузчики могут загружать не все байты (например, загрузчик PE загружает все
байты, а загрузчик ELF – не все).
Применив внесенные в Ghidra изменения, проверьте, что
сгенерированный файл содержит все ваши заплаты и исполняется. Если латался PE-файл, то экспорт в формате Binary генерирует залатанную версию оригинального двоичного файла.
Так же обстоит дело, если файл импортировался загрузчиком
Raw Binary. Правда, как обсуждалось в главе 17, для файла,
импортированного загрузчиком Raw Binary, форматирование
памяти приходится практически целиком выполнять вручную,
но за все приходится платить. К счастью, можно прибегнуть
к помощи скриптов и получить решение, работающее для любого загрузчика.
696

ГЛАВА 22

Экспорт с применением скрипта
Вместо того чтобы тщательно тестировать каждый загрузчик
Ghidra, чтобы понять, охватывают ли созданные им блоки памяти весь диапазон байтов файла, мы можем написать скрипт,
который сохранит залатанную версию программы и не будет
зависеть от загрузчика. Скрипт всегда будет обрабатывать весь
диапазон байтов оригинального файла вне зависимости от известной Ghidra карты памяти.
public void run() throws Exception {
Memory mem = currentProgram.getMemory();
 java.util.List fbytes = mem.getAllFileBytes();
if (fbytes.size() != 1) {
return;
}
 FileBytes fb = fbytes.get(0);
 File of = askFile("Choose output file", "Save");
FileOutputStream fos = new FileOutputStream(of, false);
writePatchFile(fb, fos);
fos.close();
}

Скрипт начинает работу с получения списка объектов
FileBytes . Объект FileBytes инкапсулирует все байты из импортированного файла программы и отслеживает оригинальное и модифицированное значение каждого байта. Ghidra позволяет импортировать сразу несколько файлов в один проект,
но этот скрипт обрабатывает байты только из первого импортированного файла (первый диапазон байтов файла) .
После запроса выходного файла  объект FileBytes и открытый поток OutputStream передаются функции writePatchFile,
которая отвечает за детали генерирования измененного исполняемого файла.
Чтобы представить карту памяти программы, загрузчики
Ghidra обрабатывают записи таблицы перемещений так же,
как это делает загрузчик во время выполнения. В результате
адреса в программе, помеченные как нуждающиеся в корректировке (поскольку для них имеются записи в таблице перемещений), модифицируются Ghidra – вместо оригинальных значений, как в файле, они содержат правильно перемещенные
Изменение двоичного кода

697

значения. При генерировании залатанной версии двоичного
файла мы не хотим включать байты, которые Ghidra модифицировала с целью перемещения.
Показанная ниже функция writePatchFile начинается с генерирования множества адресов, измененных во время выполнения (и в Ghidra), в соответствии с таблицей перемещений
программы.
public void writePatchFile(FileBytes fb, OutputStream os) throws
Exception {
Memory mem = currentProgram.getMemory();
Iterator relocs;
 relocs = currentProgram.getRelocationTable().getRelocations();
HashSet exclusions = new HashSet();
while (relocs.hasNext()) {
Relocation r = relocs.next();
 AddressSourceInfo info = mem.getAddressSourceInfo(r.getAddress());
for (long offset = 0; offset < r.getBytes().length; offset++) {
 exclusions.add(info.getFileOffset() + offset);
}
}
 saveBytes(fb, os, exclusions);
}

Имея итератор по таблице перемещений программы ,
функция получает объект AddressSourceInfo для каждой записи в таблице .
Объект AddressSourceInfo сопоставляет адресу в программе
дисковый файл и смещение в этом файле, с которого был загружен соответствующий байт. Смещение каждого перемещаемого
байта добавляется в множество смещений , которые должны
игнорироваться при генерировании окончательного залатанного файла. В самом конце вызывается функция saveBytes ,
которая записывает залатанную версию текущего файла. Эта
функция показана ниже:
public void saveBytes(FileBytes fb, OutputStream os, Set
exclusions)
throws Exception {
long begin = fb.getFileOffset();
long end = begin + fb.getSize();

698

ГЛАВА 22

 for (long offset = begin; offset < end; offset++) {
 int orig = fb.getOriginalByte(offset) & 0xff;
 int mod = fb.getModifiedByte(offset) & 0xff;
if (!exclusions.contains(offset) && orig != mod) {
 os.write(mod);
}
else {
 os.write(orig);
}
}
}

Эта функция обходит весь диапазон байтов файла  и смот­
рит, какой байт сохранить в файле: оригинальный или модифицированный.
Для каждого смещения в файле используются методы
класса FileBytes, чтобы получить оригинальное значение
байта , загруженное из импортированного файла, текущее значение , которое могло быть модифицировано самой
Ghidra или пользователем. Если оригинальное значение
отличается от текущего и байту не соответствует никакая
запись в таблице перемещений, то в выходной файл записывается модифицированное значение , в противном случае
оригинальное .
И в заключение рассмотрим пример латания двоичного файла, после чего убедимся, что заплата работает, как мы рассчитывали.

ПРИМЕР: ЛАТАНИЕ ДВОИЧНОГО
ФАЙЛА
Следующий пример продемонстрирует латание в контексте.
Предположим, что имеется вредоносная программа, которая
проверяет наличие отладчика, и если таковой обнаружен, то
выходит, не позволяя изучить свое поведение. Ниже приведен исходный код, где эта идея реализована в тривиальной
программе.

Изменение двоичного кода

699

int is_debugger_present() {
return ptrace(PTRACE_TRACEME, 0, 0, 0) == -1;
}
void do_work() {
 if (is_debugger_present()) {
printf("Не буду работать под отладчиком - выхожу!\n\n");
exit(-1);
}
// А здесь происходит что-то интересное
printf("Убедились, что отладчика нет, поэтому займемся\n"

"интересными вещами, которые аналитики видеть\n"

"не должны!\n\n");
}
int main() {
do_work();
return 0;
}

Программа проверяет наличие отладчика  и выходит, обнаружив его. В противном случае она переходит к своим неблаговидным занятиям. Ниже показан результат программы,
работающей не под отладчиком:
# ./debug_check_x64
Убедились, что отладчика нет, поэтому займемся
интересными вещами, которые аналитики видеть
не должны!

Если программа работает под отладчиком, то результат будет другой:
# gdb ./debug_check_x64
Reading symbols from ./debug_check_x64...(no debugging symbols
found)...done.
(gdb) run
Starting program: /ghidrabook/CH22/debug_check_x64
Не буду работать под отладчиком - выхожу!
[Inferior 1 (process 434) exited with code 0377]
(gdb)

Загрузив этот двоичный файл в Ghidra, мы увидим в окне
листинга следующий код:
700

ГЛАВА 22

undefined do_work()
undefined AL:1
001006f8 PUSH RBP
001006f9 MOV RBP,RSP
001006fc MOV EAX,0x0
00100701 CALL is_debugger_present
00100706 TEST EAX,EAX
00100708 JZ LAB_00100720
0010070a LEA RDI,[s_Не_буду_работать_под_отладчиком_-_выхожу!_001007d8]
00100711 CALL puts
00100716 MOV EDI,0xffffffff
0010071b CALL exit
-- Flow Override: CALL_RETURN (CALL_TERMINATOR)
LAB_00100720
00100720 LEA RDI,[s_Убедились,_что_отладчика_нет_001008
00100727 CALL puts
0010072c NOP
0010072d POP RBP
0010072e RET

В окне декомпилятора находится соответствующий код:
void do_work(void)
{
int iVar1;
iVar1 = is_debugger_present();
if (iVar1 != 0) {
puts("Не буду работать под отладчиком - выхожу!\n");
/* WARNING: Subroutine does not return */
exit(-1);
}
puts("Убедились, что отладчика нет, поэтому займемся\n"

"интересными вещами, которые аналитики видеть\n"

"не должны!\n\n");
);
return;
}

Чтобы залатать этот двоичный файл, так чтобы обойти проверку, мы могли бы забить обращение к функции is_debugger_
present командами NOP, изменить проверяемое условие или изменить тело функции is_debugger_present. Воспользовавшись
пунктом Patch Instruction из контекстного меню, легко замеИзменение двоичного кода

701

нить JZ на JNZ (т. е. сделать так, что программа будет работать
только под отладчиком). Это показано на рис. 22.15.

Рис. 22.15. Замена JZ на JNZ
Тогда в окне декомпилятора появится такой код:
void do_work(void)
{
int iVar1;
iVar1 = is_debugger_present();
if (iVar1 == 0) {
puts("Не буду работать под отладчиком - выхожу!\n");
/* WARNING: Subroutine does not return */
exit(-1);
}
puts("Убедились, что отладчика нет, поэтому займемся\n"

"интересными вещами, которые аналитики видеть\n"

"не должны!\n\n");
);
return;
}

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

702

ГЛАВА 22

# ./debug_check_x64.patched
Не буду работать под отладчиком - выхожу!
# gdb ./debug_check_x64.patched
Reading symbols from ./debug_check_x64...(no debugging symbols
found)...done.
(gdb) run
Starting program: /ghidrabook/CH22/debug_check_x64
Убедились, что отладчика нет, поэтому займемся
интересными вещами, которые аналитики видеть
не должны!
[Inferior1 (process 445) exited normally]
(gdb)

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

РЕЗЮМЕ
Какова бы ни была причина, побудившая вас заняться латанием двоичного файла, разработка заплаты требует тщательного
планирования и последующего развертывания. Ghidra предоставляет все необходимое для планирования заплаты: вы можете создать черновой вариант, воспользовавшись шестнадцатеричным редактором, встроенным ассемблером или скриптом,
затем понаблюдать за результатами изменений и, возможно,
откатить их, выполнив команду Undo, перед тем как генерировать измененную версию оригинального двоичного файла.
В следующей главе мы покажем, как использовать Ghidra для
сравнения залатанной и незалатанной версий двоичного файла, а также обсудим средства Ghidra, предназначенные для
более продвинутого сравнения двоичных файлов и отслеживания версий.

Изменение двоичного кода

703

23

ОПРЕДЕЛЕНИЕ РАЗНОСТИ
ДВОИЧНЫХ ФАЙЛОВ
И ОТСЛЕЖИВАНИЕ ВЕРСИЙ

В предыдущих главах мы познакомились с тем,
как Ghidra может помочь при решении задач обратной разработки. Мы видели много способов
преобразования и аннотирования проделанной
работы – все для того, чтобы лучше понять двоичный
файл и документировать плоды своих трудов.
В этой главе мы поговорим о вычислении разности (дельты) двоичных файлов и инструменте отслеживания версий
в Ghidra. Это позволит выявлять сходства и различия между
файлами и функциями и применять результаты прошлых анализов к новым файлам. Мы будем рассматривать определение
различий между файлами с трех точек зрения: разность двоичных файлов, сравнение функций и отслеживание версий.

РАЗНОСТЬ ДВОИЧНЫХ ФАЙЛОВ
В предыдущей главе мы залатали двоичный файл, изменив
поток управления в функции, – мы обошли вызов exit, поменяв всего один байт в одной команде: JZ (74) на JNZ (75). Чтобы
убедиться в правильности сделанного и документировать, что
именно было изменено, можно было бы воспользоваться внешним инструментом, например VBinDiff или WinDiff, сравнивающим два файла на уровне байтов. Однако для сравнения файлов на уровне команд нужен более изощренный инструмент
Program Diff, доступный в окне листинга Ghidra. Вычисленные
различия можно просмотреть в специальных представлениях,
которые позволяют выделить различия, лучше понять смысл
каждого изменения и предпринять те или иные действия, зависящие от типа различия.
Для сравнения двух файлов, которые были импортированы в проект и находятся в одинаковом состоянии (например,
оба проанализированы или ни один не проанализирован), откройте один из файлов в браузере кода, выполните команду
ToolsProgram Differences (ИнструментыОтличия программ) и выберите второй файл из текущего проекта. Или
можно использовать значок на панели инструментов в окне
листинга, показанный на рис. 23.1. Этот значок открывает
и закрывает инструмент Program Diff.
Diff View

Переключатель, позволяющий открыть изакрыть
инструмент Program Diff

Рис. 23.1. Значок представления разности в браузере кода
В этом примере мы сначала откроем незалатанную версию
файла, щелкнем по значку Diff View и выберем залатанную
версию. В результате откроется окно определения различий
между программами, показанное на рис. 23.2.
По умолчанию отмечены все флажки, но в данном случае
нам достаточно только флажка Bytes, чтобы подтвердить, что
заплата наложена правильно. Нажав кнопку OK, мы увидим
оба двоичных файла в окне листинга, разделенном пополам.
По умолчанию листинги синхронизированы, поэтому, пере706

ГЛАВА 23

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

Рис. 23.2. Диалоговое окно определения различий между
программами
Открывая два файла для вычисления разности, Ghidra первоначально позиционирует листинг в начале каждого файла.
Значок стрелки вниз на панели инструментов в окне листинга
(или клавишу Ctrl-Alt-N) можно использовать для перехода
к первому отличию. Чтобы привлечь ваше внимание к отличающемуся коду, изменения обозначаются цветом в обоих файлах
в окне листинга, а также в окне декомпилятора, если различие
находится внутри функции (окно декомпилятора синхронизировано с первым из двух файлов). Переход к первому отличию
показывает один байт: команду JZ (74) в оригинальном листинге и JNZ (75) во втором.
Чтобы рассмотреть детали, выберите из меню пункт
WindowDiffDiff Details (ОкноРазностьДетали разности). В нижней части окна браузера кода появится окно деталей разности, содержащее следующий отчет:

Определение разности двоичных файлов и отслеживание версий

707

Diff address range 1 of 1.
Difference details for address range: [ 00100708 - 00100709 ]
Byte Diffs :
Address Program1 Program2
00100708 0x74
0x75
Code Unit Diffs :
Program1 CH23:/DiffDemo/debug_check_x64 :
00100708 - 00100709
JZ 0x00100720
Instruction Prototype hash = 16af243b
Program2 CH23:/DiffDemo/debug_check_x64.patched :
00100708 - 00100709
JNZ 0x00100720
Instruction Prototype hash = 176d4e0c

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

Инструмент Program Diff
Девять флажков в верхней части рис. 23.2 описывают, что можно сравнивать; мы можем выбрать любую их комбинацию или
вообще все. По умолчанию инструмент Program Diff работает
со всем файлом. Если требуется ограничить сравнение конкретным диапазоном адресов, то перед тем как открывать инструмент, следует выделить диапазон в первом файле. После того
как все выделено и нажата кнопка OK, появится разделенное
на две части окно листинга, в котором представлена разность
программ.

Представление разности программ
Представление разности программ позволяет просматривать
оба файла одновременно. По существу, в окне листинга теперь
отображается два листинга – слева и справа. Окно деталей разности открывается в нижней части окна браузера кода. В этом
окне левый файл называется Program1 (тот файл, который был
708

ГЛАВА 23

открыт первым), а правый – Program2 (тот файл, который сравнивается с Program1). В окне декомпилятора показано содержимое Program1. При сравнении двух файлов Ghidra может
вычислять разность в любом направлении. Работая с инструментом Program Diff, вы сами должны помнить, что есть что.
Как правило, мы начинаем анализировать файл, а потом понимаем, что часть кода или даже весь код выглядит знакомо,
поэтому возникает желание найти отличия от ранее проанализированного файла. По счастью, в окне разности общие участки файлов выравниваются за счет вставки в нужные места
пустых строк. Отличия выделяются, и панель инструментов
предоставляет средства для навигации и позволяет решить,
как обрабатывать отличия.

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

Применить
отличия

Применить заданные параметры и остаться на том же месте

Применить
отличия
и сместиться

Применить заданные параметры и перейти
к следующему выделенному отличию

Игнорировать
отличия и сместиться

Игнорировать заданные параметры и перейти к следующему
выделенному отличию

Показать детали

Открывает окно деталей разности, в котором показана
информация о выделенном отличии

Перейти
к следующему

Перейти к следующему выделенному отличию

Перейти
к предыдущему

Перейти к предыдущему выделенному отличию

Показать параметры
применения отличий

Открывает окно параметров и позволяет изменить их

Вычислить разность
программ

Заново открывает диалоговое окно определения различий
между программами и позволяет изменить флажки и диапазон

Рис. 23.3. Значки на панели инструментов окна разности
программ
Определение разности двоичных файлов и отслеживание версий

709

Параметры применения отличий
Параметры применения отличий определяют, какие действия
предпринять, когда между файлами обнаружено различие.
Щелчок по значку «Показать параметры применения отличий» открывает окно, показанное на рис. 23.4.

Рис. 23.4. Окно параметров применения отличий
Каждый параметр определяет, какое действие по умолчанию
применить к первой программе на основе содержимого второй
и как именно. В каждом раскрывающемся списке может быть
четыре варианта:
► Ignore. Не изменять первую программу (присутствует
всегда);
► Replace. Изменить содержимое первой программы, так чтобы оно совпало с содержимым второй (присутствует всегда);
► Merge. Добавить отличие из второй программы в первую. Если применяется к метке, то метка, назначенная
основной, не изменяется (присутствует только для меток
и комментариев);
► Merge & Set Primary. То же, что Merge, но основной становится метка во второй программе, если это возможно
(присутствует только для меток).
В верхней части рис. 23.4 показана панель инструментов
с двумя значками. Значок Save as Default (Сохранить для
применения по умолчанию) сохраняет текущие параметры
применения отличий. Значок со стрелкой открывает меню, ко710

ГЛАВА 23

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

Рис. 23.5. Меню изменения параметров применения отличий
Если выбран пункт Set Merge, а операция объединения
(Merge) неприменима к конкретному параметру, то она будет
заменена на Set Replace. Для меток выбор Set Merge устанавливает режим Merge & Set Primary.
Щелкайте по значку «Применить отличия» на панели инструментов, если хотите применить все изменения в соответствии с параметрами по умолчанию. Закончив работу с инструментом Program Diff, щелкните по значку представления
разности в окне листинга – появится диалоговое окно, показанное на рис. 23.6.

Рис. 23.6. Окно подтверждения закрытия сеанса
После подтверждения закрытия сеанса второй файл закроется, и вы вернетесь в обычный режим окна листинга, когда
отображается только первый файл (со всеми изменениями,
внесенными в процессе анализа разности).
Инструмент Program Diff проектировался в основном для
двух ситуаций: во-первых, сравнить файлы, проанализированные двумя пользователями, не имеющими доступа к общему
серверу Ghidra; во-вторых, сравнить двоичный код, сгенерированный на основе разных версий одного и того же исходного кода
(например, исправленную и неисправленную версии разделяеОпределение разности двоичных файлов и отслеживание версий

711

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

Пример: объединение двух
проанализированных файлов
Предположим, что вы анализируете двоичный файл, содержащий
какую-то криптографическую функцию. Коллега говорит, что
тоже анализирует файл, в котором, кажется, есть криптографическая функция, и, по всей видимости, они принадлежат одному
и тому же семейству вредоносных программ. Он согласен передать
вам свой проект, чтобы вы могли сравнить оба файла. Взглянув на
файлы в окне представления разности, вы сразу же обнаруживаете, что, скорее всего, анализируете один и тот же файл.
Проблема в том, что каждый из вас прошел часть пути и модифицировал содержимое файла на основе проделанного анализа. Теперь нужно объединить оба файла, чтобы каждый мог
воспользоваться плодами трудов другого. Вы готовы взять на
себя эту работу, открываете свой двоичный файл в браузере кода
и начинаете сеанс работы с Program Diff, добавив файл коллеги для сравнения.
Щелкнув по значку со стрелкой вниз на панели инструментов,
вы попадаете на первое отличие. В этот момент можно открыть
окно деталей разности, выбрав соответствующий инструмент на
панели инструментов Program Diff (или нажав клавишу F5).
В результате будет показан следующий листинг (разбитый на две
части для удобства обсуждения). Ниже показана первая часть:
Diff address range 1 of 4. 
Difference details for address: 0010075a 
Function Diffs : 
Program1 CH23:/Crypto/diff_sample1 :
Signature: void encrypt_rot13(char * inbuffer, char * outbuffer) 
Thunk? : no
Stack Frame:
Parameters: 
DataType Storage
FirstUse Name
Size Source
/char *
RDI:8
0x0
inbuffer 8
USER_DEFINED

712

ГЛАВА 23

/char *
RSI:8
0x0
Local Variables: 
DataType Storage
FirstUse
/int
EAX:4
0xc0
/int
Stack[-0x1c]:4 0x0
/char
Stack[-0x1d]:1 0x0
Program2 CH23:/Crypto/diff_sample1a : 
Signature: void encrypt(char * param_1,
Thunk? : no
Stack Frame:
Parameters:
DataType Storage
FirstUse
/char *
RDI:8
0x0
/long
RSI:8
0x0
Local Variables:
DataType Storage
FirstUse
/undefined4 Stack[-0x1c]:4 0x0
/undefined1 Stack[-0x1d]:1 0x0

outbuffer 8

USER_DEFINED

Name
length
idx
curr_char

Source
USER_DEFINED
USER_DEFINED
USER_DEFINED

Size
4
4
1

long param_2)

Name
param_1
param_2

Size Source
8
DEFAULT
8
DEFAULT

Name
Size Source
local_1c 4
DEFAULT
local_1d 1
DEFAULT

Первый из четырех найденных адресных диапазонов разности  в этом файле ассоциирован с текущим адресом, 0010075a .
В начале листинга мы видим разность между заголовками
функций в двух файлах . В своем файле вы указали осмысленное имя функции и ее параметров . Кроме того, для каждого
параметра определен тип . Осмысленные имена и типы сопоставлены также локальным переменным . Во второй программе аналитик не внес никаких изменений в созданный Ghidra
заголовок соответствующей функции.
Вы хотите сохранить свою версию изменений в определении
функции и локальных переменных. Можно было бы воспользоваться значком на панели инструментов, чтобы отвергнуть
изменение, но тогда были бы отвергнуты все различия, связанные с этим адресом. Поскольку мы еще не рассмотрели все
отличия, просто прокрутим окно деталей разности вниз до следующего отличия.
Следующая часть, ассоциированная с первым адресным диа­
пазоном, содержит отличия в метках и комментариях.

Определение разности двоичных файлов и отслеживание версий

713

 Label Diffs :
Program1 CH23:/Crypto/diff_sample1 at 0010075a :
0010075a is an External Entry Point.
Name
Type
Primary Source
Namespace
 encrypt_rot13 Function
yes
USER_DEFINED Global
Program2 CH23:/Crypto/diff_sample1a at 0010075a :
0010075a is an External Entry Point.
Name
Type
Primary Source
Namespace
 encrypt
Function
yes
USER_DEFINED Global
 Plate-Comment Diffs :
 Program1 CH23:/Crypto/diff_sample1 at 0010075a :
****************************************************************
* FUNCTION *
* This is a crypto function originally named cryptor. Renamed *
* to use our standard format encrypt_rot13. Changed the *
* function parameters to char *. Added meaningful variable *
* names. Function first seen in fileC13d by Ken H *
****************************************************************
Program2 CH23:/Crypto/diff_sample1a at 0010075a :
No Plate-Comment.
 EOL-Comment Diffs :
Program1 CH23:/Crypto/diff_sample1 at 0010075a :
No EOL-Comment.
 Program2 CH23:/Crypto/diff_sample1a at 0010075a :
This looks like an encryption routine. TODO: Analyze to get more
information.

В разделе отличий меток  есть только различие в имени
функции , которое мы уже обсуждали. В разделе PlateComment  в вашем файле имеется подробный комментарий ,
а в другом вводных комментариев нет вовсе. В разделе EOLComment  имеется краткий комментарий, добавленный другим
аналитиком , которого в вашем файле нет. Но в этом комментарии находится только напоминание TODO себе о том, что вы
уже и так сделали.
Оценив все различия между файлами, вы решаете сохранить
свой вариант и ничего не брать из другого. Для этого нужно
щелкнуть по значку «Игнорировать отличия и сместиться». Таким образом, вы перейдете к следующему отличию. Поскольку
окно деталей разности уже открыто, его содержимое обновляется при навигации, и вы видите следующий отчет:
714

ГЛАВА 23

Diff address range 1 of 3. 
Difference details for address range: [ 0010081a - 0010081e ]
Reference Diffs :
Program1 CH23:/Crypto/diff_sample1 at 0010081a :
Reference Type: WRITE From: 0010081a Mnemonic
RAX USER_DEFINED Primary

To: register:

Program2 CH23:/Crypto/diff_sample1a at 0010081a :
No unmatched references.

Вы уменьшили количество диапазонов, содержащих отличия, отказавшись от предыдущего отличия . И снова в вашем файле информации больше, чем во втором. На этот раз
вы переходите к следующему отличию, щелкнув по значку со
стрелкой вниз. И попадаете сюда:
Diff address range 2 of 3. 
Difference details for address: 00100830
Function Diffs :
Program1 CH23:/Crypto/diff_sample1 :
Signature: undefined display_message()
Thunk? : no
Calling Convention: unknown
Return Value :

DataType
Storage
FirstUse Name

/undefined AL:1
0x0

Parameters:
No parameters.
Program2 CH23:/Crypto/diff_sample1a :
Signature: void display_message(char * message) 
Thunk? : no
Calling Convention: __stdcall
Return Value :

DataType
Storage
FirstUse Name

/void

0x0

Parameters:

DataType
Storage
FirstUse Name

/char *
RDI:8
0x0
message

Size Source
1
IMPORTED

Size Source
0
IMPORTED
Size Source
8
USER_DEFINED

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

715

ет, что второй файл содержит информацию, введенную другим
аналитиком, которой в вашем файле нет. В сигнатуре функции имеется тип возвращаемого значения и описание парамет­
ра . Вы можете включить эту информацию в свой двоичный
файл, щелкнув правой кнопкой мыши по отличию в правом
окне листинга и выбрав из контекстного меню команду Apply
Selection (Применить выбор) (клавиша F3) или щелкнув по
значку «Применить отличия» на панели инструментов.
Перейдя к следующему отличию, вы увидите такие детали:
 Diff address range 2 of 2.
Difference details for address range: [ 00100848 - 0010084c ]
Pre-Comment Diffs :
Program1 CH23:/Crypto/diff_sample1 at 00100848 :
No Pre-Comment.
Program2 CH23:/Crypto/diff_sample1a at 00100848 :
 This is a potential vulnerability. The parameter is being passed
in to printf as the first/only parameter which may result in a format
string vulnerability.

Количество диапазонов уменьшилось, потому что вы применили отличия в предыдущем диапазоне . В этом последнем
отличии мы видим интересное замечание в разделе Pre-Comment
 другого файла. Аналитик обнаружил потенциальную уязвимость. Чтобы включить эту информацию в свой файл, примените отличие.
Закончив сравнение обоих файлов, щелкните по значку Diff
View и подтвердите, что хотите закрыть текущий сеанс. Теперь в листинге отражены плоды совместного анализа, так что
можете сохранить и закрыть свой файл.
Инструмент Program Diff дает возможность изучить различия между двумя версиями одного файла. Конечно, его можно применить и для вычисления разности между двумя никак
не связанными между собой файлами, но если он что и покажет, то только случайное сходство. Поэтому перейдем к другому инструменту, который служит для сравнения выбранных
функций в одной и той же или разных программах.
716

ГЛАВА 23

СРАВНЕНИЕ ФУНКЦИЙ
Если вы видите функцию, похожую на ту, что анализировали
в прошлом, то полезно будет сравнить обе функции напрямую
и применить результат старого анализа к текущей функции
там, где это уместно. Ghidra предлагает такую возможность
в окне сравнения функций, где можно смотреть одновременно
на две функции (рис. 23.7).













Рис. 23.7. Вид листинга в окне сравнения функций

Окно сравнения функций
Чтобы воспользоваться окном сравнения функций, откройте
в браузере кода один или несколько двоичных файлов, содержащих функции, загрузите начальную функцию, выделив ее
на активной вкладке браузера кода, а затем выполните команду Compare Selected Functions (Сравнить выбранные
функции) (клавиша Shift-C) из контекстного меню. В окне
сравнения функций показаны две функции бок о бок, и потенциальные отличия подсвечены (рис. 23.7). (Если вы выбрали
только одну функцию, то она будет отображаться в обоих окнах, пока не будут загружены дополнительные.)
Чтобы добавить дополнительные функции для сравнения,
щелкните по значку Add Functions (Добавить функции) .
Будет показан список всех функций в активной программе
Определение разности двоичных файлов и отслеживание версий

717

в браузере кода. Вы можете выбрать функцию из списка или
сделать активной другую функцию, перейдя на другую вкладку в окне листинга.
Слева от активного листинга (того, что обведен рамкой )
присутствует стрелка-курсор . Если функции совпадают, то
стрелка появится в том же месте и в другом окне. На рис. 23.7 команде в главном окне не соответствует никакая команда в другом окне, поэтому стрелка-курсор видна только в одном окне.
В окно сравнения функций можно загрузить более двух
функций из более чем двух двоичных файлов. На каждую панель можно добавлять и удалять функции по мере необходимости. Выпадающее меню позволяет указать, какая функция
должна отображаться в соответствующем окне .
Это окно позволяет легко переключаться между представлением декомпилятора  и представлением листинга .
На рис. 23.8. показано представление декомпилятора для рассматриваемого примера.

Рис. 23.8. Представление декомпилятора в окне сравнения
функций
Возможности этого окна в значительной мере перекрываются с инструментом Program Diff, только в данном случае мы
сравниваем функции, а не файлы целиком, и можем легко пе718

ГЛАВА 23

реключаться между декомпилированным кодом и листингом.
На рис. 23.9 описана панель инструментов этого окна.

Выбор маркера

Переключение между маркером всей области, маркером
несовпавшей области имаркерами областей

Перейти
кследующей

Перейти кследующей несовпавшей области

Перейти
кпредыдущей

Перейти кпредыдущей несовпавшей области

Отличия байтов

Если включено, неподсвечивать отличия байтов

Отличия констант

Если включено, неподсвечивать отличия констант

Отличия
регистров

Если включено, неподсвечивать регистры-операнды

Наведение мыши

Если включено, показывать информацию при наведении мыши на элемент

Добавить функции

Добавить новую функцию для сравнения

Следующая
функция

Перейти кследующей функции для той стороны,
что находится вфокусе

Предыдущая
функция

Перейти кпредыдущей функции для той стороны, что
находится вфокусе

Удалить
функцию

Закрыть текущую функцию для той стороны, что
находится вфокусе

Синхронизация
прокрутки

Переключить режим синхронизации прокрутки
обеих частей окна

Синхронизация
навигации

Если включено, то выбор новой функции на одной панели
вызывает переход ктой же функции на другой панели

Параметры
листинга

Позволяет задать параметры листинга, например
заголовки

Представление
разности

Открывает изакрывает представление
Program Diff

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

Определение разности двоичных файлов и отслеживание версий

719

ПРИМЕР: СРАВНЕНИЕ
КРИПТОГРАФИЧЕСКИХ ФУНКЦИЙ
Поздравляем с повышением! После успешного анализа и использования инструмента Program Diff для криптографических функций вы признаны специалистом по криптографии
в своей организации. Теперь всякий раз, как один из коллег
подозревает, что столкнулся с криптографической функцией,
он отправляет файл вам – вдруг вы ее опознаете.
И вот пришел очередной файл от коллеги, и вы хотите определить, та ли это криптографическая функция, что вы разбирали в прошлый раз, или что-то новенькое. Чем загружать
и сравнивать криптографическую функцию с новой, вы заводите в Ghidra специальный проект, который содержит все ранее
проанализированные и документированные криптографические функции. Ваша цель – в одну часть окна сравнения функций загрузить уже существующие криптографические функции, а в другую – новый файл. (Для простоты будем считать,
что пока в вашей коллекции всего одна проанализированная
функция: ROT13 из предыдущего примера.)
После загрузки полной коллекции проанализированных
криптографических файлов в браузер кода и функции encrypt_
rot13 в окно сравнения функций нужно загрузить новый файл
в тот же экземпляр браузера кода (FileOpen) и сделать его
активным. В этот момент файл можно исследовать, но это не­
обязательно. Всегда можно вернуться в окно браузера кода,
если не удается найти нужную функцию. В данном случае, щелкнув по значку Добавить функции на панели инструментов
окна сравнения функций, вы увидите полный список функций
в новом двоичном файле, и где-то посередине будет находиться
функция с интригующим именем encrypt (рис. 23.10).

720

ГЛАВА 23

Рис. 23.10. Окно выбора функций, в котором выбрана функция
encrypt
Беглый взгляд на функции в представлении декомпилятора
(рис. 23.11) показывает, что они не имеют ничего общего.

Определение разности двоичных файлов и отслеживание версий

721

Рис. 23.11. Две криптографические функции в представлении
декомпилятора в окне сравнения функций
Представление листинга, показанное на рис. 23.12, подтверждает, что между этими двумя функциями действительно
имеются серьезные различия.

722

ГЛАВА 23

Рис. 23.12. Две криптографические функции в представлении
листина в окне сравнения функций. Различия выделены цветом
В ходе дальнейшего анализа обнаруживается, что новая
функция применяет к каждому байту операцию XOR с одним
и тем же значением 0xa5. Определенно, это совсем не то, что
делает прежняя криптографическая функция, поэтому вы
придумываете для нее новое имя, документируете и добавляе­
те в свою коллекцию (теперь в ней два ценных экземпляра!).
Возвращаясь в браузер кода, вы обновляете сигнатуру функции и добавляете комментарии. Изменения отражаются также
в окне сравнения функций.
По ходу дела вы замечаете, что в новом двоичном файле
есть функция с именем display_message, как и в файле, с которым вы его сравниваете. Вы вспоминаете, что в этой функции когда-то была найдена уязвимость, и решаете сравнить
новую со старой. Вы загружаете их в окно сравнения функций,
чтобы посмотреть, есть ли у них что-то общее, кроме имени.
На рис. 23.13 показано, что они различаются и в представлении декомпилятора, и в представлении листинга.
Определение разности двоичных файлов и отслеживание версий

723

Рис. 23.13. Функции display_message в представлениях
декомпилятора и листинга
Во втором примере параметр param_1 передается функции
puts, так что уязвимость устранена.
Закончив документирование этой функции, вы обнаруживаете, что от коллег пришел еще один двоичный файл. Чтобы начать процесс сравнения с самого начала, вы можете воспользоваться панелью инструментов окна сравнения функций
и удалить обе функции display_message из окна, оставив только
свою коллекцию криптографических функций, в которой теперь два элемента: encrypt_rot13 и encrypt_XOR_a5.
724

ГЛАВА 23

При первом знакомстве с файлом выясняется, что в нем есть
три функции, имеющие отношение к шифрованию: encrypt,
encrypt_strong и encrypt_super_strong. Вы загружаете их в окно
сравнения функций, чтобы сравнить с существующими. При
сравнении encrypt_rot13 с каждой из новых функций вы замечаете следующее:
encrypt_rot13 и encrypt. Почти ничего общего. Функция
encrypt – всего лишь привратник, который может вызывать одну из двух других функций;
encrypt_rot13 и encrypt_strong. Почти идентичны;
encrypt_rot13 и encrypt_super_strong. Сильно различаются. Более внимательное рассмотрение приводит к выводу,
что это не одна и та же функция.
Пристальное изучение отличий показывает, что команды
в функциях encrypt_rot13 и encrypt_strong одинаковы – различаются они в основном адресами в метках, как показано на рис. 23.14.

Рис. 23.14. Окно сравнения функций – различаются адреса
в метках
Странно было бы ожидать совпадения меток – ведь местоположения функций в двоичных файлах различаются. Но метки согласованы с текущим адресом, поэтому мы, вероятно, имеем дело
с одной и той же функцией. Еще одно – последнее – отличие имеет
место в одном байте при вызове strlen, оно показано на рис. 23.15.
Причина похожа – все объясняется различием в относительном
положении функции шифрования и strlen в двоичных файлах.
Определение разности двоичных файлов и отслеживание версий

725

Рис 23.15. Окно сравнения функций – различается один байт
при вызове strlen
Установив, что это одна и та же функция, вы можете щелкнуть
правой кнопкой мыши по ранее проанализированной функции и выбрать из контекстного меню команду Apply Function
Signature To Other Side (Применить сигнатуру функции к другой стороне). Это обновит сигнатуру функции во всех необходимых
местах, включая окно листинга и дерево символов. Отметим, что
окно сравнения функций не предоставляет всех возможностей, доступных в представлении разности. Чтобы скопировать дополнительную информацию (например, подробные комментарии, связанные с функцией), пользуйтесь инструментом Program Diff.
Закончив сравнение с encrypt_rot13, вы обращаете взоры на
функцию encrypt_XOR_a5 и замечаете следующие факты:
encrypt_XOR_a5 и encrypt. Почти ничего общего;
encrypt_XOR_a5 и encrypt_strong. Сильно различаются. Более внимательное рассмотрение отличий снова приводит
к выводу, что это не одно и то же;
encrypt_XOR_a5 и encrypt_super_strong. Почти идентичны.
Отличия между encrypt_XOR_a5 и encrypt_super_strong также
сводятся к адресам в метках и некоторым байтам в обращении
к strlen. Действовать можно так же, как в предыдущем случае.
Хотя этот пример тривиален (и мало похож на реальные криптографические функции, с которыми можно столкнуться на
практике), он все же демонстрирует, как можно воспользоваться
окном сравнения функций, чтобы свести к минимуму повторный
анализ при встрече со знакомыми функциями в новых файлах.
Последний инструмент для сравнения двух файлов самый
сложный: отслеживание версий.
726

ГЛАВА 23

ОТСЛЕЖИВАНИЕ ВЕРСИЙ
Представьте, что вы потратили несколько месяцев на анализ
очень большого двоичного файла. В нем сотни или даже тысячи
функций и ни одного символа. В процессе работы вы присвои­
ли большинству функций осмысленные имена, переименовали данные, локальные переменные и параметры функций,
добавили горы комментариев. На воспроизведение всего этого
уйдут дни, а то и больше.
Теперь представьте, что вышла новая версия двоичного файла и весь мир перестал использовать ту версию, о которой вы
так много знаете. Можно было бы продолжать анализировать
старую версию, стараясь узнать о ней еще больше в предположении, что новая ведет себя аналогично, но тогда вы ничего
не узнаете о новых и модифицированных возможностях обновленного файла. Поэтому вы принимаете решение начать работу над новой версией, но довольно быстро выясняется, что вы
тратите кучу времени на чтение старого размеченного файла,
чтобы продвинуться в анализе нового.
Переключение с одного окна браузера кода на другое и обратно – не самое полезное времяпрепровождение. Пора отказаться от браузера кода в пользу другого инструмента, имеющегося в арсенале Ghidra и показанного на рис. 23.16.

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

727

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

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

Корреляторы
На верхнем уровне инструмент отслеживания версий ищет корреляции между двумя файлами. Имеется семь типов корреляторов:
► корреляторы сопоставления данных;
► корреляторы сопоставления функций;
► корреляторы сопоставления унаследованного импорта;
► подразумеваемые корреляторы;
► корреляторы ручного сопоставления;
► корреляторы сопоставления имен символов;
► корреляторы ссылок.
728

ГЛАВА 23

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

Сеансы
Углубленное рассмотрение полного сеанса заняло бы слишком
много времени. Однако отметим, что базовый сеанс отслеживания версий может включать следующие шаги.
1. Открыть инструмент отслеживания версий.
2. Создать новый сеанс, выбрав исходный и конечный файлы.
3. Для всех подходящих корреляторов: добавить в существующий сеанс, выбрать коррелятор, выбрать все найденные
соответствия, принять все соответствия и применить содержащиеся в них элементы разметки.
4. Сохранить сеанс.
5. Закрыть сеанс.
Определение разности двоичных файлов и отслеживание версий

729

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

РЕЗЮМЕ
В этой главе мы отвлеклись от анализа одного двоичного файла
и познакомились со способами определения сходства и различия
между файлами с помощью инструментов вычисления разности
программ, сравнения функций и отслеживания версий. Они помогают сэкономить время при переносе результатов проделанной работы на новые двоичные файлы, объединении аннотаций,
внесенных разными людьми, и определении того, что именно
изменилось при переходе от одной версии программы к другой.
Мы завершаем наше путешествие по обширной территории
Ghidra. Вы должны понимать, что видели лишь верхушку айсберга возможностей этой программы. Теперь вам предстоит глубже разобраться в Ghidra и вариантах ее применения к стоящим
перед вами задачам обратной разработки. Если появятся вопросы, сообщество Ghidra будет готово помочь вам на сайтах GitHub,
Stack Exchange, Reddit, YouTube и многих других форумах.
Но еще важнее то, что теперь вы сами можете внести свою лепту, отвечая на вопросы и помогая другим. Ghidra живет и развивается благодаря поддержке сообщества. Мы надеемся, что вы
примете в этом участие, это можно сделать разными способами:
готовить учебные пособия, писать и публиковать скрипты и модули Ghidra, находить и разрешать проблемы, а быть может,
даже разработать новую функциональность в самой Ghidra. Будущее Ghidra определяется сообществом, а теперь вы – его член.
Добро пожаловать, и удачи на стезе обратной разработки!

730

ГЛАВА 23

GHIDRA
ДЛЯ ПОЛЬЗОВАТЕЛЕЙ IDA

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

ОСНОВЫ
Отправляясь в путешествие, не вредно иметь с собой путеводитель, который поможет выучить совершенно новый набор
горячих клавиш. Шпаргалка по Ghidra Cheat Sheet (https://
ghidra-sre.org/CheatSheet.html) содержит три в одном: типич-

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

Создание базы данных
IDA импортирует один двоичный файл в одну базу данных
и принципиально является однопользовательской. С другой
стороны, в основе организации Ghidra лежит проект, который
может содержать несколько файлов. При этом поддерживается совместная обратная разработка несколькими пользователями, работающими над одним проектом. Понятию базы данных IDA ближе всего соответствует одна программа в проекте
Ghidra. Пользовательский интерфейс Ghidra разделен на две
основные компоненты: проект и браузер кода.
В Ghidra вы первым делом создаете проект (разделяемый
или неразделяемый) и импортируете в него «программы» (двоичные файлы) с помощью окна проекта. Открывая в IDA новый
двоичный файл, вы по сути дела создаете новую базу данных,
и вместе с IDA выполняете следующие действия.
1. (IDA) Опросить все имеющиеся загрузчики и выяснить, какие из них распознают выбранный файл.
2. (IDA) Открыть диалоговое окно загрузки файла, в котором
присутствует список допустимых загрузчиков, процессорных модулей и параметров анализа.
3. (Пользователь) Выбрать модуль загрузчика, который будет
загружать содержимое файла в новую базу данных, или согласиться с предложением IDA по умолчанию.
4. (Пользователь) Выбрать процессорный модуль, который будет отвечать за дизассемблирование содержимого базы данных, или согласиться с предложением IDA по умолчанию
(продиктованным выбором модуля загрузчика).
5. (Пользователь) Задать параметры анализа, которые будут
использованы при создании начальной базы данных, или
согласиться с предложением IDA по умолчанию. В этот момент можно также вообще запретить анализ.
6. (Пользователь) Подтвердить принятые решения, нажав
кнопку OK.
732

ГЛАВА 23

7. (IDA) Выбранный модуль загрузчика заполняет базу данных, читая байты из оригинального файла. Загрузчики IDA
обычно не загружают весь файл целиком, и, как правило,
невозможно воссоздать оригинальный файл по содержимому базы данных.
8. (IDA) Если анализ разрешен, то выбранный процессорный
модуль дизассемблирует код, идентифицированный загрузчиком и выбранными анализаторами (в IDA анализаторы
называются опциями ядра).
9. Получившаяся база данных отображается в пользовательском интерфейсе IDA.
В Ghidra есть аналоги каждого из вышеперечисленных шагов, однако процесс разбит на два отдельных этапа: импорт
и анализ. Процесс импорта в Ghidra обычно начинается в окне
проекта и включает следующие шаги:
1. (Ghidra) Опросить все имеющиеся загрузчики и выяснить,
какие из них распознают выбранный файл.
2. (Ghidra) Открыть диалоговое окно импорта, в котором присутствует список допустимых форматов (грубо говоря, загрузчиков) и языков (грубо говоря, процессорных модулей).
3. (Пользователь) Выбрать формат импорта файла в текущий
проект или согласиться с предложением Ghidra по умолчанию.
4. (Пользователь) Выбрать язык дизассемблирования программы или согласиться с предложением Ghidra по умолчанию.
5. (Пользователь) Подтвердить принятые решения, нажав
кнопку OK.
6. (Ghidra) Загрузчик, ассоциированный с выбранным форматом,
загружает байты из оригинального файла в новую «программу» в текущем проекте. Загрузчик создает секции программы
и обрабатывает символы и таблицы импорта и экспорта, содержащиеся в двоичном файле, но не выполняет никакого
анализа, сопряженного с дизассемблированием. Загрузчики
Ghidra обычно загружают в проект весь файл, хотя некоторые
его части могут быть не видны в браузере кода.
Хотя этот процесс похож на создание базы данных в IDA,
некоторые шаги отсутствуют. В Ghidra анализ производится
в браузере кода. После того как файл успешно импортирован,
двойной щелчок по нему в представлении проекта открывает
Ghidra для пользователей IDA

733

файл в браузере кода. При первом открытии программы Ghidra
выполняет следующие шаги.
1. (Ghidra) Открыть браузер кода, показать результаты процесса импорта и спросить, нужно ли анализировать файл.
2. (Пользователь) Решить, нужно ли анализировать файл.
Если вы откажетесь от анализа, то попадете в браузер кода,
где сможете просматривать байтовое содержимое, не подвергнутое дизассемблированию. В этом случае вы можете
в любой момент выбрать команду AnalysisAuto Analyze,
чтобы проанализировать файл. Когда бы вы ни решили
произвести анализ, Ghidra предложит список «анализаторов», совместимых с текущим форматом файла и языком.
Вы можете указать, какиеанализаторы выполнить, а затем
изменить параметры анализатора, перед тем как Ghidra
приступит к начальному анализу.
3. (Ghidra) Выполнить все выбранные анализаторы и дать
пользователю возможность начать работу с полностью проанализированной программой в браузере кода.
За дополнительной информацией об этапах импорта и анализа обратитесь к соответствующим главам книги. В IDA нет
аналогов представления проекта и совместной обратной разработки, если не считать разделяемой базы данных Lumina.
Представление проекта описано в главе 4. Разделяемые проекты и поддержка совместной работы обсуждаются в главе 11.
Введение в браузер кода – тема главы 4, а более подробно он
изучается в главе 5 и далее до конца книги.
Браузер кода – это инструмент Ghidra и ваш основной интерфейс анализа программ. Будучи таковым, он является компонентом Ghidra, больше всего напоминающим пользовательский
интерфейс IDA, поэтому потратим некоторое время на соотнесение элементов интерфейса IDA с их аналогами в браузере кода.

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

ГЛАВА 23

Представление листинга
В центре браузера кода находится окно листинга, где в классическом виде отображаются результаты дизассемблирования,
как в IDA в текстовом режиме. Для настройки формата листинга форматер полей браузера позволяет изменять, переставлять
местами и удалять отдельные элементы. Как и в IDA, навигация по окнам листинга осуществляется в основном двойными
щелчками мыши по меткам (имена в смысле IDA) для перехода по связанному с меткой адресу. Контекстные меню дают
доступ к типичным операциям над метками, в т. ч. переименованию и изменению типа.
Как и в IDA, у каждой функции в листинге имеется заглавный комментарий, в котором указан прототип функции, приведено краткое описание ее локальных переменных и отображаются перекрестные ссылки на эту функцию. Для доступа
к эквиваленту представления стека IDA следует щелкнуть
правой кнопкой мыши по заголовку функции и выбрать из
контекстного меню пункт FunctionEdit Stack Frame.
Если вам нравится, как в IDA подсвечиваются все вхождения
строки, по которой вы щелкнули (например, имя регистра или
мнемоническое название команды), то вы, возможно, будете разочарованы, узнав, что в Ghidra такое поведение по умолчанию
выключено. Чтобы включить его, выберите параметр EditTool
OptionsListing FieldsCursor Text Highlight и измените
значение переключателя Mouse Button to Activate с MIDDLE
на LEFT. Еще одно средство, которое можно любить или ненавидеть, – флажок Markup Register Variable References,
который разрешает Ghidra автоматически переименовывать
регистры, в которых хранятся входные параметры функции.
Чтобы выключить это поведение и заставить Ghidra использовать операнды команды в виде имен регистров, перейдите
в окно EditTool OptionsListing FieldsOperands Fields
и сбросьте флажок Markup Register Variable References.
Наконец, если вы хотите, чтобы Ghidra делала «то, что нужно» в ответ на действия, подсказываемые мышечной памятью,
натренированной на горячие клавиши IDA, придется провести
некоторое время в меню EditTool OptionsKey Bindings
и переназначить горячи клавиши Ghidra, так чтобы они совпаGhidra для пользователей IDA

735

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

Графическое представление
Окно листинга в Ghidra целиком текстовое. Если вы привыкли
работать с графами в IDA, то нужно будет открыть отдельное
окно графа функции. Как и графическое представление в IDA,
окно графа функции умеет отображать только одну функцию
одновременно, а манипулирование элементами в нем осуществ­
ляется так же, как в окне листинга.
По умолчанию алгоритм изображения графа в Ghidra может
проводить ребра под вершинами, соответствующими простым
блокам, что усложняет прослеживание ребер. Это поведение
можно подавить, зайдя в окно EditTool OptionsFunction
GraphNested Code Layout и отметив флажок Route Edges
Around Vertices.

Декомпилятор
Ghidra умеет производить декомпиляцию для всех поддерживаемых процессоров. По умолчанию окно декомпилятора находится справа от окна листинга, и в нем отображается декомпилированный исходный C-код той функции в листинге, внутри которой
находится курсор. Если вы хотите добавлять и видеть концевые
комментарии в сгенерированном C-коде, то необходимо разрешить этот режим; для этого выберите пункт меню EditTool
OptionsDecompilerDisplay и отметьте флажок Display
EOL com­ments. На той же вкладке имеется флажок Disable
printing of type casts (Запретить печать приведений типов),
который в некоторых случаях может улучшить понятность кода,
освободив его от загромождающих конструкций.
Декомпилятор также имеет склонность к агрессивной оптимизации генерируемого кода. Если, читая дизассемблированный код, вы чувствуете, что в декомпилированной версии
что-то опущено, то причина, вероятно, в том, что декомпилятор исключил код, который счел мертвым. Чтобы включить
этот код в окно декомпилятора, выберите из меню пункт
736

ГЛАВА 23

EditTool OptionsDecompilerAnalysis и сбросьте флажок Eliminate dead code (Устранять мертвый код). Декомпилятор подробно обсуждается в главе 19.

Дерево символов
Окно дерева символов в браузере кода содержит иерархическое
представление всех символов программы. В дереве символов
имеется шесть папок верхнего уровня, представляющих шесть
возможных классов символов. Щелчок по имени в любой папке
приводит к переходу на соответствующий адрес в окне листинга.
► Imports. Эта папка имеет смысл для динамически скомпонованных двоичных файлов, в ней перечисляются
внешние функции и библиотеки, на которые в программе есть ссылки. Больше всего это напоминает вкладку
Imports в IDA.
► Exports. В этой папке находятся символы программы,
видимые за ее пределами. Это те же символы, что выводятся утилитой nm.
► Functions. В этой папке представлены все функции,
имеющиеся в листинге программы.
► Labels. В этой папке находятся дополнительные нелокальные метки, определенные внутри программы.
► Classes. В этой папке находятся имена классов C++, для
которых Ghidra нашла информацию RTTI (идентификация типа во время выполнения).
► Namespaces. В этой папке находятся все пространства
имен, созданные Ghidra в процессе анализа программы. Дополнительные сведения о пространствах имен см.
в справке по Ghidra.

Диспетчер типов данных
Диспетчер типов данных хранит все, что Ghidra знает о структурах данных и прототипах функций. Каждая папка в диспетчере типов данных приблизительно эквивалентна библиотеке
типов в IDA (til-файлу). Диспетчер типов данных играет роль
окон структур, перечислений, локальных типов и библиотек
типов в IDA и подробно обсуждается в главе 8.
Ghidra для пользователей IDA

737

СКРИПТЫ
Ghidra написана на языке Java, и естественным языком скриптов для нее тоже является Java. Помимо стандартных скриптов,
к расширениям Ghidra относятся написанные на Java анализаторы, плагины и загрузчики. Анализаторы и плагины в совокупности играют ту же роль, что плагины в IDA, а загрузчики выполняют, по существу, те же функции, что загрузчики
в IDA. Ghidra поддерживает концепцию процессорного модуля,
но процессоры определяются на языке спецификаций SLEIGH.
Ghidra включает простой редактор для написания несложных скриптов, а также плагин Eclipse для создания более
сложных скриптов и расширений. Использование Python поддерживается средствами Jython. Ghidra API реализован в виде
иерархии классов, представляющих различные аспекты двоичного файла в виде Java-объектов, а также вспомогательных
классов, упрощающих доступ к некоторым наиболее востребованным классам API. Скрипты Ghidra обсуждаются в главах 14 и 15, а расширения – в главах 15, 17 и18.

РЕЗЮМЕ
Возможности Ghidra и IDA сходны. Иногда окна Ghidra настолько похожи, что единственное, что может затормозить работу, – другие горячие клавиши, кнопки на панели управления
и меню. Но бывает, что информация представлена не так, как
в IDA, и тогда кривая обучения становится круче. Не важно,
решите ли вы воспользоваться средствами настройки Ghidra
и сделать ее максимально похожей на IDA или предпочтете
потратить время на изучение новых способов работы, вероятно,
вы придете к выводу, что Ghidra, как правило, обладает функциональностью, необходимой лично вам для обратной разработки, а в некоторых случаях открывает совершенно новые возможности для решения задачи.

738

ГЛАВА 23

Предметный указатель
A

абстрактная функция 249
абстрактный базовый класс
(C++) 249
автоматизированное создание
структуры 586
автоматический анализ 90, 139,
162, 357, 365
автоматический класс
хранения 253
адрес возврата 144, 149, 156,
160, 164
адресные диапазоны 710
активность 170, 247
анализатор 574, 584
RTTI 258
идентификаторов
функций 117, 371
необслуживаемый 459
однократный 362
стека 143
функций, не возвращающих
управление 584
анализаторы
Decompiler Parameter ID 86
Decompiler Switch Analysis 86
декомпилятор 573
создание в Eclipse 445
анализ динамический 31, 656
анализ статический 31
аннотация 195
арифметика указателей 246
архивы
создание нового архива
проекта 371
создание нового архива
файлов 370
создание новых архивов типов
данных 367
типов данных 365
ассемблер 28
ассемблер (Ghidra) 688

ассемблера язык 28
директивы 28
аутентификация
сервер Ghidra 306, 317
функции 684

Б

базовый адрес 82
базовый виртуальный
адрес 493
байт-код 29
библиотеки
libc.a 382, 384
libcrypto.so 361
lib.so.6 361
libssl.so 361
динамически
компонуемые 296
загрузка внешних 80
импортируемые 116
разделяемые 714
типов 216
блоки памяти 132, 494
браузер кода 84
меню 96
окна
байтов 123, 685
графа вызовов функции 133
графа функции 108, 279
декомпилятора 120, 573
дерева символов 115
деревьев программы 114
диспетчера скриптов 390
диспетчера типов данных 89, 119, 215
карты памяти 132
консоли 120
листинга 89, 102, 631
определенных данных 125
определенных строк 127
ссылок на символы 131
таблицы символов 128
панель инструментов 113
Ghidra для пользователей IDA

739

глобальная таблица
смещений 297

рекурсивный спуск 37
теория 28
динамическая
компоновка 52, 292
динамическое выделение
памяти 226, 254
диспетчер скриптов 425, 439
окно 390
дочерние процессы 636
заголовки секций 55
загрузчики 78
Raw Binary 82, 487, 493, 504,
513, 698
опрос со стороны импортера 488
примеры
загрузчик шелл-кода 524
неизвестный тип
файла (PE) 490
создание модуля 504, 508
шаблон модуля 509

Д

З

В

взаимная трассировка 655
вид со спутника 110
вид со спутника (графы) 110, 282
виртуализация 641
обнаружение 651
виртуальная машина 651
возвратно-ориентированное
программирование (ROP) 443
вредоносное программное
обеспечение 31, 628, 636, 651
встраиваемые
конструкторы 258
встраиваемые функции 258, 610
встроенные типы 366

Г

двоичный поиск 595
декорирование имен (C++) 58,
255, 613
деление на нуль 639
деобфускация
вставка 640, 642
скриптовая 656
эмуляторная 663
деструкторы (C++) 253, 255
дизассемблеры 29, 38
diStorm 62
MASM 35
ndisasm 62
дизассемблирование
базовый алгоритм 33
введение 27
инструменты 43
команды безусловного
перехода 38
команды возврата 40
команды вызова функций 39
команды условного перехода 38
линейная развертка 35
навигация по листингу 138
последовательные команды 37
рассинхронизация 629

740

ГЛАВА 23

закладка ошибки 585, 631
запись активации. См. кадр
стека
запретом выполнения (NX) 443
зачищенный двоичный
файл 47, 220, 616

И

идентификаторы функций 371
изменение масштаба (в окне
графа функции) 110, 111
инструменты глубокой
инспекции 59
инфраструктура открытых
ключей (PKI) 308
исключения 636, 654

К

кадр стека 89, 143, 152, 164, 539
класс хранения 253
код операции 34
командные паттерны 679
комментарии (Ghidra) 189
аннотации 195
вводные 192
для параметров 194

заключительные 192
концевые 191
повторяемые 194
предварительные 192
компоновщик 220
конструкторы
C++ 248, 253
SLEIGH 548
встраиваемые 258
контрольная точка 651
концевой комментарий 191
корреляторы 730
коэффициент заполнения 596

Л

латание 676
пример 701
простые заплаты
использование ассемблера 688
оформление в виде скрипта 687
средство просмотра байтов 685
слишком большие заплаты на
код 692
сложные заплаты 692
форматы экспорта 697
экспорт с применением
скрипта 699
лифтинг 561
лицензии 66

М

магическое число 44, 487, 512,
527
массивы 206, 210
Array вариант
типа (Ghidra) 225
базовый адрес 218, 228
в глобальной памяти 218
в куче 226
в стеке 223
границы 219
доступ к элементам 217
значение индекса 217, 221, 225
переменный индекс 221
постоянный индекс 221, 229

статическое присваивание 226
структур 236
элементы 217, 225
машинный язык 28
межпроцессное
взаимодействие 636
мертвые листинги 56
метасимволы 470, 682
метки 138
добавление 185
закрепленные 188
манипулирование 178
навигация 189
переименование 184
префиксы 186
удаление 189
механизмы вызова
функций 143
модель слова 208, 362
изменение 364
модуль анализатора
гаджет ROP 443
тестирование в Eclipse 453
шаблон (Eclipse) 435, 445
мусорный регистр 153

Н

назначение клавиш 338, 344
наследование 246, 255
нелинейный поток 105
немусорный регистр 153
необслуживаемый
анализатор 459, 464
запуск 460
метасимволы 470
написание скриптов 477
параметры командной
строки 467
сообщения об ошибках 463
файл readme 460
флаги при работе
с сервером 474

О

область влияния
подсветка 583
обозреватель пакетов
(Eclipse) 433, 436, 438
Ghidra для пользователей IDA

741

обратная область влияния 583
обратные ссылки 263
обфускация 48, 628
импортированной функции
645, 653
кода операции 639
потока управления 636
утилиты
ASPack 48, 641, 662
tElock 636, 641, 646, 648, 662
UPX 376, 641, 642, 663
объединение 241
объектный файл 220
окно байтов 123, 685
оптимизированный код 151,
164, 606
организационно уникальный
идентификатор (OUI) 650
отладчики 637, 654
gdb 37
OllyDbg 639
WinDbg 37
воспрепятствование
отладке 654
обнаружение 653
относительный виртуальный
адрес 495
отслеживание версий 323, 707

П

пакетный импорт 314, 465, 482
панорамирование (в окне графа
функции) 110, 111
параметры анализа, окно 84, 85
пароли 308, 319
перегруженные функции 58,
255
перекрестные ссылки 106, 112,
125, 261, 267, 274, 276, 679
на данные 264, 615
на код 265
перечисление 418
типа записи 271
типа перехода 270
типа указателя 272
типа чтения 271
переменные

742

ГЛАВА 23

глобальные 219, 240
локальные 137, 143, 144, 153,
166, 167, 225
переименование 179
переопределение
проваливания 633, 691
переопределение сигнатуры
функции 580
переполнение буфера 262, 444
переход
поток 270
перечисление команд 417
перечисление функций 416
песочница 641
пещеры в коде 693
ПЗУ образ 63
плагины 65, 333, 426, 540
C-Parser 368
FidPlugin 373
FrontEndPlugin 344
зависимости 351
плотность 596
поиск прямых ссылок 618, 678
полоса обзора типа адреса 642
порог взаимодействия 285
порог рисования 111
порядок байтов 359
поток вызова 268
поток выполнения 265
похищение файла (сервер
Ghidra) 326
предотвращение выполнения
данных (DEP) 443
примитивы синхронизации 636
пролог 145, 153, 166, 503
промежуток между
функциями 694
промежуточное представление
(IR) 561
промежуточный язык (IL) 561
простой блок 108, 270, 280,
286, 574
простые преобразования
данных 206
противодействие обратному
конструированию 628,
636, 654

противодействие отладке 675
процессорные модули 539
добавление команды 549
добавление регистра 569
модификация команды 558
прямая область влияния 583

Р

рабочее пространство 334, 354
разделяемые проекты 304, 326
архивирование 314
аутентификация 306, 317, 319
доступ 309, 319
объединение файлов 324
просмотр информации о
проекте 320
репозиторий 309, 321
удаление 313
управление версиями 323
разность двоичных файлов 708
разрядность 359
распаковка
вставка 644
рассинхронизация 629, 639
ребра 262
регистровый переход 633
редактор цветов 337
реестр 641, 653
рейтинг ассемблера 689
рекурсия 144
реорганизация окон 334

С

самомодифицируемый код 630,
656
сеансы 731
секции программы 114
.bss 218, 221
.data 218, 500
.idata 500
.text 496, 499, 693
семафора 636
системный вызов 152
соглашения о вызове 144, 146,
153
cdecl 146
fastcall 149

stdcall 148
в C++ 150
thiscall 150
состояние гонки 309
сочленение 283
спецификация языка и
компилятора 79, 359, 491, 506
сравнение функций 719
ссылки
XREF 106, 263
внешние 276
на память 276
на регистры 276
на символы 222
на стек 276
обратные 263, 276
окно добавления 278
перекрестные 106, 125, 133,
263, 270, 276, 539, 615, 679,
684, 696, 697
прямые 263, 276
форматирование XREF 264
явные прямые 276
статическая компоновка 52
стек 630, 658, 661, 668
стека представления 159
сторонние компоненты 66, 70
структуры 213
в глобальной памяти 231
в куче 233
в стеке 232
выравнивание полей 230, 244
доступ к полям 230
массивы 236
наложение 244
окно редактора структуры 243
размер 234
редактирование 242
создание 238
счетчик команд 631, 639

Т

таблица адресов 37
таблица перемещений 699
таблица символов 55, 89, 220
табличный поиск 595
типы данных 213, 217, 242
Ghidra для пользователей IDA

743

точка входа 34, 117, 131, 619
точки останова
аппаратные 639, 654
программные 654

У

указатель кадра 144, 157, 169
указатель стека 144, 153, 163, 167
упаковщик 376
управление версиями 323, 325
объединение файлов 324
уязвимости 31, 675

Ф

фаззинг 31
файлы
загрузка (Ghidra) 78
похищенные 326
расширения 44, 487
.a 383
.class 366
.dll 53, 117, 149
.fidb 377
.fidbf 117, 381
.gdt 366
.gif 352
.gpr 322
.idx 546
.jpg 352
.keep 330
.ldefs 531
.o 383
.opinion 532, 552
.png 352
.prf 369
.pspec 543
.py 401
.rep 322
.sinc 543, 550
.sla 544
.slaspec 543, 553
.sng 363
.so 53
.spec 542
.tar 315
.tool 353
.txt 543

744

ГЛАВА 23

.xml 543
.zip 307, 315
частный (сервер Ghidra) 329
флаги компилятора 220, 594, 604
форматер полей браузера 107,
196, 340
фрагмент 114
функции
аргументы 143, 149
идентификация 89
атрибуты 200
библиотечные 89, 254
встраиваемые 258, 610
манипулирование 200
механизмы вызова 143
нахождение main 619
не возвращающие
управление 584
перегруженные 255, 613
переопределение сигнатур 580
пролог 145
пространство имен 184
прототипы 188, 206, 214,
255, 579
сигнатура 581, 718, 725
с переменным числом
аргументов 147, 255, 579
сравнение 719
шлюзы 296
эпилог 145
функция-шлюз 296

Х

хеширование
полный хеш-код 371
специальный хеш-код 371
хеш-функция 650

Ч

частные заголовки 55
чисто виртуальная функция 249

Ш

шелл-код 487, 489, 504
шестнадцатеричный
редактор 685
экспертная служба 528

Э

эксплойт 262, 443, 650
эмуляция 663, 668
эпилог 145, 153, 156

Я

язык межрегистровых
пересылок 561

A

ABI (двоичный интерфейс
прикладных программ ) 152,
157, 613
Add Block (панель инстументов
окна карты памяти) 497
Add Functions 719
Add Reference from 277
AddressOfEntryPoint поле 495
AddressSourceInfo
объект 700
Address интерфейс 405
addrinfo тип данных 215, 246
ai_socktype 216
Analysis меню
(браузер кода) 97
Analyze All Open 362, 386
analyzeHeadless 460
analyzeHeadlessREADME.
html 67, 460, 470
Apply Function Signature To
Other Side 728
Apply Selection 718
ARM 544, 561, 657
Ascii, формат экспорта
(Ghidra) 697
ASPack 641, 662
ASProtect 641
Attach existing FidDb 375
Auto Create Structure 588, 590

B

Batch Import 314
Binary, формат экспорта
(Ghidra) 697
buildLanguage.xml 543
Burneye 657
Byte Viewer Options 686

C

C++
dynamic_cast 256, 613
RTTI 256, 614
typeid 256, 613
vf-таблицы 248, 257, 272, 615
абстрактный базовый класс 249
анализатор RTTI 258
виртуальные функции 248,
272, 614
декорирование имен 59, 255
деструкторы 253, 255
жизненный цикл объекта 253
зависимости от компилятора
RTTI 614
перегрузка функций 613
класс хранения 253
компиляторы 230, 593
конструктор 248, 253
наследование 246, 258, 614
обратное конструирование 246
оператор delete 255
оператор new 226, 250
полиморфизм 246, 257
соглашения о вызове 150
указатель this 247
указатель на vf-таблицу 248
чисто виртуальная
функция 249
C/C++, формат экспорта
(Ghidra) 697
c++filt утилита 58
Characteristics (PE-файлы) 501
CheatSheet.html 67
Choose active FidDbs 375
clang 593, 613, 621
Clear Code Bytes 204, 632
clearListing метод 687
crackme 664
Create new empty FidDb 375
Cuckoo песочница 641
Cygwin 44, 56

D

data/languages каталог 533
Delphi 593
Detach existing FidDb 375
Ghidra для пользователей IDA

745

Diff View 708
dist каталог 454
docs каталог 67
dumpbin утилита 57, 645
dynamic_cast 256, 613

E

Edit Function 202
EmulatorHelper класс 663
dispose метод 670
enableMemoryWriteTracking
метод 667
getEmulateExecutionState
метод 668
getTrackedMemoryWriteSet
метод 669
readMemoryByte метод 669
setBreakpoint метод 668
Emulator класс 663
Entropy полоса 103

F

FidDb (база данных
идентификаторов функций)
заполнение 375
отсоединение 375
поля диалогового окна
заполнения 371, 378, 385, 623
присоединение 375
создание 375
FidPlugin 373
FileBytes объект 699
file утилита 44, 382
Flat API 404, 406, 409, 417
FlatProgramAPI класс 404
addEntryPoint метод 530
clearListing метод 413
createAsciiString метод 413
createByte метод 413
createData метод 530
createFunction метод 413
createLabel метод 411, 530
createMemoryBlock
метод 514, 530
createMemoryReference
метод 530
createUnicodeString метод 413
disassemble метод 413

746

ГЛАВА 23

findBytes метод 411
find метод 411
getBytes метод 409
getByte метод 409, 423
getDataAfter метод 410
getDataAt метод 410, 530
getFirstData метод 410
getFirstFunction метод 412, 416
getFirstInstruction метод 410
getFunctionAfter метод 412, 416
getFunctionAt метод 412, 420
getGlobalFunctions метод 412
getInstructionAfter метод 410
getInstructionAt метод 410
getInt метод 409
getLong метод 409
getReferencesFrom
метод 412, 418
getReferencesTo метод 412, 420
getSymbolAt метод 411
getSymbols метод 411
removeFunctionAt метод 413
setEOLComment метод 413
Function ID плагин, пример 373
Function интерфейс
getBody метод 415
getPrototypeString метод 415
getStackFrame метод 415

G

Gaobot червь 48
getaddrinfo 215
ghidraRun 70
ghidra_scripts 390
GhidraScript класс 393, 399, 403
askAddress метод 408
askDirectory метод 408
askFile метод 408
askInt метод 408
askString метод 408
askYesNo метод 408
currentAddress переменная
экземпляра 406, 417
currentLocation переменная
экземпляра 407
currentProgram переменная
экземпляра 398, 406, 416

currentSelection переменная
экземпляра 407
goTo метод 408
popup метод 407
printf метод 407, 416
println метод 407
toAddr метод 409, 422

H

HTML, формат экспорта
(Ghidra) 697

I

ia.sinc 550, 557, 562, 567
IDA
Ghidra для пользователей
IDA 733
IDE (интегрированная среда
разработки) 425
IMAGE_DOS_HEADER 491
IMAGE_NT_HEADERS 492, 495
IMAGE_SECTION_HEADER 495
Instruction интерфейс 415
getComment метод 416
getMnemonicString
метод 415
getNumOperands метод 416
getOperandType
метод 416
toString метод 416
Intel Hex, формат экспорта
(Ghidra) 697
IsDebuggerPresent 653

J

Jython 401, 402

K

kernel32.dll 622
GetModulehandleA 645
GetProcAddress 646

L

ldd утилита 52, 54
licenses каталог 66
LoadLibrary 646
local_ префикс 179

M

MAC-адрес 652
Make Char Array 210
Make String 210
Memory класс 409
Metasploit 62

N

nm утилита 49, 55

O

objdump утилита 55, 138, 166
OllyDbg 639
OpenJDK 306
OpenSSL 361, 368
opinion-файл 532
otool утилита 56

P

packed атрибут 231
pack прагма 230
param_ префикс 179
PE-файлы
PDB (база данных
программы) 87
PEiD (утилита) 48
PE Tools 47
PE-файлы
IMAGE_DOS_HEADER 491
IMAGE_NT_HEADERS 492, 495
IMAGE_SECTION_HEADER 495
анализ 165
базовый виртуальный
адрес 493
заголовки 47, 269
импорт 358
латания 698
нахождение функции
main 620
параметры загрузки 80
пещеры в коде 693
поле Characteristics 501
приоритет загрузчика 513
Plugin Path 316
PointerToRawData поле 496
Populate FidDb from
programs 375
Ghidra для пользователей IDA

747

procmon (Process Monitor) 653
Program API 404, 409, 412
Program класс 404
getFunctionManager метод 414
getLanguageID метод 415
getListing метод 414
getMaxAddress метод 414
getMemory метод 414
getMinAddress метод 414
getReferenceManager метод 414
getSymbolTable метод 414
ptrace 655
p-код 561

Q

QuickUnpack 642

R

Raw Binary загрузчик 82, 487,
493, 504, 513, 520, 698
readelf 56
Red Pill 652
Reference интерфейс
getFromAddress метод 406
getReferenceType метод 406
getToAddress метод 406

S

Shiva 629, 635, 655
Simplify predication 574
SLEIGH 69, 543
ia.sinc 550
адресное пространство
register 564
конструкторы 548
определение регистров 563
присоединение
переменных 570
редактор (Eclipse) 548
спецификация 574, 688
токены 566
sleighArgs.txt 543
strcpy (C) 276

748

ГЛАВА 23

strings утилита 60, 61
SuperH4 572
switch предложение 594
Symbol интерфейс
getAddress метод 405
getName метод 405
Sysinternals 653

T

TaskMonitor класс 407
tElock 636, 641, 646
this указатель 150, 247
TODO комментарии 431, 447,
552

V

VBinDiff 705, 708
VMProtect 641
VMware 651
VMware Tools 651
VMX (расширение для
виртуальных машин) 549
volatile ключевое слово 608

W

WinDbg 37
WinDiff 708
Windows Subsystem for Linux
(WSL 44, 56
WinMain 619
winnt.h 638
Wireshark 653

X

XML, формат экспорта (Ghidra)
698
XREF 106, 263

Z

Zip, формат экспорта (Ghidra)
697
Z флаг (x86) 634

Книги издательства «ДМК Пресс» можно заказать
в торгово-издательском холдинге «Планета Альянс» наложенным платежом,
выслав открытку или письмо по почтовому адресу:
115487, г. Москва, 2-й Нагатинский пр-д, д. 6А.
При оформлении заказа следует указать адрес (полностью),
по которому должны быть высланы книги;
фамилию, имя и отчество получателя.
Желательно также указать свой телефон и электронный адрес.
Эти книги вы можете заказать и в интернет-магазине:
www.galaktika-dmk.com.
Оптовые закупки: тел. (499) 782-38-89.
Электронный адрес: books@alians-kniga.ru.

Крис Игл, Кара Нэнс

GHIDRA
Полное руководство
Главный редактор

Мовчан Д. А.

dmkpress@gmail.com

Зам. главного редактора
Перевод
Корректор
Верстка
Дизайн обложки

Сенченкова Е. А.
Слинкин А. А.
Синяева Г. И.
Луценко С. В.
Мовчан А. Г.

Формат 70×100 1/16.
Гарнитура «Century Schoolbook». Печать цифровая.
Усл. печ. л. 60,94. Тираж 200 экз.
Веб-сайт издательства: www.dmkpress.com