О том, что это за зверь такой — Ghidra («Гидра») — и с чем его едят она ест программки, многие уже, наверняка, знают не понаслышке, хотя в открытый доступ сей инструмент попал совсем недавно — в марте этого года. Не буду докучать читателям описанием Гидры, ее функциональности и т.д. Те, кто в теме, уже, уверен, всё это сами изучили, а кто еще не в теме — могут это сделать в любое время, благо на просторах веба сейчас найти подробную информацию не составит труда. Кстати, один из аспектов Гидры (разработку плагинов к ней) уже освещался на Хабре (отличная статья!) Я же дам только основные ссылки:


Итак, Гидра — это бесплатный кроссплатформенный интерактивный дизассемблер и декомпилятор с модульной структурой, с поддержкой почти всех основных архитектур ЦПУ и гибким графическим интерфейсом для работы с дизассемблированным кодом, памятью, восстановленным (декомпилированным) кодом, отладочными символами и многое-многое другое.

Давайте попробуем уже что-нибудь сломать этой Гидрой!

Шаг 1. Находим и изучаем крякми


В качестве «жертвы» найдем простую «крякми» (crackme) программку. Я просто зашел на сайт crackmes.one, указал в поиске уровень сложности = 2-3 («простой» и «средний»), исходный язык программы = «C/C++» и платформу = «Multiplatform», как на скриншоте ниже:



Поиск выдал 2 результата (внизу зеленым шрифтом). Первая крякми оказалась 16-битной и не запустилась на моей Win10 64-bit, а вот вторая (level_2 by seveb) подошла. Вы можете скачать ее по этой ссылке.

Скачиваем и распаковываем крякми; пароль на архив, как указано на сайте, — crackmes.de. В архиве находим два каталога, соответствующие ОС Linux и Windows. На своей машине я перехожу в каталог Windows и встречаю в нем единственную «экзешку» — level_2.exe. Давайте запустим и посмотрим, чего она хочет:



Похоже, облом! При запуске программа ничего не выводит. Пробуем запустить еще раз, передав ей произвольную строку в качестве параметра (вдруг, она ждет ключ?) — и вновь ничего… Но не стоит отчаиваться. Давайте предположим, что и параметры запуска нам тоже предстоит выяснить в качестве задания! Пора расчехлять наш «швейцарский нож» — Гидру.

Шаг 2. Создание проекта в Гидре и предварительный анализ


Предположим, что Гидра у тебя уже установлена. Если еще нет, то все просто.

Установка Ghidra
1) установи JDK версии 11 или выше (у меня 12)

2) скачай Гидру (например, отсюда) и установи ее (на момент написания статьи последняя версия Гидры — 9.0.2, у меня стоит 9.0.1)

Запускаем Гидру и в открывшемся Менеджере проектов сразу создаем новый проект; я дал ему имя crackme3 (т.е.проекты crackme и crackme2 уже у меня созданы). Проект — это, по сути, каталог файлов, в него можно добавлять любые файлы для изучения (exe, dll и т.д.). Мы сразу же добавим наш level_2.exe (File | Import или просто клавиша I):



Видим, что уже до импорта Гидра определила нашу подопытную крякми как 32-разрядный PE (portable executable) для ОС Win32 и платформы x86. После импорта наш ждет еще больше информации:



Здесь, кроме вышеуказанной разрядности, нас может еще заинтересовать порядок байтов (endianness), который в нашем случае — Little (от младшего к старшему байту), что и следовало ожидать для «интеловской» 86-й платформы.

С предварительным анализом мы закончили.

Шаг 3. Выполнение автоматического анализа


Время запустить полный автоматический анализ программы в Гидре. Это делается двойным кликом на соответствующем файле (level_2.exe). Имея модульную структуру, Гидра обеспечивает всю свою основную функциональность при помощи системы плагинов, которые можно добавлять / отключать или самостоятельно разрабатывать. Так же и с анализом — каждый плагин отвечает за свой вид анализа. Поэтому сначала перед нами открывается вот такое окошко, в котором можно выбрать интересующие виды анализа:

Окно настройки анализа

Для наших целей имеет смысл оставить настройки по умолчанию и запустить анализ. Сам анализ выполняется довольно быстро (у меня занял около 7 секунд), хотя пользователи на форумах сетуют на то, что для больших проектов Гидра проигрывает в скорости IDA Pro. Возможно, это и так, но для небольших файлов эта разница несущественна.

