Данное описание подготовлено и публикуется со слов участника CTF, занявшего почетное второе место – Сементинова Сергея ( @cema_rus ). Мы разбили прохождение реверста на несколько статей потому что они достаточно обширны и подробны, это уже вторая часть, первая часть разбором с заданиями Task1.exe, Task2.exe, Task3.exe, taskSyzran.exe уже доступна, как и третья часть разбора с финальным заланием Final.exe. Дальше текст передается as-is.

Deep.exe

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

Сначала планировалось выполнять это задание с применением все тех же IDR и IDA, но в IDA при декомпиляции вылазила ошибка и, честно сказать, было лень с ней разбираться - было решено использовать “новый для себя” инструмент - Ghidra (https://github.com/NationalSecurityAgency/ghidra/releases).

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

По двойному щелчку на приложении запускается окно CodeBrowser и, при первом запуске, предлагается провести анализ кода, соглашаемся на всё, жмём на кнопку Analyze и дожидаемся окончания анализа.

Теперь имеем код и давайте его разбирать, попутно переименовывая различные переменные и функции для удобного понимания:

Как обычно, начинаем с условия, когда флаг считается правильным. Переменная DAT_0040a800 должна, после всех вычислений, иметь значение равное 100. В самой функции не видно объявления этой переменной, только начальная инициализация. Скорее всего, эта переменная глобальная (в чём мы убедимся позже). Переименовываем и идём дальше.

При дальнейшем изучении понимаем, что функция FUN_00403eb4 возвращает длину строки, а переменная DAT_0040a798 - это введенный нами влаг. На и тело условия, при помощи логики и фантазии, приводим примерно к такому виду:

На этом этапе, уже у многих будет общее понимание алгоритма проверки. А за деталями пойдём по очереди в fInputCharLeft, fInputCharRight, fInputCharUp и fInputCharDown.

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

Непонятным остаётся только один момент: Что это за условие такое в функциях?! Какое отношение оно имеет к введенному нами флагу?! Ерунда какая-то, которая заставила меня изрядно поломать голову… И, самые внимательные, уже обратили внимание на какую-то странную + 3 в условии.

Продолжаем смотреть другие функции, в надежде что придёт озарение и… Натыкаемся на это:

Некая функция с инициализацией некоего массива и, при детальном рассмотрении, видим, что наш массив сидит по адресу 0x0040a79c, а указатель на введенную нами строку по адресу 0x0040a798:

Немного поразмыслив, понимаем, что конструкция вида:

*(byte *)((int)&sInputFlag + iCheckingCounter + 3)

означает не обращение к нашей строке, а обращение к этому массиву, т.к. iCheckCounter не должен быть меньше 1, а общая сумма (0x0040a798 + 1 + 3 == 0x0040a79c) равна адресу нашего массива. В общем… Декомпилятор немного неправильно распознал конструкцию, но оттого стало только интереснее)

Что получаем в итоге? Алгоритм проверки флага:

При запуске iCheckCounter = 1;

  1. Проверяем все входные условия, в виде фигурных скобок, длины и прочего;
  2. Если символ из набора “направлений движения”, то проверяем содержимое элемента массива someArray[iCheckCounter - 1] (т.к. у нас индексация начинается с 0) и если установлен бит, соответствующий направлению, идём дальше.
  3. Так двигаемся до тех пор, пока iCheckCounter не достигнет значения 100;

Алгоритм получили, давайте “накидаем” генератор флагов:

Ограничение на длину флага установил в 20 символов, мне показалось, что этого будет достаточно. Запускаем, смотрим результат:

Выбираем самый короткий, проверяем в программе и на сайте - Correct!


Файлы заданий