Перейти к содержимому
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after a top-up of $10 or more. Until Nov 30.

How it works

Знакомство с Ghidra на безобидном бинарнике, который вы компилируете сами

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

Средний11 мин чтенияАвтор: NotHoly

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

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

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

Что такое Ghidra

Ghidra — это фреймворк для реверс-инжиниринга программ. Его разработало Агентство национальной безопасности США, а опубликован он на GitHub в репозитории NationalSecurityAgency/ghidra. Работает на Windows, macOS и Linux. Со скомпилированной программой Ghidra умеет делать четыре вещи, которые нам здесь нужны:

  • Дизассемблировать машинный код в инструкции ассемблера.
  • Декомпилировать функции в C-подобный псевдокод встроенным декомпилятором.
  • Находить строки и перекрёстные ссылки (XREFs), то есть места в программе, где используется строка или адрес.
  • Сохранять изменения в проекте, включая имена, которые вы дали функциям и переменным.

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

Зачем разбирать программу, которую написали сами

Можно скачать с форума какой-нибудь бинарник и начать изучать его. Не делайте этого. Незнакомый бинарник может оказаться вредоносным ПО, а проверить вывод инструмента вы не сможете, потому что правильного ответа не знаете.

Программа, которую вы скомпилировали сами, даёт три вещи:

  1. Эталон. Вы знаете, что check() сравнивает аргумент с "LAB-1234".
  2. Две сборки одного кода. В одной сохранены отладочная информация и символы. Другая очищена и оптимизирована. Сравнив их, вы увидите, что убрал компилятор.
  3. Безопасную цель. Программа только печатает ok или no. Она не обращается к сети и не записывает файлы.

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

Подготовка лаборатории

Что понадобится

  • Компилятор C. В Linux установите gcc через менеджер пакетов. В macOS подойдёт clang из инструментов командной строки Xcode, флаги те же. В Windows используйте WSL или контейнер с GCC.
  • JDK для Ghidra. Ghidra работает на Java, и без установленного JDK лаунчер не запустится.
  • Ghidra из официальных релизов на GitHub.
  • Терминал с sha256sum. В Git Bash для Windows он есть.

Установка JDK и Ghidra

Установите тот JDK, который требует релиз Ghidra. Не полагайтесь на номер версии из старых руководств. Когда я смотрел README репозитория и руководство Getting Started в день написания статьи, оба требовали 64-битный JDK 25, а README ссылался на сборки Eclipse Temurin. Найдите в README или примечаниях к релизу точную версию для скачанного вами файла и используйте её. Несовпадение версии Java часто мешает Ghidra запуститься.

Скачивайте Ghidra только со страницы официальных релизов на github.com/NationalSecurityAgency/ghidra. Репозиторий ссылается на файлы релиза. Распакуйте релиз в новую папку: руководство Getting Started запрещает распаковывать Ghidra поверх существующей установки. Запускайте ghidraRun в Linux и macOS или ghidraRun.bat в Windows.

Проверка загрузки

Прежде чем что-либо распаковывать, сверьте файл с опубликованным значением SHA-256. Выполните эту команду в папке, куда сохранили релиз:

bash
sha256sum ghidra_*.zip

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

Проверка хеша доказывает только то, что файл совпадает с указанным издателем. Надёжность самого издателя она не доказывает. Берите файл из официального источника и сохраняйте его имя и хеш в заметках.

Пишем игрушечную программу

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

bash
mkdir -p ghidra-lab && cd ghidra-lab
cat > toy.c <<'EOF'
#include <stdio.h>
#include <string.h>
int check(const char *k){ return strcmp(k, "LAB-1234") == 0; }
int main(int c, char **v){ if(c>1 && check(v[1])) puts("ok"); else puts("no"); return 0; }
EOF

Программа читает один аргумент командной строки. Если он равен LAB-1234, она печатает ok. В остальных случаях печатает no. С точки зрения безопасности эта проверка ничего не стоит: это фиксированная строка в файле, который может прочитать кто угодно. Так и задумано. Вы найдёте строку в бинарнике за несколько минут.

Запишите хеш исходника, чтобы потом можно было доказать, какой файл вы анализировали:

bash
sha256sum toy.c

На моей машине команда напечатала:

text
d9746d8fb118fa954a46b4d501ddf9b0ed86e986e7c99adb05e4a1a60ca0071d  toy.c

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

Компилируем дважды

Соберите один исходник в два бинарника с разными настройками:

bash
gcc -O0 -g -o toy_debug toy.c
gcc -O2 -s -o toy_stripped toy.c

