Друзья, продолжаем публиковать решения нашего CTF-марафона! В нем было пять уровней сложности, в каждом по пять заданий — всего 25 заданий. Каждую неделю мы выкладываем по 5 решений — сегодня рассказываем о четвертом уровне сложности. Предыдущие уровни вы можете изучить здесь: часть 1, часть 2, часть 3.

Результаты марафона мы подвели в начале апреля, но задания все еще доступны — и вы можете попробовать решить их для себя.

CTF-2023 от «Доктор Веб»: задания уровня Legendary

1. DrYara

Судя по отзывам участников, это задание скорее пятого уровня I Feel No Pain и должно оцениваться в 500 баллов, но у нас получилось, что оно лишь четвертого уровня и оценивается в 400 баллов.

В задании дается скомпилированное yara-правило. Обычно yara-правила нужно только компилировать, а декомпилировать их не приходится, поэтому для этого подходящего инструментария, как правило, нет. Конечно, есть проекты на GitHub, но, как оказывается, что версия яры, для которой был скомпилирован этот файл совершенно не подходит для этой софтины. 

Следовательно, давайте будем изобретать велосипед и исследовать файл яры. Какие источники могут в этом помочь: REVERSING 2020 - Rules as code, A look at the YARA compiler's output 

https://bnbdr.github.io/posts/swisscheese/ 

Так-же у нас есть исходники, они всегда при нас: 

https://github.com/VirusTotal/yara 

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

Хидер: 

Стоит заранее определить версию yara — в данном случае должна подойти 4.2.x. Как это понять? Сейчас стоит версия яры 0x13 (19):

Идем в гитхаб яры, шагаем по веткам и ищем ветку, в которой будет аналогичная версия арены (yara/libyara/include/yara/arena.h): 

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

Шаг 1.

Базовую структуру файла, которую необходимо составить, можно увидеть в бинарном файле:

Если мы знакомы с ярой, то сразу видим символы переменных, начинающихся с $, а также некие значения, с разделителем 00. Так как первая переменная называется magic, а ее значение равно PK, то можно определить, что мы будем иметь дело с zip архивом:  

Тот же трюк провернем для всех переменных: 

В результате получается:

Затем нужно сопоставить названия со структурой zip-архива, а точнее его хидера:

В результате почти все известные переменные указаны — осталось еще добавить строки, но мы пока не знаем как 

Шаг 2. 

Переходим на следующее правило, MeDoesWat, определяем его метадату просто читая данные после разделителя. Само правило содержит только некую переменную $wat , но значение не задано явно — для того, чтобы узнать его, необходимо смотреть байткод.

Ищем точку входа, тут можно отдебажить, сопоставить по структурам в статике или вообще собрать свою яру с принтами и дебагом через визуалку: 

Начинаем парсить байткод, лично я просто собрал свою яру и нашел точку входа для байткода яры:

Далее этот байткод нужно прочитать. Если читать его визуалкой, то получится подстроиться под каждое новое условие, которое нужно будет выполнить для удачного запуска yara.

Каждая отработка опкода содержит под собой вызов дебажного принта, который, при желании, можно заставить работать:

Есть вариант сделать дебажный принт путем find & replace:

В результате получается:

Спарсить опкоды тоже можно через find & replace, а не через скрипт:

Результат получается не идеальный, но читаемый. Если знать точку входа, то можно будет считать оффсеты и корректно пропарсить аргументы к опкодам, например:

Так же можно остановиться опкоде и посмотреть, что он делает — например, обратить внимание на опкод filesize:

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

Судя по опкодам, которые идут после, затем пушится размер и происходит что-то еще — скорее всего, так или иначе поверяется размер файла, по которому будет отработано правило. Опкод OP_INT_GT (скорее всего, это int_greater) сравнивает два регистра: в r1 лежит размер файла, в r2 — 0x100000, число для сравнения.

Следовательно, файл должен стать размером больше 1 МБ. Бьем нули:

Таким же методом находим референс на следующую конструкцию (есть еще интересные опкоды типа OP_BITWISE_XOR, но про них в третьем шаге): 

