На конференции OffZone 2025 мы впервые провели наш CTF— Solar 4RAYS FlagHunt. Подготовили для участников 11 тасков разной степени сложности. Всего в соревновании приняли участие более 200 человек, а 89 из них успешно решили хотя бы одну задачу. В этой статье мы подробно разберем способы выполнения заданий.

Miracle elixir

Категория: Reverse

Сложность: Easy

Текст задания:

Команда реагирования на инцидент принесла пачку файлов. Даже удалось записать трафик. Посмотрите, может, что-то удалось украсть хакерам?

В качестве дополнительных файлов были также дамп трафика и *.beam-файлы.

Решение

Предлагаем сначала посмотреть на дамп трафика dump.pcap. Когда его откроем, увидим трафик системы erlang distribution. В контексте этого протокола данные передаются в открытом виде, а wireshark позволяет разобрать конкретные значения передаваемых переменных. Если просмотреть трафик полностью, то можно увидеть взаимодействие двух нод: ihost@172.22.1.3 и cc@172.22.1.2. В начале происходит установка соединения «ihost» с «cc», после чего — выполнение команд. Команды идут с хоста «cc» на хост «ihost». Посмотрев на команды, в конце увидим чтение файла /secret_data/flag1.txt и отправку его содержимого. Содержимое выглядит как нечитаемые данные. Энтропия, конечно, невысокая, но и данных не много.

Энтропия содержимого файла flag1.txt
Энтропия содержимого файла flag1.txt

Так как задание на реверс, переходим к beam-файлам. Этот формат содержит байт-код для виртуальной машины erlang. По названию файлов приходим к тому, что это байт-код, сгенерированный эликсиром. Для декомпиляции есть прекрасный проект на GitHub. Рассмотрев разные файлы, приходим к выводу, что файлы при отправке шифруются. А ключ устанавливается заранее.

Обработка команды выгрузки файла
Обработка команды выгрузки файла

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

Команда передачи ключа
Команда передачи ключа

Осталось совсем немного, у нас есть функция шифрования (Crypto.encrypt), но нет функции расшифровки (Crypto.decrypt). Но если внимательнее посмотреть, то данные шифруются при помощи обычного гаммирования. Следовательно, можно использовать Crypto.encrypt и для расшифровки.

Потому берем iex и используем возможность вызывать функции из скомпилированных модулей. На выходе получаем флаг:

4rays{3lix1r_i5_co0l_and_bad@ss_jo2av30sm}

Persist hunters

Категория: Forensics

Текст задания:

Пока я устанавливал свои любимые приложения, сработал антивирус. Не помню, на что именно, но теперь что-то будет запускаться самостоятельно без моего ведома. Я смог собрать некоторые артефакты с системы — помоги найти все закрепленные вредоносные импланты. Для всех 5 заданий по форензике используется один и тот же архив Triage_Win10.zip, а флаг содержит в себе номер задания.

Для выполнения задания участникам предоставлялся архив с некоторыми артефактами «зараженной» системы Windows 10, среди артефактов были: MFT, файлы реестра, история браузеров, Prefetch, файлы планировщика задач, а также WMI, WER и WDI. В данных артефактах можно было обнаружить пять «вредоносных» нагрузок, которые были тем или иным образом закреплены в системе и могли запуститься при выполнении определенных условий (например, при перезагрузке системы).

Чтобы решить задание, требовалось поискать в интернете или спросить своего ИИ-ассистента, что такое Windows Persistence, а далее — идти по порядку. Угадайка? Нет, ведь это реальная рутина форензика — проверить, что в системе нет вредоносных имплантов в автозагрузке.

Уровень 1: раздел реестра Autoruns

Сложность: Easy

Вероятно, самый известный способ автозагрузки, которым пользуются злоумышленники, если не боятся быстрого обнаружения, это разделы реестра Run/RunOnce, которые есть в SOFTWARE и NTUSER.DAT.

SOFTWARE\Microsoft\Windows\CurrentVersion\Run