Флаги делают разные вещи. Описаны они в руководстве GCC, которое стоит прочитать хотя бы раз:

  • -O0 отключает большинство оптимизаций. Это значение по умолчанию, и руководство GCC описывает его как настройку, при которой отладка остаётся предсказуемой. Код близко следует исходнику.
  • -O2 включает большинство оптимизаций, которые не жертвуют размером ради скорости. В руководстве сказано, что он также включает встраивание небольших функций, даже не объявленных как inline. Код может сильно отличаться от исходника.
  • -g добавляет отладочную информацию в родном формате операционной системы. Ghidra умеет читать её часть, отладчики используют всю.
  • -s удаляет таблицу символов из выходного файла на этапе линковки. Имена функций check и main в файле больше не хранятся.

Польза лабораторной работы как раз в этих двух бинарниках: исходник один, флаги разные. Сохраните оба.

Запускаем обе сборки

Запустите их до того, как откроете Ghidra. Программу, которую вы ещё не запускали, вы пока не понимаете.

bash
./toy_debug LAB-1234
./toy_debug wrong
./toy_debug
./toy_stripped LAB-1234
./toy_stripped wrong

Я запускал их в контейнере Debian с GCC 14.2.0 на хосте x86-64. Вывод был таким:

text
ok
no
no
ok
no

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

Изучаем бинарники из командной строки

Перед Ghidra посмотрите, что хранится в каждом файле. Хватит двух инструментов: nm выводит символы, а strings выводит печатаемый текст.

bash
nm toy_debug | grep -E " (check|main)$"
nm toy_stripped
strings toy_stripped | grep LAB
ls -l toy_debug toy_stripped

На моей машине вывод был таким:

text
0000000000001149 T check
0000000000001175 T main
nm: toy_stripped: no symbols
LAB-1234
-rwxr-xr-x 1 root root 17384 Nov  8 09:17 toy_debug
-rwxr-xr-x 1 root root 14480 Nov  8 09:17 toy_stripped

Прочитайте эти строки внимательно:

  • В отладочной сборке всё ещё есть имена check и main. Они пришли из вашего исходника.
  • В очищенной сборке символов нет. nm нечего выводить.
  • Строка LAB-1234 осталась в обоих файлах. Строка относится к данным, а удаление символов не трогает данные, нужные программе во время работы. Строка лежит в файле как константа, поэтому эту лабораторную работу легко начать.

Теперь дизассемблируйте check в отладочной сборке:

bash
objdump -d -M intel toy_debug | grep -A14 "<check>:"

Вывод выглядел так:

text
0000000000001149 <check>:
    1149:	55                   	push   rbp
    114a:	48 89 e5             	mov    rbp,rsp
    114d:	48 83 ec 10          	sub    rsp,0x10
    1151:	48 89 7d f8          	mov    QWORD PTR [rbp-0x8],rdi
    1155:	48 8b 45 f8          	mov    rax,QWORD PTR [rbp-0x8]
    1159:	48 8d 15 a4 0e 00 00 	lea    rdx,[rip+0xea4]        # 2004 <_IO_stdin_used+0x4>
    1160:	48 89 d6             	mov    rsi,rdx
    1163:	48 89 c7             	mov    rdi,rax
    1166:	e8 d5 fe ff ff       	call   1040 <strcmp@plt>
    116b:	85 c0                	test   eax,eax
    116d:	0f 94 c0             	sete   al
    1170:	0f b6 c0             	movzx  eax,al
    1173:	c9                   	leave
    1174:	c3                   	ret

В этих инструкциях виден исходник: вызов strcmp, проверка возвращаемого значения и sete, который превращает «равно нулю» в 0 или 1. Вспомните об этом, когда будете читать декомпилятор.

Теперь посмотрите на оптимизированную сборку. Я дизассемблировал toy_stripped тем же способом. Компилятор поместил сравнение прямо в main, которая в моей сборке находится по адресу 0x1060: cmp edi,0x1 для проверки c>1, затем mov rdi,[rsi+0x8], чтобы загрузить v[1], затем call strcmp@plt. Отдельную невстроенную копию check по адресу 0x1190 он тоже оставил, потому что функция не объявлена как static и её может вызвать другой файл. С другим тулчейном адреса будут другими, так что считайте эти числа картой того, что искать, и не пытайтесь их повторить.

Импорт в Ghidra

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

  1. Запустите Ghidra командой ghidraRun (или ghidraRun.bat).
  2. Выберите File > New Project. Укажите Non-Shared Project и задайте имя, например toy-lab. Сохраните проект в папке, которой вы распоряжаетесь.
  3. Выберите File > Import File. Укажите toy_debug. Примите формат, который определила Ghidra, если он соответствует вашей системе. Файл, собранный на x86-64 Linux, должен отображаться как ELF с процессором x86.
  4. Повторите импорт для toy_stripped. Теперь в проекте две записи.
  5. Откройте каждый файл в инструменте CodeBrowser. Когда Ghidra спросит, анализировать ли программу, примите для этой работы анализ по умолчанию.

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

