Ломаем простую «крякми» при помощи Ghidra — Часть 1

О том, что это за зверь такой — 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(), которая, как мы выяснили, отвечает за проверку искомого ключа в файле.
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 30

    +6
    О, как внезапно кончился диван! (с)
      0
      Дальше у нас цикл из 2 итераций.

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


          local_18 = 0;
          while (local_18 < 3) {

            local_18 = local_18 + 1;
          }

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

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

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


          Convert - Char Sequence


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

            0
            Да, можно и так. Хотя мне удобнее просто подсматривать. Но спасибо — добавил в статью.
            +3
            Предлагаю разобрать какой-нибудь стелс полиморфный 4х килобайтный вирусняк, OneHalf например, должно быть очень интересно и компактно.
              0
              Вот вам подробный разбор небезызвестного WannaCry.
                0
                Никаких космических технологий в том OneHalf'е то и нет.
                В своё время шуму он наделал скорее благодаря зловредности (втихую шифровал диск), чем какому-то супер полиморфизму.
                Но для тех времен было прикольно, да. Похоронил Aidstest вместе с идеей простого сигнатурного сканера.
                Dr.Web оказался молодцом, не только детектируя эту заразу в загрузочном секторе, но и при удачном стечении обстоятельств расшифровывая диск. Не помню, был ли их «эвристический анализ» частичным эмулятором или тупо строил дерево возможных переходов.
                В те славные времена касперский имел штатную возможность для написания и подключения наколеночного плагина для детектирования и лечения вирусов, не известных лаборатории и не включенных в основную базу сканера.
                  0
                  В 3,5 КБ было:
                  запуск из загрузочного сектора, запуск из файла
                  заражение загрузочного сектора, заражение файла
                  полиморфизм
                  стелс механизм(ы?)
                  шифратор + дешифратор «на лету» (помню случай когда системный диск расшифровывался больше суток и удачно расшифровался, эта тварь жила на компе несколько дней и расшифровывала открываемые файлы пока не была убита и диск расшифрован Дрвебом)
                +1
                PS. Добавил в начало статьи ссылку на отличную статью про плагины к Гидре, которая уже публиковалась на Хабре. Спасибо DrMefistO!
                  0
                  Спасибо! Гидру в массы:)
                  +3
                  Спасибо за ссылку на мою статью.:)

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

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

                      0
                      А мне вот после более чем двадцати лет использования IDA на эту гидру сложновато переползать. И одна из сложностей это как раз неудобство интерфейса по сравнению с IDA :-) Хотя это скорее непривычность чем неудобство. Да и тормоз эта гидра по сравнению с IDA. Что не удивительно, Java ведь. Так что пожалуй что не буду пока. Но со стороны посмотрю что там творится.
                        0
                        Вот здесь еще интересный обзор человека, который по его словам 22 года юзает Иду. Он пока переходить тоже не торопится и занимает вашу позицию — смотреть со стороны.
                      0
                      А там есть какой-нибудь механизм/плагин/поддержка из коробки для конвертации готовых IDA файлов в Гидру? Есть у меня один долгоиграющий проект по реверсингу, к которому приходится периодически возвращаться. Приложение очень большое из нескольких исполняемых файлов и я в нём разбираю только нужные мне места. Несколько десятков своих структур, и несколько сотен методов приведены в порядок и прочее — с нуля не хочу начинать. Если есть возможность открыть это всё в гидре было бы здорово.
                        +1
                        Ну, есть, например, вот эта мать драконов :)
                        Будет время — отдельную статью напишу про этот фреймворк.
                          +1
                          А там есть какой-нибудь механизм/плагин/поддержка из коробки для конвертации готовых IDA файлов в Гидру?

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

                            0
                            Спасибо. Посмотрю.
                        0
                        смогу ли я этой штукой ломать андроидные аппы с целью убирать рекламу?
                          0
                          Да, ARM поддерживается из коробки. Но в случае с андроидом, я бы все-таки работал в AndroidStudio.
                            0

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

                              0
                              Вот пример анализа андроидного приложения.
                                0
                                Ну там в видео всё, что угодно, кроме 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


                          0
                          Я наверное покажусь людям параноиком, но работать с программой для взлома предоставленной АНБ, это как запрыгнуть к незнакомому дядьке в грузовик за конфетой. И ведь даже исходники выложили, но всё равно стойкое чувство подвоха.
                            0
                            Раз выложили исходники, значит готовы ко всестороннему анализу их со стороны мирового сообщества. И все, что будет находиться, будет репортиться — на Гитхабе уже >300 баг-репортов. Но это означает, как мне представляется, прозрачность и открытость здесь со стороны пресловутых шпионов. Хотя есть и другое мнение — это было сделано специально, чтобы собрать экспертное мнение и баги и улучшить свой продукт (который затем можно будет опять закрыть).
                              0
                              Don't trust — verify.
                              0

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

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

                              Only users with full accounts can post comments. Log in, please.