Итак, анализ завершен. Его результаты отображены в окне браузера кода (Code Browser):



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

Обзор интерфейса браузера кода
Настройки интерфейса по умолчанию разбивают окно на три части.

В центральной части располагается основное окно — листинг дизассемблера, который более или менее похож на своих «собратьев» в IDA, OllyDbg и т.д. По умолчанию столбцы в этом листинге таковы (слева направо): адрес памяти, опкод команды, ASM команда, параметры ASM команды, перекрестная ссылка (если применимо). Естественно, отображение можно изменить, нажав на кнопку в виде кирпичной стены в тулбаре этого окна. Если честно, подобной гибкой настройки вывода дизассемблера я нигде не видел, это чрезвычайно удобно.

В левой части 3 панели:

  1. Секции программы (для перехода по секциям кликаем мышью)
  2. Дерево символов (импорты, экспорты, функции, заголовки и т.д.)
  3. Дерево типов используемых переменных

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

В правой части — листинг декомпилированного кода (в нашем случае на языке C).

Кроме окон по умолчанию, в меню Window можно выбрать и расположить в любом месте браузера еще с десяток других окон и отображений. Для удобства я добавил окно просмотра памяти (Bytes) и окно с графом функций (Function Graph) в центральную часть, а в правую часть — строковые переменные (Strings) и таблицу функций (Functions). Эти окна теперь доступны в отдельных вкладках. Также любые окна можно открепить и сделать «плавающими», размещая и изменяя их размер по своего усмотрению — это также очень продуманное, на мой взгляд, решение.

Шаг 4. Изучение алгоритма программы — функция main()


Что ж, приступим к непосредственному анализу нашей крякми-программки. Начинать следует в большинстве случаев с поиска точки входа программы, т.е. основной функции, которая вызывается при ее запуске. Зная, что наша крякми написана на C/C++, догадываемся, что имя основной функции будет main() или что-то в этом духе :) Сказано-сделано. Вводим «main» в фильтр Дерева символов (в левой панели) и видим функцию _main() в секции Functions. Переходим на нее кликом мыши.

Обзор функции main() и переименование непонятных функций


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

Сразу отмечу важную особенность работы в Гидре (в отличие, скажем, от работы в IDA). Работа в Гидре ориентирована, в первую очередь, именно на анализ декомпилированного кода. По этой причине создатели Гидры (мы помним — речь о шпионах из АНБ :)) уделили большое внимание качеству декомпиляции и удобству работы с кодом. В частности, перейти к определению функций, переменных и секций памяти можно просто двойным кликом в коде. Также любую переменную и функцию можно тут же переименовать, что весьма удобно, так как дефолтные имена не несут в себе смысла и могут сбить с толку. Как ты увидишь далее, этим механизмом мы будем часто пользоваться.

Итак, перед нами функция main(), которую Гидра «препарировала» следующим образом:

Листинг main()
int __cdecl _main(int _Argc,char **_Argv,char **_Env)

{
  bool bVar1;
  int iVar2;
  char *_Dest;
  size_t sVar3;
  FILE *_File;
  char **ppcVar4;
  int local_18;
  
  ___main();
  if (_Argc == 3) {
    bVar1 = false;
    _Dest = (char *)_text(0x100,1);
    local_18 = 0;
    while (local_18 < 3) {
      if (bVar1) {
        _text(_Dest,0,0x100);
        _text(_Dest,_Argv[local_18],0x100);
        break;
      }
      sVar3 = _text(_Argv[local_18]);
      if (((sVar3 == 2) && (((int)*_Argv[local_18] & 0x7fffffffU) == 0x2d)) &&
         (((int)_Argv[local_18][1] & 0x7fffffffU) == 0x66)) {
        bVar1 = true;
      }
      local_18 = local_18 + 1;
    }
    if ((bVar1) && (*_Dest != 0)) {
      _File = _text(_Dest,"rb");
      if (_File == (FILE *)0x0) {
        _text("Failed to open file");
        return 1;
      }
      ppcVar4 = _construct_key(_File);
      if (ppcVar4 == (char **)0x0) {
        _text("Nope.");
        _free_key((void **)0x0);
      }
      else {
        _text("%s%s%s%s\n",*ppcVar4 + 0x10d,*ppcVar4 + 0x219,*ppcVar4 + 0x325,*ppcVar4 + 0x431);
        _free_key(ppcVar4);
      }
      _text(_File);
    }
    _text(_Dest);
    iVar2 = 0;
  }
  else {
    iVar2 = 1;
  }
  return iVar2;
}


