Разбор задания ICQST с SPbCTF's Student CTF 2020 Quals
2020-11-16 01:48
На прошедшем в воскресенье SPbCTF's Student CTF 2020 Quals был ряд заданий, с которыми справилось лишь несколько команд. В данном посте будет разобрано задание категории Reverse, которое наша команда решила первой.
После скачивания мы получаем некоторый deb-пакет, внутри которого находится *.dylib файл под архитектуру ARM64. Судя по всему, это динамическая библиотека для IOS-совместимых устройств.
Посмотреть код в динамике вряд ли получится, поэтому запускаем IDA PRO и начинаем статический анализ.
Из описания задания можно понять, что перед нами некоторая малварь, которая требует ввести верный пинкод, и тогда наши данные не отправятся на сайт NSA. Найденный код инициализации это подтверждает.
Кода и функций не так много, так что их физически можно просмотреть. Внутри функции [Path go:] можно найти блок, в котором происходит некоторая расшифровка и, судя по строчкам, получается флаг.
Посмотрим на функцию расшифровки:
Это алгоритм RC4, где ключом является переданный нами аргумент, а расшифровывается некоторый массив байт по адресу 0x7e8d.
Отлично, осталось понять, как формируется ключ, который лежит в переменной path.
Общая концепция формирования пути довольно простая: пользователь нажимает цифру на клавиатуре, и эта цифра попадает в переменные path и passcode. Однако, от каждого нажатия вызывается функция [Path go:], которая и содержит блок с расшифровкой флага.
В данную функцию передаётся значение нажатой цифры (то есть, число от 0 до 9).
Разберём данную функцию подробнее:
Изначально мы рассчитываем некоторый индекс и берём значение по данному индексу в переменной map. В расчёте индекса принимает участие значение переменной pos и значение переменной pressedDigit — нажатой пользователем цифры. Для того, чтобы понять, как именно будет сделан первый и последующие вызовы данной функции, нам надо узнать изначальные значение map и pos. Для этого мы можем открыть функцию [Path initWithCapacity:].
В ней мы видим, что map инициализируется как некоторый массив байт с помощью байт из памяти, а остальные переменные инициализированы нулём, кроме переменной allowed.
Теперь мы можем продолжить анализ функции go и разбирать блоки, которые вызываются после проверки нажатой цифры. Первым таким блоком является проверка, что цифра равна 5.
Данный блок содержит много операций, однако, все они довольно простые и разобрать их можно без большой сложности. В самом начале блока мы добавляем строку "5" к переменной path и после этого получаем хэш. Алгоритм хэша довольно простой, и его можно просто скопировать и запустить.
После того, как был получен хэш, мы обновляем значения в переменной map с индекса равного новой позиции, умноженной на 10. Обновление происходит с помощью выполнения операции XOR над очередным элементом в цикле. Далее мы устанавливаем новую позицию и обновляем значение cur, добавляя единицу.
Рассмотрим блок, когда нажатая цифра не равна 5 и не равна 0.
В данном блоке сразу же выставляется новая позиция, и далее проверяется, что текущая длина переменной path не равна нулю. Если это правда, то происходит следующая проверка, логика которой заключается в том, что если последняя цифра в path и цифра, которая была только что введена, дают в сумме число 10, то последняя цифра удаляется.
В противном случае, мы просто добавляем эту цифру в конец как строку и идём дальше.
Теперь рассмотрим, что происходит, когда введенная цифра равна нулю.
Самая первая проверка сравнивает две переменные, и мы помним, что переменная allowed имеет значение 2, а переменная cur увеличивается на 1, только когда была нажата 5, то есть, мы уже видим, что нам надо нажать две "5", чтобы активировать данный блок, причём не обязательно нажатия должны быть подряд.
Далее в строку добавляется символ нуля, после чего происходит установка нового значение переменной "flags", и по итогу это значение должно достичь 7, а curr нуля.
Из кода становится понятно, что мы должны зайти в данный блок 3 раза после того, как два раза зашли в блок 5.
То есть, нам нужно обойти некоторый граф таким образом, чтобы сначала зайти в две пятёрки (необязательно подряд), а после зайти в три нуля (не обязательно подряд). И тогда мы получим верный маршрут, который и является ключом для RC4.
Наш граф хранится в памяти, и мы можем его запросто достать.
Так же важно не забыть, что в памяти хранится массив элементов типа QWORD, однако в алгоритме идет работа с 4-байтными числами, поэтому старшие 4 надо обрезать.
Будем обходить данный граф с помощью поиска в ширину, добавив несколько ограничений. Для начала реализуем функции подсчета контрольной суммы и обновления нашего графа. Их почти досимвольно можно взять из декомпилированного кода в IDA.
Важно не забыть добавить & 0xFFFFFFFF в функции h, иначе мы будем неправильно обрабатывать переполнение! Далее начнем писать сам поиск. Исходя из условий корректности, которые мы видели выше, для хранения состояния нам надо знать текущее поле, значение переменной state, набранный путь (его мы выведем в конце, а также будем использовать в функции контрольной суммы), количество набранных пятерок и нулей (их можно посчитать из переменной path, но для удобства будем передавать их отдельно).
Таким образом, получим следующее начало функции:
Далее нам нужно перебрать очередную цифру, которую мы нажмем, от 0 до 9. При этом сразу начнем отсекать варианты, которые нам не подходят.
Если мы еще не нажали две 5, то нельзя нажимать 0;
Если уже нажали две 5, то нельзя нажимать 5;
Будем считать, что в правильном ответе нет двух цифр подряд, сумма которых 10, чтобы уменьшить количество вариантов перебора (таким образом мы запрещаем стирать последнюю цифру);
Сразу проверим, что мы не выйдем за границы графа, нажав такую цифру.
Добавим новую цифру к пути, пересчитаем state. После этого проверим, что новый стейт "хороший" — не выходит за границы нашего графа. И наконец, если была нажата 5, то обновим наш граф.
Осталось лишь обновить количество нажатых 0 и 5 и запустить новую ветвь перебора.
Полную версию решения можно найти на нашем github.
Запустив решение, мгновенно получаем единственный подходящий код — 78772654435671798113109698668084720.
Используем этот код как ключ для RС4 и расшифровываем наш флаг.
Задание оказалось довольно интересным и неплохо проверяло базовые навыки статического анализа кода.