https://github.com/VirusTotal/yara/blob/c0a2b5a6c37f6016ed5ae5a424e7ae017e7da34d/libyara/include/yara/re.h 

Получаем нечто подобное: 

01 ?? ?? ?? ?? ?? ?? 02 ?? ?? ?? ?? ?? AB CD EF ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 00 00 ?? ?? ?? ?? ?? ?? 00 ?? ?? ?? ?? ?? 6C ?? ?? 20 44 41 ?? ?? ?? ?? ?? ??  

**Почему это wat? 

Теперь, сверяем имеющиеся у нас константы и пытаемся выставить их так, чтобы все это попадало под эту регулярку.  Почему именно так? Мы можем поискать подобные байты в уже имеющихся выражениях и заметим следующую картину: 

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

Правило MeDoesWat теперь отрабатывает: 

Еще можно вспомнить, что первое правило у нас так и не отработало, чтобы не делать кучу лишних скринов скажу так, я прошелся по разным опкодам, заинтересовали меня OP_FOUND_AT и OP_FOUND_IN. Опкод OP_FOUND_AT отрабатывает без всяких нареканий, так-что будем смотреть в OP_FOUND_IN: 

Опкод подгружает в себя регистры r1, r2, r3, со значениями 0xFFFABADAFBAFAFF, 0x200 и неким указателем. Указатель привел меня на строки VOLGA, DA_CRACK и переменную $extra, но они по какой-то причине не отрабатывали. И тут находим такой вот дефайн: 

Судя по всему правило скомпилировалось с ошибкой и теперь просто не хочет работать, так как опкод OP_FOUND_IN прекращает работу из-за значения 0xFFFABADAFBAFAFF. Мы уже нашли нужные локации для наших строк, так что понятно, что диапазон должен быть выставлен такой: 0 … 0x200. 

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

Теперь осталось напечатать флаг, который печатается правилом MeDoesFlag, с относительно недавно добавленной функций console.log() в яру. Именно тут и вылезает тот самый опкод OP_BITWISE_XOR. console.log() можно определить по этой странной вставочке: 

Именно она, в том числе, передается в OP_CALL при исполнении этого опкода. 

https://yara.readthedocs.io/en/latest/modules/console.html 

Код подгружает значение нашего ключа $key и ксорит его с определенными локациями, которые он подгрузил через OP_PUSH_8. Так как яра не умеет в сложные математические операции и арреи, то она 25 раз пытается найти какие либо совпадения с ключом и проксорить их со значением взятым из файла. Так как длина самого ключа 25 байт, то можем проксорить с нужными значениями, которые были взяты ярой, отследить их можно так-же через дебаг или посчитать в статике: 

Значение из файла всегда подгружается в r1: 

Составляем следующую последовательность: 

Флаг: DrWeb{Y4r4_Rul35_P4DD1N6} 

P.S. Можно еще попробовать различные библиотеки, особенно питоновские, есть шансы, что они смогут распаристь яру по своим локальным переменным, но это не точно. Главное всегда соблюдать нужную версию арены, для исполнения яра скриптов. 

2. My first GUI 

Запускаем таску и видим следующее:

Просто форма и картинка с издевкой... Первое, что надо сделать — проверить, на чем написан семпл:

Проходимся по функциям в IDA, находим следующее, тут мы видим странные константы и вызов функции после (до функции можно дойти изучив структуру MFC или пройдясь по импортам с конца): 

Далее мы весьма быстро находим простую проверку на дебаг: 

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

Далее стоит обратить внимание на загрузки неких ресурсов:

Затем это передается в функцию с некоторым алгоритмом. Судя по константам и сдвигам, это очень напоминает base64 — тут же задается Algid для шифрования после:

Полученное значение используется для вычисления ключа RC2:

Теперь необходимо попробовать это отладить — расшифровать мусор.

Далее — внимательнее рассмотреть получения ключа. Можно увидеть, что за дефолтным алфавитом base64 следует другой:

IDA изначально неправильно определила алфавит, обозвав это одним целым массивом в 128 байт. Говорим IDA, как должно быть, и патчим код, меняя адрес алфавита на второй:

Опять ничего не получилось... 

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