Находим строку и идём по её ссылкам

Начните с toy_stripped, потому что это более сложный случай. Затем сравните с toy_debug.

  1. Откройте Window > Defined Strings.
  2. Найдите в списке LAB-1234. Дважды щёлкните по ней, чтобы перейти к её адресу в листинге.
  3. Щёлкните правой кнопкой по адресу строки и откройте меню ссылок. Ищите пункт вроде References > Show References to. Каждая ссылка указывает на место в коде, которое читает этот адрес.
  4. Перейдите по ссылке. Она приведёт в функцию, которая сравнивает ваш ввод со строкой.

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

То же самое проделайте со строками "ok" и "no". Обе используются в main. Если идти от них из main, получится та же картина с другой стороны.

Читаем декомпилятор рядом с исходником

Откройте панель декомпилятора для функции, которая сравнивает строку. Рядом, в другом окне, откройте toy.c.

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

  • Функция с одним аргументом, указателем на символы.
  • Вызов функции с именем strcmp или символа, который инструмент помечает как strcmp.
  • Сравнение, которое возвращает 1, когда результат равен нулю.

Теперь перечислите различия, которые видите. Типичные такие:

  • Типы. Декомпилятор может показать char * как char * или взять более свободный тип вроде undefined8. Может не знать, что ввод объявлен как const.
  • Имена. Ваш параметр k появится как param_1 или похожая заглушка.
  • Возвращаемое значение. Выражение на C strcmp(...) == 0 может выглядеть как сравнение и следующий за ним условный возврат. Смысл тот же, форма другая.
  • Обработка аргументов в main. Ваша проверка c > 1 превратится в сравнение количества аргументов. Следите за смещением на единицу: argv[0] содержит имя программы, а v[1] — первый настоящий аргумент.

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

Переименовываем переменные в очищенной сборке

Теперь та часть работы, которая делает очищенный бинарник читаемым.

  1. В панели декомпилятора щёлкните по параметру, в котором лежит входная строка.
  2. Щёлкните правой кнопкой и выберите Rename Variable либо выделите переменную и нажмите L. Назовите её input.
  3. Переименуйте саму функцию в check_input в листинге или дереве символов.
  4. В main переименуйте переменную с массивом аргументов в argv, а счётчик в argc.

Ghidra хранит эти имена в проекте. Бинарник на диске не меняется: вы записываете своё прочтение, а скомпилированный файл остаётся нетронутым.

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

Сравниваем две сборки

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

  • Какие имена функций сохранились в очищенной сборке?
  • Встроила ли сборка -O2 функцию check в main или оставила отдельной? Поищите в листинге отдельное место вызова strcmp и отдельный ret.
  • Какие части исходника легко увидеть в обоих представлениях?
  • Какие части трудно найти в очищенном представлении?

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

Частые ошибки

Пропуск запуска. Если открыть бинарник в Ghidra до запуска, вы не будете знать, как программа должна себя вести. Сначала запустите её и запишите вывод.

Доверие к именам декомпилятора. Имя вроде FUN_00101190 или local_18 — заглушка. Новое имя становится вашим утверждением, и для него нужны доказательства.

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

Образец из ненадёжного источника. Безопасная отправная точка — ваш собственный скомпилированный файл. Освоившись здесь, вы по-прежнему берёте либо свой код, либо образец из проверенного источника, причём источник надо проверить заранее. Статья о хешах файлов и этике VirusTotal объясняет, почему одного хеша для этого мало.

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

Забытая версия JDK. Если Ghidra не запускается, первым делом сверьте версию Java с примечаниями к релизу.

Что дальше

Когда игрушечная программа заработает, усложняйте упражнение под контролем:

  • Замените "LAB-1234" более длинной строкой и перекомпилируйте. Снова найдите её в обеих сборках.
  • Добавьте вторую функцию проверки, которая сравнивает ввод с числом, и найдите, где Ghidra показывает преобразование.
  • Соберите программу с -O0 -s и сравните результат с -O2 -s. Разница покажет, как сильно оптимизатор меняет форму кода, сохраняя логику.
  • Прочитайте собственную документацию Ghidra о декомпиляторе и листинге. На официальном сайте и в папке docs на GitHub интерфейс описан подробнее, чем может вместить эта статья.

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

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

Источники

Источники

  1. Ghidra: Getting Started (GitHub repository)
  2. Ghidra repository README
  3. Ghidra project site
  4. GCC manual: Optimize Options
  5. GCC manual: Debugging Options

Ещё по теме: Реверс-инжиниринг