Так, при проверке раздела Run в SOFTWARE вы могли обнаружить ключ «1», который выполняет запуск закодированной в Base64 команды PowerShell.

Для получения флага оставалось выполнить следующие шаги:

RegistryExplorer > Значение из Run > CyberChef > From Base64 > Remove null bytes > Flag

Значение раздела Run
Значение раздела Run

Уровень 2: служба Windows

Сложность: Easy

Немного повысим сложность: теперь требуется найти вредоносную службу в Windows:

SYSTEM\ControlSet001\Services

В результате анализа командлайнов всех служб вы можете обнаружить, что в системе установлена служба WinDefender (мимикрия под Windows Defender), которая запускает cmd-команду, содержащую Base64.

Шаги для получения флага:

RegistryExplorer > Командлайн службы WinDefender > CyberChef > From Base64 > From Base64 > Flag

Значение раздела Services
Значение раздела Services

Уровень 3: директория Autoruns

Сложность: Medium

Альтернативой раздела Run являются директории автозагрузки:

C:\Users\<User>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup

C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp


Смотрим MFT и находим start.bat в автозагрузке в ProgramData. Открываем файл и забираем флаг.

Шаги для получения флага:

start.bat > CyberChef > From Hex > XOR > Flag

Q: А как мы откроем файл, если у нас лишь MFT?

A: Резидентные файлы NTFS — если файл занимает меньше 1 КБ, то он хранится в самой записи MFT.


MFT-запись файла start.bat
MFT-запись файла start.bat

Уровень 4: WMI

Сложность: Medium

Детские игры закончились. WMI — мощный инструмент администрирования и достаточно редкий способ закрепления вредоносной нагрузки. WMI позволяет подписываться на события системы для последующего запуска определенных команд. Основным методом анализа является поиск созданных в системе EventFilter и EventConsumer.

Для этого требуется файл базы данных WMI — OBJECTS.DATA:

C:\Windows\System32\wbem\Repository\OBJECTS.DATA

По данному файлу требуется поискать EventFilter.Name и EventConsumer.Name, в результате будет обнаружено 2 подписки: SCM Event Log (легитимно) и SecurityStatusWmi (что-то инородное)

Следующий шаг — поиск по OBJECTS.DATA команды, которую выполняет SecurityStatusWmi.

Шаги для получения флага:

Командлайн SecurityStatusWmi > CyberChef > From Base64 > From Hex > XOR > Flag

Командлайн SecurityStatusWmi из файла OBJECTS.DATA
Командлайн SecurityStatusWmi из файла OBJECTS.DATA

Уровень 5: Планировщик задач

Сложность: Hard

Вы внимательно исследовали всю директорию «C:\Windows\System32\Tasks»? Увы, ранее на PhD мы рассказывали, что это не имеет смысла, ведь все задачи давно хранятся в реестре Windows (выступление, статья). Все, что вам требовалось, это исследовать значения раздела реестра TaskCache:

SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache

Самый простой способ — использовать наш плагин для Registry Explorer, который доступен бесплатно в нашем GitHub, ведь на задачу даже сработал встроенный в плагин Alert (поставьте звезду в гите, если мы подарили вам легчайшие офкоины).

Как решить без нашего плагина? Тяжко, но возможно:

  1. В системе есть 2 задачи «\Microsoft\Windows\Diagnosis\Scheduled» и «\Microsoft\Windows\Diagnosis\Scheduler», которые, согласно DynamicInfo, созданы одновременно 06.08.2025 08:29:02+00:00.
  2. При анализе значений в дереве TaskCahce можно обнаружить, что раздел Scheduled создан в 2019 году (т. к. это системная задача), а Scheduler — 11.08.2025 19:23:50+00:00, в окрестности размещения прочих вредоносных нагрузок.
  3. Далее требуется найти Actions задачи {C3944556-16CF-467E-89E3-29D4BFD3EC5A} в разделе TaskCahce\Tasks, где вы сможете обнаружить командлайн:

cmd.exe /c start http://localhost:3000/?ccmmdd=&lt;Payload&gt;