Как только мы поменяем алгоритм на RC4 - мы получаем наш флаг: DrWeb{m3g4_MFC_m0zg} 

Скриптец на питоне для решения таска:  

Флаг: DrWeb{m3g4_MFC_m0zg} 

3. std::GetGud 

Если запустить исполняемый файл, он просит ввести флаг:

Перед открытием в IDA проверяем, что это за зверь: 

Также надо посмотреть на бинарник — он 64-х битный. При открытии IDA сразу обращают на себя внимание std::-функции.

Этот код можно распарсить на структуры и переименовать их — но здесь мы опишем быстрое решение таска, а не реверс с восстановлением рабочих сорсов.

Для неопытного реверс-инжинера сложности могут возникать из-за обильных конструкций, которые визуалка компилирует при работе с C++.

Если пройтись по функциям, можно наткнуться на явную инициализацию ключа:

Return идет в следующую функцию, которая занимается расшифровкой — строк не видно, и, судя по всеми, именно она их и расшифровывает:

Оказалось это не ключ, больше похоже на строку, которую будем расшифровывать.

Функция расшифровки вызывается несколько раз, с разной инициализацией зашифрованных данных. Расшифрованная строка:

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

Натыкаемся на весьма странную пелену математических операций с байтами: 

Его можно чуть-чуть восстановить:

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

Флаг: DrWeb{Tr0j4n.S1gg5n.1337} 

4. So low, so deep 

Открываем файл и видим следующее: 

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

Чтобы сильно не трудиться над декомпиляцией этого кода используем онлайн инструмент sharplab.io: 

Подгружаем туда наш код и видим подобные ошибки: 

Если присмотреться, то можно понять, что ошибки возникают в частности в try-catch блоках, из-за весьма странных и невалидных инструкций 

Больше всего вопросов вызывают эти два фрагмента:

Но легче, возможно, будет в целом вырезать все try-catch и поправить стек. Почистив код, можно получить следующее:

Ключ находится здесь — его можно опознать по IL-коду, сравнив с C# кодом:

Декодируем флаг. 

Флаг: DrWeb{5h4rp_bu7_l0w_LvL} 

5. OhMyZip 

Открывая этот файл у себя, все, что нам высветится это сообщение “LoL, you have been hacked”. Посмотрев на файл можно понять, что этодолжен быть самораспаковывающийся 7z архив, который явно не предпологался для запускания подобного. 

Можно поискать в файле аномалии — особенно если есть опыт в вирусном анализе. В исполняемом файле оказывается совершенно лишняя секция:

Вот эта самая секция, отделенная паддингом из нулей: 

Сам «малварный» код обфусцирован вермишелью из джампов и дешифровки, и смотреть его в статике не имеет смысла — особенно если он что-нибудь дропает:

А вот тот самый кусок кода, который вызывается недалеко от точки входа и который вызывает наш “малварный” код из лишней PE секции:

Просто бегая по инструкциям находим адрес, с которого качается пейлоад: 

Можно скачать этот пейлоад и найти PowerShell -скрипт, который сообщал “LoL, you have been hacked”. Если запустить скрипт, то он не выведет флаг — он падает в конструкции try-catch:

Необходимо найти кусок кода, который здесь вызывается неявно, и декодировать его — в данном случае это функция “[jdasnklgsnfglsn.dfgsfdgsv3c]::fdsvfbsdfbsfd()”.

Раскодировав этот кусок, можно найти там цельную функцию C#, которая отвечает за загрузку второго пейлоада:

В загруженном файле видим такую строку в base64, окруженную “_”:

Чтобы заставить код расшифровываться, а не выплевывать ошибку, придется подумать, мы имеем bas64 строку, которая совпадает по размеру (после декодировки) для ключа шифрования, следовательно, убираем лишние функции для кодировки, которые мешали нам запустить код и запускаем: 

После расшифровки получается такой кусок данных в base64:

Декодируем Base64 и получаем такой шеллкод, опять на PowerShell:

И далее запускается видео:

И высвечивается флаг:

Флаг: DrWeb{p0w3r5h3ll.d0wnl04d3r.65536} 

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