Вроде бы с виду все нормально — определения переменных, стандартные C-шные типы, условия, циклы, вызовы функций. Но взглянув на код внимательнее, замечаем, что имена некоторых функций почему-то не определились и заменены псевдофункцией _text() (в окне декомпилятора — .text()). Давайте сразу начнем определения, что это за функции.

Перейдя двойным кликом в тело первого вызова

 _Dest = (char *)_text(0x100,1);

видим, что это — всего лишь функция-обертка вокруг стандартной функции calloc(), служащей для выделения памяти под данные. Поэтому давайте просто переименуем эту функцию в calloc2(). Установив курсор на заголовке функции, вызываем контекстное меню и выбираем Rename function (горячая клавиша — L) и вводим в открывшееся поле новое название:



Видим, что функция тут же переименовалась. Возвращаемся назад в тело main() (кнопка Back в тулбаре или Alt + <--) и видим, что здесь вместо загадочного _text() уже стоит calloc2(). Отлично!

То же самое проделываем и со всеми остальными функциями-обертками: поочередно переходим в их определение, смотрим, что они делают, переименовываем (я к стандартным названиям C-функций добавлял индекс 2) и возвращаемся назад в основную функцию.

Постигаем код функции main()


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

if (_Argc == 3) { ... }

_Argc — это количество параметров (аргументов) командной строки, передаваемых в main(). То есть, наша программа «кушает» 2 аргумента (первый аргумент, мы помним, — это всегда путь к исполняемому файлу).

ОК, идем дальше. Вот здесь мы создаем C-строку (массив char) из 256 символов:

char *_Dest;

_Dest = (char *)calloc2(0x100,1); // эквивалент new char[256] в C++

Дальше у нас цикл из 3 итераций. В нем сначала проверяем, установлен ли флаг bVar1 и если да — копируем следующий аргумент командной строки (строку) в _Dest:

while (i < 3) {
				/* цикл по аргументам ком. строки */
  if (bVar1) {
				/* инициализировать массив */
	memset2(_Dest,0,0x100);
				/* скопировать строку в _Dest и прервать цикл */
	strncpy2(_Dest,_Argv[i],0x100);
	break;
  }
...
}

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

n_strlen = strlen2(_Argv[i]);
if (((n_strlen == 2) && (((int)*_Argv[i] & 0x7fffffffU) == 0x2d)) && 
  (((int)_Argv[i][1] & 0x7fffffffU) == 0x66)) {
      bVar1 = true;
}

Первая строка вычисляет длину этого аргумента. Далее условие проверяет, что длина аргумента должна равняться 2, предпоследний символ == "-" и последний символ == «f». Обрати внимание, как декомпилятор «перевел» извлечение символов из строки при помощи байтовой маски.
Десятичные значения чисел, а заодно и соответствующие ASCII-символы можно подсмотреть, удерживая курсор над соответствующим шестнадцатеричным литералом. Отображение ASCII не всегда работает (?), поэтому рекомендую глядеть ASCII таблицу в Интернете. Также можно прямо в Гидре конвертировать скаляры из любой системы счисления в любую другую (через контекстное меню --> Convert), в этом случае данное число везде будет отображаться в выбранной системе счисления (в дизассемблере и в декомпиляторе); но лично я предпочитаю в коде оставлять hex'ы для стройности работы, т.к. адреса памяти, смещения и т.д. везде задаются именно hex'ами.
После цикла идет этот код:

if ((bVar1) && (*_Dest != 0)) {
                    /* если получили аргументы 1) "-f" и 2) строку -
                       открыть указанный файл для чтения в двоичном формате */
      _File = fopen2(_Dest,"rb");
      if (_File == (FILE *)0x0) {
                    /* вернуть 1 при ошибке чтения */
        perror2("Failed to open file");
        return 1;
      }
 ...
}