Либо, если вы робот, можно было посмотреть все 210 Actions в hex-формате и найти заветный флаг.

Дальнейшие шаги для получения флага:

Payload > Url Decode > From Base85 > XOR > Flag

Вывод плагина 4RAYS TaskCache в RegistryExplorer
Вывод плагина 4RAYS TaskCache в RegistryExplorer

Homyak Tap

Категория: PPC

Сложность: Easy

Разбор вашего любимого задания:

Сразу при переходе на страничку видим большую зеленую кнопку, которая увеличивает число кликов. Для получения флага необходимо кликнуть 8888 раз.

Для этого посмотрим, что происходит, когда мы нажимаем на кнопку.

Запрос
Запрос

При нажатии на кнопку мы отправляем запрос с координатами клика. Можно также заметить, что при нажатии кнопки в одном месте сервер обнуляет клики.

Чтобы решить данное задание, воспользуемся Python. Для этого напишем следующий код:

    
def send_random_tap(api_url: str, homyak_value: str):
    x = random.randint(600, 900)
    y = random.randint(600, 900)
    payload = {"x": x, "y": y}
    cookies = {"homyak": “[your cookie]”}
    requests.post(
        f"{api_url}/api/tap",
        data=json.dumps(payload),
        cookies=cookies,
        headers={"Content-Type": "application/json"}
    )
    

После достижения необходимого количество кликов можно получить флаг по кнопке Flag.

Homyak Exchange

Категория: PPC
Сложность: Easy

По кнопке Exchange попадаем на Homyak Exchange. Тут мы видим волатильный актив $HOMYAK, на котором нам предлагается заработать 500 000.

Для этого нам необходимо торговать фьючерсами. Всего доступно 3 фьючерса на 3–5–7 минут.

Обратимся к консоли, чтобы посмотреть, что происходит. Тут видно, что биржа каждые две секунды получает курс актива и время до экспирации фьючерсов, отправляя json { t:0 }.