Здесь я сразу добавил комментарии. Проверяем правильность аргументов ("-f путь_к_файлу") и открываем соответствующий файл (2-й переданный аргумент, который мы скопировали в _Dest). Файл будет читаться в двоичном формате, на что указывает параметр «rb» функции fopen(). При ошибке чтения (например, файл недоступен) выводится сообщение об ошибке в поток stderror и программа завершается с кодом 1.

Далее — самое интересное:

			/* !!! ПРОВЕРКА КЛЮЧА В ФАЙЛЕ !!! */
ppcVar3 = _construct_key(_File);
if (ppcVar3 == (char **)0x0) {
			/* если получили пустой массив, вывести "Nope" */
puts2("Nope.");
_free_key((void **)0x0);
}
else {
			/* массив не пуст - вывести ключ и освободить память */
printf2("%s%s%s%s\n",*ppcVar3 + 0x10d,*ppcVar3 + 0x219,*ppcVar3 + 0x325,*ppcVar3 + 0x431);
_free_key(ppcVar3);
}
fclose2(_File);

Дескриптор открытого файла (_File) передается в функцию _construct_key(), которая, очевидно, и производит проверку искомого ключа. Эта функция возвращает двумерный массив байтов (char**), который сохраняется в переменную ppcVar3. Если массив оказывается пуст, в консоль выводится лаконичное «Nope» (т.е. по-нашему «Не-а!») и память освобождается. В противном случае (если массив не пуст) — выводится по-видимому верный ключ и память также освобождается. В конце функции закрывается дескриптор файла, освобождается память и возвращается значение iVar2.

Итак, теперь мы поняли, что нам необходимо:

1) создать двоичный файл с верным ключом;
2) передать его путь в крякми после аргумента "-f"


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

Комментарии (30)


  1. alexxxst
    09.04.2019 19:30
    +5

    О, как внезапно кончился диван! (с)


  1. kITerE
    09.04.2019 20:07

    Дальше у нас цикл из 2 итераций.

    Судя по приведенному листингу итераций все же будет три (0, 1 и 2):


        local_18 = 0;
        while (local_18 < 3) {

          local_18 = local_18 + 1;
        }

    То есть автор crackme зачем-то анализирует и argv[0].


    1. S0mbre Автор
      10.04.2019 00:55

      Вы правы, спасибо. Исправил.


    1. xndr
      10.04.2019 08:32

      Убедиться, что это его crackme — в чужом все может быть по-другому :)


  1. kITerE
    09.04.2019 20:27

    Десятичные значения чисел, а заодно и соответствующие ASCII-символы можно подсмотреть, удерживая курсор над соответствующим шестнадцатеричным литералом. Отображение ASCII не всегда работает (?), поэтому рекомендую глядеть ASCII таблицу в Интернете.

    В окне листинга дизассемблера:


    Convert - Char Sequence


    1. S0mbre Автор
      10.04.2019 00:55

      Да, можно и так. Хотя мне удобнее просто подсматривать. Но спасибо — добавил в статью.


  1. Ivanii
    09.04.2019 21:51
    +1

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


    1. S0mbre Автор
      10.04.2019 00:56

      Вот вам подробный разбор небезызвестного WannaCry.


    1. fukkit
      10.04.2019 11:03

      Никаких космических технологий в том OneHalf'е то и нет.
      В своё время шуму он наделал скорее благодаря зловредности (втихую шифровал диск), чем какому-то супер полиморфизму.
      Но для тех времен было прикольно, да. Похоронил Aidstest вместе с идеей простого сигнатурного сканера.
      Dr.Web оказался молодцом, не только детектируя эту заразу в загрузочном секторе, но и при удачном стечении обстоятельств расшифровывая диск. Не помню, был ли их «эвристический анализ» частичным эмулятором или тупо строил дерево возможных переходов.
      В те славные времена касперский имел штатную возможность для написания и подключения наколеночного плагина для детектирования и лечения вирусов, не известных лаборатории и не включенных в основную базу сканера.


      1. Ivanii
        10.04.2019 11:16

        В 3,5 КБ было:
        запуск из загрузочного сектора, запуск из файла
        заражение загрузочного сектора, заражение файла
        полиморфизм
        стелс механизм(ы?)
        шифратор + дешифратор «на лету» (помню случай когда системный диск расшифровывался больше суток и удачно расшифровался, эта тварь жила на компе несколько дней и расшифровывала открываемые файлы пока не была убита и диск расшифрован Дрвебом)


  1. S0mbre Автор
    10.04.2019 08:31

    PS. Добавил в начало статьи ссылку на отличную статью про плагины к Гидре, которая уже публиковалась на Хабре. Спасибо DrMefistO!


    1. DrMefistO
      10.04.2019 10:12

      Спасибо! Гидру в массы:)


  1. DrMefistO
    10.04.2019 10:10

    Спасибо за ссылку на мою статью.:)

    Основной момент, который меня не радует во всех этих статьях и видео по Гидре — это изучение того, что давно уже было изучено и показано, но в Иде.
    Есть же столько нового и классного функционала в продукте от АНБ (да хотя бы декомпилеры почти под всё), а все по-прежнему примитивные крякми изучают. Покажите новое кто-нибудь!


    1. S0mbre Автор
      10.04.2019 14:17

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


      1. pfemidi
        10.04.2019 17:54

        А мне вот после более чем двадцати лет использования IDA на эту гидру сложновато переползать. И одна из сложностей это как раз неудобство интерфейса по сравнению с IDA :-) Хотя это скорее непривычность чем неудобство. Да и тормоз эта гидра по сравнению с IDA. Что не удивительно, Java ведь. Так что пожалуй что не буду пока. Но со стороны посмотрю что там творится.


        1. S0mbre Автор
          11.04.2019 00:08

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


    1. T-D-K
      10.04.2019 20:41

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


      1. S0mbre Автор
        11.04.2019 00:06
        +1

        Ну, есть, например, вот эта мать драконов :)
        Будет время — отдельную статью напишу про этот фреймворк.


      1. kITerE
        11.04.2019 15:29
        +1

        А там есть какой-нибудь механизм/плагин/поддержка из коробки для конвертации готовых IDA файлов в Гидру?

        Есть полный экспорт из IDA c использованием плагина — 7xx/xml_exporter.py и 6xx/xmlexp.py с последующим импортом в Ghidra.


        1. T-D-K
          11.04.2019 15:38

          Спасибо. Посмотрю.


  1. nikandr23
    10.04.2019 18:40

    смогу ли я этой штукой ломать андроидные аппы с целью убирать рекламу?


    1. S0mbre Автор
      11.04.2019 00:09

      Да, ARM поддерживается из коробки. Но в случае с андроидом, я бы все-таки работал в AndroidStudio.


      1. osmanpasha
        11.04.2019 08:36

        Андроидные приложения же на жаве, может ли гидра в виртуальные машины?


        1. S0mbre Автор
          12.04.2019 08:56

          Вот пример анализа андроидного приложения.


          1. osmanpasha
            12.04.2019 09:09

            Ну там в видео всё, что угодно, кроме Java-кода (Rust мелькнул даже) — реверсятся библиотеки для низкоуровневой работы с железом. Тем не менее, Гидра вроде поддерживает Java-байткод и dex-файлы.

            Ghidra processor modules: X86 16/32/64, ARM/AARCH64, PowerPC 32/64, VLE, MIPS 16/32/64,micro, 68xxx, Java / DEX bytecode, PA-RISC, PIC 12/16/17/18/24, Sparc 32/64, CR16C, Z80, 6502, 8051, MSP430, AVR8, AVR32, Others+ variants as well. Power users can expand by defining new ones

            — Rob Joyce (@RGB_Lights) March 5, 2019



  1. stalker1984
    10.04.2019 19:48

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


    1. S0mbre Автор
      11.04.2019 00:12

      Раз выложили исходники, значит готовы ко всестороннему анализу их со стороны мирового сообщества. И все, что будет находиться, будет репортиться — на Гитхабе уже >300 баг-репортов. Но это означает, как мне представляется, прозрачность и открытость здесь со стороны пресловутых шпионов. Хотя есть и другое мнение — это было сделано специально, чтобы собрать экспертное мнение и баги и улучшить свой продукт (который затем можно будет опять закрыть).


    1. ormoulu
      12.04.2019 11:10

      Don't trust — verify.


  1. technic93
    12.04.2019 01:49

    Надо бы написать что запускать какой то левый кракми надобы из под виртуалки или из под запасного юзнра или вообще на отдельной машине для тестов. Думаю на последнем левеле там могут быть и бомбы замедленного действия)


    1. S0mbre Автор
      12.04.2019 09:02

      Ну это как бы предполагается… Хотя я просто пока прогонял через АВ.