Попробуем понять, что мы получаем при отправке другого значения t. Увидим, что при отправке отрицательных значений мы получаем курс в будущем.

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

    
def get_futures():
    def get_price(t):
        cookies = {'homyak': '[your_cookie]’} 
        params = {'t': t}
        response = requests.post(url, json=params, cookies=cookies)
        return response.json()

    data = get_price(0)

    while True:
        for f, t in data.get("futures").items():
            print(f, get_price(-(t//2)).get('price'))
        time.sleep(min(data.get("futures").values())//2)

if __name__ == "__main__":
    get_futures()
    

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

Также можно воспользоваться тем, что в случае продажи на счет начисляются кредитные деньги.

После достижения необходимого количество кликов можно получить флаг по кнопке Flag.

Reactive Homyak

Категория: Web/MISC
Сложность: Easy

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

Заходим в devtools, и смотрим, какие файлы были подгружены.

Видим некую admin.js, упоминания которой нет на фронте. Пробуем перейти на /admin — нас отправляет на главную страницу, поскольку страница еще в разработке.

Проходимся дальше по исходникам и видим упоминанийе API-ключа для тестирования.

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

Doppelgänger

Категория: OSINT

Сложность: Medium

Нам предоставлен домен некого сайта. Перейдя по нему, видим лишь одностраничник, без какого-либо взаимодействия с API. Открытых портов, помимо SSH и HTTP/HTTPS, на сервере нет. Попытка перебора страниц или брутфорса SSH ни к чему не приводит.

На сайте также видим упоминание некого Secret, однако доступ к нему запрещен.

Возвращаемся к описанию задания — «две звезды связаны невидимой нитью». Звезды в интернете могут ассоциироваться с серверами или IP-адресами. Нужно найти некую связь между двумя хостами. Получаем IP-адрес домена и обращаемся к Censys, FOFA или иной платформе для сканирования интернета.

На порте 443 (HTTPS) обнаруживаем SSL-сертификат, по хешу или иному уникальному значению которого можно искать хосты в интернете с таким же сертификатом.

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

Rusty love

Категория: Reverse

Сложность: Hard

Нам представлен бинарный файл, написанный на Rust. Можем понять это по строкам.

Бинарный файл требует на вход флаг. Введенный флаг проходит определенные проверки, а затем выводится сообщение о правильности или неправильности данного флага. Если флаг неправильный, выводится сообщение «Ooooowieeeeeee! You should try harder!!». Находим данное сообщение в IDA, по кросс-рефам попадаем в функцию main.

Эта строка выводится в ветке false при проверке флага. В декомпиляторе видим символ }. Вероятно, этот участок кода отвечает за проверку формата флага.

Попробуем потрейсить до этого участка в дебаггере. Замечаем функцию sub_20728, в которую передается указатель на какую-то последовательность байтов, а также число. После исполнения этой функции в памяти можно увидеть строку 4rays{.

На этом моменте гипотеза подтверждается. Участок кода с проверкой символа } отвечает за проверку формата флага. Флаг выглядит так: 4rays{xxx…xxx}. Однако на данном этапе мы еще не знаем точный размер флага (чисто технически размер флага не нужен при решении задачи, однако мы захотели добавить его в код, чтобы направить решающих в правильную сторону). Также о функции sub_20728 можно сделать предположение, что, вероятно, она как-то связана с шифрованием строк. Эта гипотеза подтвердится, если после инициализации «потыкать» кросс-референсы на эту функцию.

Далее происходит проверка размера строки:

Как можно видеть, размер флага должен быть 10, не считая самой обертки 4rays{}.

Далее видим в декомпиляторе следующую проверку:

Похоже, что здесь проверяется какая-то чек-сумма. Если заглянуть в саму функцию 1ef6e, увидим следующую картину:

Действительно! В этой функции подсчитывается какая-то чек-сумма, также видим, что перед этим расшифровывается строка.

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

Зная сам алгоритм, можно теперь восстановить флаг. Для этого будем использовать SMT-солвер. Возьмем z3 для нашей задачи.

Для начала символизируем 10 байтов, создадим объект солвера и зададим ограничения для наших 10 байтов (это цифра или буква в верхнем регистре).

    
chars = [BitVec(f'c{i}', 8) for i in range(10)]
    s = Solver()

    for c in chars:
        s.add(Or(
            And(c >= ord('0'), c <= ord('9')),
            And(c >= ord('A'), c <= ord('Z'))
        ))

    val = BitVecVal(0, 64)
    

Далее дописываем сам алгоритм, получаем примерно следующее решение:

    
from z3 import *

helper = b"{T0_B3_H0N3ST_175_N0T_4_K3Y_TRY_H4RD3R}"
def solver(target_val):
    chars = [BitVec(f'c{i}', 8) for i in range(10)]
    s = Solver()

    for c in chars:
        s.add(Or(
            And(c >= ord('0'), c <= ord('9')),
            And(c >= ord('A'), c <= ord('Z'))
        ))

    val = BitVecVal(0, 64)
    xor_acc = 0x5A

    for i, c in enumerate(chars):
        # Если цифра, вычитаем 0x30, иначе вычитаем ord('A')
        digit_val = If(And(c >= ord('0'), c <= ord('9')),
                       c - ord('0'),
                       c - ord('A') + 10)
        digit_val ^= (((xor_acc >> (i % 8))) + helper[i] << (i % 8)) & 0xFF
        val = (val * 36 + ZeroExt(56, digit_val)) & 0xFFFFFFFFFFFFFFFF

    s.add(val == target_val)
    
    # Решение найдено
    if s.check() == sat: 
        m = s.model()
        return ''.join(chr(m[c].as_long()) for c in chars)
    else:
        return None

if __name__ == "__main__":
    target = 0x5869e06025c4ad
    flag = solver(target)
    print(f"Flag:", flag)
    

SMT-солвер решает для нас систему уравнений, и мы получаем флаг 4rays{W3L0V3RU57}.

Вот и все! Надеемся, вы получили удовольствие, решая наши таски. Мы уже работаем над 4RAYS FlagHunt 2. Следите за обновлениями в нашем телеграм-канале или здесь, в блоге!